Skip to content

fix(erc8128): add setIfNotExists to SecondaryStorage, use atomic nonce consumption#2

Closed
jacobot01 wants to merge 43 commits into
slice-so:erc8128from
jacobot01:fix/erc8128-setnx-nonce
Closed

fix(erc8128): add setIfNotExists to SecondaryStorage, use atomic nonce consumption#2
jacobot01 wants to merge 43 commits into
slice-so:erc8128from
jacobot01:fix/erc8128-setnx-nonce

Conversation

@jacobot01

Copy link
Copy Markdown
Collaborator

Problem

When using the erc8128 plugin with Redis as secondaryStorage, nonce consumption does GET + SET (2 round-trips) instead of a single atomic SET NX.

Changes

packages/core/src/db/type.ts

  • Added optional setIfNotExists method to the SecondaryStorage interface

packages/better-auth/src/plugins/erc8128/nonce-store.ts

  • createSecondaryStorageNonceStore now uses storage.setIfNotExists() when available (single atomic SET NX command)
  • Falls back to existing get + set pattern for storage implementations that don't support it

Impact

  • Halves nonce verification round-trips (2 → 1)
  • Eliminates race window between GET and SET
  • Fully backwards compatible (optional interface method with fallback)

jacobot and others added 30 commits March 2, 2026 13:04
…gnatures

The invalidation endpoint sets notBefore = floor(now). If a replayable sig
was created in the same second, created < notBefore is false (X < X).
Using now+1 ensures same-second sigs are invalidated.
…nonce + invalidation

When secondaryStorage (e.g. Redis) is configured, nonces and invalidation
records are now stored there by default with TTL-based auto-cleanup.

Set storeInDatabase: true to dual-write (DB + secondaryStorage), using
secondaryStorage as a fast read-through layer.

Follows Better Auth's session.storeSessionInDatabase pattern.

New files:
- invalidation-store.ts: InvalidationOps abstraction (DB / secondaryStorage / dual)

Modified:
- nonce-store.ts: added createSecondaryStorageNonceStore()
- index.ts: added storeInDatabase option, replaced inline DB queries with
  InvalidationOps abstraction, lazy-initialized storage selection
…tion stores

Docs:
- Replaced 'Verification Cache' section with 'Storage' section
- Documents storeInDatabase option, storage resolution table
- Links to BA's session.storeSessionInDatabase pattern
- Added storeInDatabase to options table

Tests (22 new):
- nonce-store: secondaryStorage nonce store (consume, TTL, error handling),
  adapter fallback on error
- invalidation-store: DB ops (upsert, find, normalize), secondaryStorage ops
  (store, retrieve, TTL, error swallowing), dual ops (dual-write, SS-first
  read, DB fallback)
Previously getNonceStore() fell back to DB-only when storeInDatabase
was true. Now it creates a dual nonce store that consumes from both
secondaryStorage (fast path) and DB (durability), matching the
invalidation store's dual-write behavior.

Also adds createDualNonceStore() and 2 new tests for it.
- add storage capability detection (secondary-storage | database | none)
- disable /erc8128/verify and /erc8128/invalidate in no-storage mode (404)
- expose discovery capabilities in /.well-known/erc8128
- enforce replayable routes require storage (401 replayable_requires_storage)
- add process-local in-memory nonce + invalidation fallback stores
- make nonce store singleton per plugin instance
- tighten docs quickstart and storage matrix for stateless behavior
- add memory nonce store tests and update discovery expectation

Tested: 108/108 passing across erc8128 plugin suites
@jacopo-eth jacopo-eth deleted the branch slice-so:erc8128 March 12, 2026 16:47
@jacopo-eth jacopo-eth closed this Mar 12, 2026
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.

2 participants