Skip to content

fix: prevent sentinel tx replays#392

Open
matthias-wright wants to merge 2 commits into
veridise-audit-april-2026from
m/sentinel-tx-replay-protection
Open

fix: prevent sentinel tx replays#392
matthias-wright wants to merge 2 commits into
veridise-audit-april-2026from
m/sentinel-tx-replay-protection

Conversation

@matthias-wright
Copy link
Copy Markdown
Contributor

Hardens the sentinel transaction introduced in #369 against replay attacks.

apply_sentinel_action now runs these five checks in order:

  1. signer == configured governance address (unchanged)
  2. validator_id matches the validator's current per-process session id
  3. recent_block_hash is on this validator's canonical chain, and expires_at_block − reference ≤ 256 (matches EVM BLOCKHASH lookback)
  4. canonical head ≤ expires_at_block
  5. try_advance_admin_nonce(envelope.nonce) — strict-greater, so out-of-order delivery silently drops stale payloads instead of blocking later ones

validator_id is 32 random bytes minted by Whitelist::new(); both it and admin_nonce live in memory only and reset on restart. A fresh validator_id invalidates any in-flight captured payload from the previous incarnation, so resetting admin_nonce = 0 after restart is safe.

The nonce advance is intentionally placed after the action-specific expires_at check, so a governance typo doesn't burn a nonce and force a refetch + re-sign.

Governance can't sign anything until it knows the live validator_id and the current admin_nonce. Two new unauthenticated read endpoints on the ops server expose them:

  • ops_getValidatorIdB256
  • ops_getAdminNonceu64

The middleware bypasses auth for these two method names.

This is the shim contract that represents the ABI:

  contract RpcShim {
      function whitelistKey(
          address target,
          uint64 expiresAt,
          bytes32 recentBlockHash,
          uint64 expiresAtBlock,
          bytes32 validatorId,
          uint64 nonce
      ) external {}

      function revokeKey(
          address target,
          bytes32 recentBlockHash,
          uint64 expiresAtBlock,
          bytes32 validatorId,
          uint64 nonce
      ) external {}
  }

@matthias-wright matthias-wright requested a review from cdrappi as a code owner May 20, 2026 15:14
@github-actions
Copy link
Copy Markdown
Contributor

Implements comprehensive sentinel transaction validation with replay protection for ops authentication.

Phase 2

  • crates/rpc/rpc-layer/src/signature_auth_layer.rs:359nonce.expect("nonce is required when needs_nonce is true") lacks #[allow(clippy::expect_used)] attribute. The expect is justified (nonce guaranteed to be Some when needs_nonce=true) but needs the allow annotation per Seismic CI rules.
  • crates/rpc/rpc-layer/src/signature_auth_layer.rs:405expect("building error response should not fail") lacks clippy allow. This is a justified system-level invariant but needs the annotation.
  • crates/rpc/rpc-layer/src/signature_auth_layer.rs:409expect("system clock before unix epoch") lacks clippy allow. System clock checks are reasonable to panic on but need the annotation.

Phase 3

  • Genesis hash updated correctly in chainspec/src/lib.rs:398 to match the new bytecode.

LGTM with the clippy annotations needed above. The sentinel validation logic is comprehensive with proper authentication, replay protection, and expiration handling. Tests correctly use Seismic chain specs and timestamp multipliers.

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