Skip to content

Gate Access Control

RunTimeAdmin edited this page Oct 30, 2026 · 24 revisions

Gate Access Control

**Referenced Files in This Document** - [gate.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/gate.ts) - [gate.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/gate.test.ts) - [reputation-model.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/reputation-model.md) - [README.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/README.md)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion
  10. Appendices

Introduction

This document explains the gate-based access control implementation in the Sigvara SDK. A gate is an authorization layer that requires an agent to prove identity, demonstrate active standing, and meet a reputation threshold before being admitted. It also manages nonces to prevent replay attacks and ensure each challenge-response pair is used exactly once.

The gate separates authentication from authorization:

  • Authentication proves that the caller controls the claimed agent identity for this specific request.
  • Authorization checks whether that agent is active, holds at least the minimum stake in force now (alpha.9 trusts the stored status), and has sufficient matured reputation.

Gates are intended for off-chain services such as web applications, APIs, or tooling that need reputation-based access control without sending an on-chain transaction for every decision. Each admit does make read calls to the chain: one for the agent's registered key, then the status, the bond held now and the score, and the time the score was last written if you set maxScoreAgeSeconds.

This page describes the gate in 1.0.0-alpha.12, published to npm on 29 October 2026; the gate itself did not change since 1.0.0-alpha.10. These changes to the gate are new since 1.0.0-alpha.9 (listed in the changelog): the constructor refusing a threshold, ttlSeconds or maxScoreAgeSeconds that is not a usable number, the timeoutMs option and the 30 second read timeout, the status, the bond held now and the score being read together, the optional maxScoreAgeSeconds limit with its stale_score refusal, and the refusal of a challenge dated in the future.

Project Structure

The gate logic lives in the SDK package under packages/sdk. The relevant files are:

  • packages/sdk/src/gate.ts: the gate class, nonce store interface, and admission flow.
  • packages/sdk/test/gate.test.ts: tests covering admission, refusal reasons, nonce handling, concurrency, store behavior, and configuration that would fail open.
  • docs/reputation-model.md: background on how reputation scores are computed, matured, and consumed.
  • packages/sdk/README.md: integration notes about audience configuration and shared nonce stores.
graph TB
Client["Client Application"] --> Gate["SigvaraGate<br/>packages/sdk/src/gate.ts"]
Gate --> Verifier["SigvaraVerifier<br/>packages/sdk/src/verifier.ts"]
Gate --> NonceStore["NonceStore<br/>In-memory or custom"]
Verifier --> Chain["On-chain Reputation Data"]
Loading

Diagram sources

Section sources

Core Components

  • SigvaraGate: the main authorization component. It issues challenges, verifies signatures, tracks nonces, checks agent activity, reads matured reputation, and returns structured admission results.
  • NonceStore: an interface for tracking spent nonces. It supports per-process memory storage by default and can be replaced with a distributed store for multi-instance deployments.
  • AdmitResult and AdmitReason: typed responses describing whether access was granted and why it was refused.
  • GateConfig: configuration for RPC connection, contract addresses, chain ID, reputation threshold, audience binding, challenge TTL, optional nonce store, the read timeout timeoutMs, and the optional score-age limit maxScoreAgeSeconds.

Key responsibilities:

  • Issue a signed challenge containing a nonce, timestamp, audience, and DID.
  • Verify the signature against the on-chain verifier.
  • Prevent replay using a nonce store.
  • Check whether the agent is active and still holds the minimum stake in force now (alpha.9 reads the stored status only).
  • If maxScoreAgeSeconds is set, refuse a score written longer ago than that, or never.
  • Compare the matured score against the configured threshold.
  • Return a clear reason for every refusal.
  • Refuse to be built with a threshold that is not a finite number of 0 or more, or a ttlSeconds or maxScoreAgeSeconds that is not a positive number.

Section sources

Architecture Overview

The gate implements a challenge-response protocol with explicit replay protection and reputation gating.

sequenceDiagram
participant App as "Application"
participant Gate as "SigvaraGate"
participant Store as "NonceStore"
participant Verifier as "SigvaraVerifier"
participant Chain as "Reputation Contract"
App->>Gate : "challenge(did)"
Gate-->>App : "Challenge {payload, nonce, expiresAt}"
App->>App : "Agent signs payload"
App->>Gate : "admit(did, challenge, signature)"
Gate->>Gate : "Parse signed payload"
Gate->>Verifier : "verifySignature(did, payload, signature, ttl, audience)"
Verifier-->>Gate : "verified or not"
alt "Invalid proof"
Gate-->>App : "{ok : false, reason : 'bad_proof'}"
else "Valid proof"
Gate->>Store : "consume(nonce, expiresAt)"
Store-->>Gate : "allowed or already spent"
alt "Already spent"
Gate-->>App : "{ok : false, reason : 'replayed'}"
else "Allowed"
Note over Gate,Verifier : isActive, hasMinimumStake and getTotalScore are sent together
Gate->>Verifier : "isActive(did) and hasMinimumStake(did)"
Verifier-->>Gate : "active and bonded, or not"
alt "Not active, or below the minimum stake"
Gate-->>App : "{ok : false, reason : 'not_active'}"
else "Active"
Gate->>Verifier : "getTotalScore(did)"
Verifier->>Chain : "read matured score"
Chain-->>Verifier : "score"
Verifier-->>Gate : "score"
alt "Score too old, only when maxScoreAgeSeconds is set"
Gate-->>App : "{ok : false, reason : 'stale_score', score}"
else "Fresh enough, or no limit set"
alt "Below threshold"
Gate-->>App : "{ok : false, reason : 'below_threshold', score}"
else "Meets threshold"
Gate-->>App : "{ok : true, score}"
end
end
end
end
end
Loading

Diagram sources

Detailed Component Analysis

Gate Evaluation Flow

The gate evaluates requests in a strict order designed to avoid unnecessary chain reads and to provide actionable refusal reasons. The proof is checked first, the nonce is spent second, and the status, the bond held now and the score are read last, so a caller who cannot prove an identity cannot make the gate read any of them. Those reads do not depend on each other and are sent together, and the answer still names standing before score (alpha.9 reads the status, then the score, and does not read the bond). If you set maxScoreAgeSeconds, the time the score was last written is read in the same batch, and a score older than the limit, or never written, is refused as stale_score after standing and before the number.

flowchart TD
Start(["Request received"]) --> Parse["Parse signed payload"]
Parse --> ValidPayload{"Payload valid?"}
ValidPayload --> |No| BadProof["Return 'bad_proof'"]
ValidPayload --> |Yes| Verify["Verify signature and audience"]
Verify --> Verified{"Signature valid?"}
Verified --> |No| BadProof
Verified --> |Yes| Consume["Consume nonce"]
Consume --> Spent{"Nonce already spent?"}
Spent --> |Yes| Replayed["Return 'replayed'"]
Spent --> |No| Active["Read status, bond held now and matured score"]
Active --> IsActive{"Active, and bonded at the current minimum?"}
IsActive --> |No| NotActive["Return 'not_active'"]
IsActive --> |Yes| Fresh{"Score too old? Only if maxScoreAgeSeconds is set"}
Fresh --> |Yes| Stale["Return 'stale_score' with score"]
Fresh --> |No| Threshold{"Score >= threshold?"}
Threshold --> |No| BelowThreshold["Return 'below_threshold' with score"]
Threshold --> |Yes| Admit["Return ok with score"]
Loading

Diagram sources

Section sources

Nonce Management and Replay Prevention

Nonces are central to preventing replay attacks. The gate owns nonce lifecycle because it issued the challenge and knows which nonces are outstanding.

Important behaviors:

  • Each challenge contains a unique nonce inside the signed payload.
  • The gate spends the nonce only after successful signature verification, so invalid proofs cannot burn nonces.
  • The expiration time comes from the signed timestamp plus the gate’s TTL, not from a caller-supplied wrapper field.
  • The default MemoryNonceStore is process-local. For multiple instances behind a load balancer, implement a shared NonceStore, preferably with an atomic consume method. The SDK README shows a Redis store (one key per nonce, SET ... NX with an expiry that outlasts the challenge) and what a SQL one has to do. With only seen and add, two instances that receive one response at the same moment can both admit it.
  • Pruning removes expired entries opportunistically; it does not replace the replay check.
classDiagram
class SigvaraGate {
-verifier : SigvaraVerifier
-nonces : NonceStore
-ttl : number
-inFlight : Set~string~
+threshold : number
+audience : string
+challenge(did) Challenge
+admit(did, challenge, signature) AdmitResult
-consumeNonce(nonce, expiresAt, nowSeconds) boolean
}
class NonceStore {
<<interface>>
+seen(nonce) boolean | Promise<boolean>
+add(nonce, expiresAt) void | Promise<void>
+prune(nowSeconds) void | Promise<void>
+consume?(nonce, expiresAt) boolean | Promise<boolean>
}
class MemoryNonceStore {
-spent : Map~string,number~
+seen(nonce) boolean
+add(nonce, expiresAt) void
+prune(nowSeconds) void
+consume(nonce, expiresAt) boolean
}
SigvaraGate --> NonceStore : "uses"
MemoryNonceStore ..|> NonceStore : "implements"
Loading

Diagram sources

Section sources

Refusal Reasons and Their Meaning

The gate distinguishes five refusal reasons so callers can respond appropriately. stale_score appears only when you set maxScoreAgeSeconds.

Reason Meaning Recommended Caller Action
bad_proof Signature verification failed, wrong agent, wrong audience, or malformed payload. Reject the request; do not treat it as a reputation problem.
replayed Valid signature but nonce already spent. Ask for a fresh challenge; investigate possible replay attempts.
not_active Agent proved itself but is not bonded, suspended, or slashed. "Bonded" is the bond held now against the minimum stake in force now, not the status stored when the agent was admitted (alpha.9 checks the stored status only). Do not suggest earning points; address standing instead.
stale_score Only with maxScoreAgeSeconds: the score was written longer ago than the limit, or never. Tell the operator the oracle has stopped writing, not that the score is low.
below_threshold Agent is active and proved itself, but matured score is below threshold. Inform the agent they need more reputation; include the current score.

Only below_threshold is about reputation. Collapsing all failures into a single boolean makes debugging impossible.

Section sources

Reputation Thresholds and Maturity

The gate compares the agent’s matured total score against the configured threshold. It uses the same value the reputation contract uses for threshold checks, avoiding disagreement with on-chain policies built on meetsThreshold. The gate also checks the bond held now against the minimum stake in force now, as SigvaraReader.isTrusted does, so the two agree for an Active agent whose bond is below the current minimum (alpha.9 checks the stored status and the score only, and admits such an agent).

Key points:

  • The gate reads the matured total score, not the raw sum of factors.
  • Raw factors may temporarily exceed the matured total while maturity ramps up.
  • The maturity mechanism prevents farm-and-cash-out attacks by limiting how fast earned reputation becomes spendable.
  • On networks where external trust and propagation are not yet reachable, the practical ceiling is lower than 100, so thresholds should be chosen carefully.
flowchart TD
Earned["Earned score finalized"] --> Matured["Matured score climbs toward earned"]
Matured --> Spendable["Spendable for gates and contracts"]
Earned --> RawFactors["Raw factor breakdown"]
RawFactors --> NotUsedByGate["Gate does not use raw sum"]
Matured --> ThresholdCheck["Gate compares matured score to threshold"]
Loading

Diagram sources

Section sources

Practical Examples

Implementing a Custom Gate

To integrate reputation-based access control:

  1. Create a SigvaraGate with your RPC URL, contract addresses, chain ID, threshold, and a stable audience. The constructor throws if the audience is missing, the threshold is not a finite number of 0 or more, or ttlSeconds is not a positive number. A threshold taken from Number(process.env.X) with X unset is NaN: on alpha.9 that admits every active agent that can prove its key, whatever its score, and from 1.0.0-alpha.10 the constructor throws. To refuse a score nobody has refreshed, also set maxScoreAgeSeconds, a few times your oracle's epoch and measured against your host's clock (off by default, one more read per admit).
  2. When a user wants to perform a protected action, call challenge(did) and return the challenge object to the client.
  3. Have the agent sign challenge.payload. The agent should use signChallengeFor(payload, audience), which signs only a challenge addressed to the audience it passes (alpha.9 has only signChallenge, which signs whatever it is handed; the agent's signChallenge is removed in 1.0.0-alpha.12).
  4. Call admit(did, challenge, signature) and inspect the result.
  5. If ok is false, handle the specific reason.

For multi-instance deployments, provide a shared NonceStore with an atomic consume method.

Section sources

Integrating With Existing Access Control Systems

Use the gate as an authorization middleware:

  • Protect routes or API endpoints by calling the gate before executing business logic.
  • Map refusal reasons to HTTP status codes or application-specific errors.
  • Cache or log decisions for observability, but do not cache admission results across challenge lifetimes.
  • Treat not_active differently from below_threshold in user-facing messaging.

Section sources

Handling Authorization Failures

  • bad_proof: reject immediately; ask the client to retry with a fresh challenge if needed.
  • replayed: reject and warn about replay attempts; require a new challenge.
  • not_active: inform the operator that the agent is not active; do not direct them to earn points.
  • stale_score (only with maxScoreAgeSeconds): tell the operator the oracle has stopped writing this score, or never wrote it; do not tell the agent to earn points.
  • below_threshold: inform the agent that their reputation is too low; include the current score and threshold.

Section sources

Security Considerations

Nonce Expiration and Bounded State

  • Challenges expire based on the signed timestamp plus the gate’s TTL.
  • Nonces are dropped after expiry through opportunistic pruning.
  • The default in-memory store keeps state bounded by issue rate rather than uptime.

Distributed Replay Protection

  • The default store is process-local.
  • Behind a load balancer, implement a shared store with atomic consume.
  • Without atomic consume, two concurrent requests with the same nonce could both observe it unspent; the gate adds an in-process guard, but that does not protect across processes.

Attack Prevention

  • Invalid proofs never consume nonces, preventing denial-of-service via nonce exhaustion.
  • Audience binding prevents relay attacks where one service’s response is reused at another.
  • Using matured score prevents short-lived farms from passing gates before maturity catches up.
  • Threshold selection should account for the reachable ceiling when external trust and propagation are limited.
  • A caller who can form a well-sized signature for a registered agent makes the gate do one identity read, because the key the signature is checked against lives on chain. Nothing else is read until the proof and the nonce pass, so rate-limit callers where the gate is exposed.
  • A challenge dated more than 60 seconds ahead of the gate's clock is refused as not fresh, so a proof over a far-future timestamp cannot stay valid, and its nonce cannot sit in the store until a far-off expiry. Challenges the gate issues itself are dated by its own clock.

Section sources

Integration Patterns for Web Applications

Recommended pattern:

  1. Server-side gate instance with a stable audience.
  2. Client-side agent signs challenge payloads.
  3. Middleware enforces admission before route handlers.
  4. Clear error messages distinguish authentication failures from reputation failures.
  5. Shared nonce store for horizontally scaled deployments.
  6. Monitoring for replay attempts and repeated bad_proof failures.
flowchart TD
Request["HTTP Request"] --> Auth["Authentication Layer"]
Auth --> GateCall["SigvaraGate.admit()"]
GateCall --> Decision{"Admitted?"}
Decision --> |No| Error["Return error with reason"]
Decision --> |Yes| Handler["Business Logic Handler"]
Error --> Log["Log reason and metadata"]
Handler --> Response["Authorized Response"]
Loading

Dependency Analysis

The gate depends on:

  • SigvaraVerifier for signature verification, activity checks, and reputation queries.
  • NonceStore for replay prevention.
  • Configuration values for threshold, audience, TTL, and chain connectivity.
graph LR
Gate["SigvaraGate"] --> Verifier["SigvaraVerifier"]
Gate --> NonceStore["NonceStore"]
Verifier --> Chain["Reputation Contract"]
Gate --> Config["GateConfig"]
Loading

Diagram sources

Section sources

Performance Considerations

  • The gate reads none of the status, the bond and the score until the signature and the nonce have passed. Checking the signature costs one identity read, so a caller who can form a well-sized signature for a registered agent still makes the gate do that one read.
  • It reads the matured total score directly rather than fetching the full reputation breakdown, minimizing contract calls.
  • It reads the status, the bond and the score together (and the score's write time if maxScoreAgeSeconds is set), so an admit costs two round trips and not three, and each read gives up after 30 seconds by default.
  • Nonce pruning runs opportunistically and stops at the first live entry in the default store, keeping admission cost proportional to recent activity rather than total lifetime state.
  • Use a shared nonce store with atomic consume in production fleets to avoid race conditions and reduce round trips.

Section sources

Troubleshooting Guide

Common Issues and Resolutions

  • Repeated bad_proof: Check that the client signs challenge.payload and returns the correct signature format. Ensure the audience matches the gate configuration.
  • Unexpected replayed: Ensure the client requests a fresh challenge for each attempt. If using multiple server instances, configure a shared nonce store with atomic consume.
  • Agents stuck at not_active: Investigate bonding, suspension, or slashing status. Do not advise earning points as the fix.
  • Agents failing at below_threshold: Review the current score and threshold. Consider adjusting the threshold relative to the reachable reputation ceiling.
  • The constructor throws: The audience is missing, the threshold is not a finite number of 0 or more, or ttlSeconds or maxScoreAgeSeconds is not a positive number. On alpha.9 only the audience is checked.
  • admit waits a long time: The RPC has gone quiet. A read is abandoned after 30 seconds, and timeoutMs changes that. On alpha.9 ethers waits five minutes.

Debugging Tips

  • Inspect the reason field in AdmitResult; it tells you exactly which layer failed.
  • Confirm that the nonce comes from the signed payload, not a caller-modified wrapper.
  • Verify that the TTL and audience are set consistently across clients and servers.
  • Monitor nonce store growth and pruning behavior in production.

Section sources

Conclusion

The Sigvara SDK gate provides a focused, secure, and debuggable authorization layer. It combines challenge-response authentication, nonce-based replay protection, activity checks, and matured reputation threshold enforcement. By returning explicit refusal reasons and separating authentication from authorization, it enables integrators to build robust reputation-based access control for web applications and other systems.

When deploying in production, prioritize:

  • A stable audience per service.
  • A shared nonce store with atomic consume.
  • Careful threshold selection aligned with the reachable reputation ceiling.
  • Clear handling of each refusal reason in user-facing flows.

Appendices

Gate Configuration Reference

Field Description Notes
rpcUrl Ethereum RPC endpoint Used by the verifier to read on-chain data
addresses Contract addresses Identity, reputation, and staking addresses
chainId Optional chain identifier Passed to the verifier, which uses it only when it builds a DID Document; the gate's own checks do not depend on it, and the DID in a challenge already carries its chain
threshold Minimum matured score required Choose against the reachable ceiling. Must be a finite number, 0 or more
audience Stable service identifier Signed into every challenge to prevent relays
ttlSeconds Challenge validity duration Default 300 seconds if omitted. Must be a positive number
nonceStore Optional custom nonce store Required for multi-instance safety
timeoutMs How long one chain read may take, in milliseconds Default 30000 (alpha.9 uses ethers' five minutes)
maxScoreAgeSeconds Refuse a score written longer ago than this many seconds, or never, with stale_score Optional and off by default. A positive number; choose a few times your oracle's epoch. Measured against the gate host's clock. Costs one more read per admit

Section sources

Overview

Clone this wiki locally