| English | 中文版 |
RFC Number: NPS-RFC-0004
Title: Append-only NID reputation log (Certificate Transparency for Agents)
Status: Accepted (Phase 1 — entry wire format + .NET reference types landed)
Author(s): Ori Lynn iamzerolin@gmail.com (LabAcacia)
Shepherd: Ori Lynn (pre-1.0 fast-track per spec/cr/README.md)
Created: 2026-04-21
Last-Updated: 2026-05-01
Accepted: 2026-04-26 (pre-1.0 fast-track; see spec/cr/README.md)
Activated: (set when first reference log operator ships, target v1.0-alpha.4)
Supersedes: none
Superseded-By: none
Affected Specs: NPS-3 NIP, NPS-4 NDP, spec/services/NPS-AaaS-Profile.md, spec/error-codes.md
Affected SDKs: .NET, Python, TypeScript, Java, Rust, Go
—
Define a Certificate-Transparency-style append-only log for NID
behavioral incidents. Entries are signed observations (abuse reports,
rate-limit violations, contract disputes, revocations) that AaaS
gateways, auditors, and CAs publish against specific NIDs. A new
optional NDP sub-path (/.nid/reputation?nid=...) lets any party
query the aggregated record. Combined with NPS-RFC-0003 assurance
levels, this gives Nodes both provenance (who is this Agent?) and
track record (has it misbehaved?).
Follow-up to the same 2026-04-20 review comment that drove RFC-0003. Assurance levels answer “who is this Agent?” but not “has this specific NID already caused trouble?” Concretely:
L2 NID that has been revoked for abuse MUST be findable by any
Node considering whether to transact with it, without requiring
pre-existing relationship with the revoking CA.Existing domain-name reputation systems (Spamhaus, DNSBL) use private lists. The Certificate Transparency model — publicly auditable, append-only, tamper-evident, reachable by anyone — maps directly to our problem because:
An entry is a signed JSON object:
{
"v": 1,
"log_id": "nid:ed25519:<log-operator-pubkey>",
"seq": 42817,
"timestamp": "2026-04-21T14:30:00Z",
"subject_nid": "nid:ed25519:<agent-pubkey>",
"incident": "rate-limit-violation",
"severity": "moderate",
"window": { "start": "2026-04-21T13:00:00Z", "end": "2026-04-21T14:00:00Z" },
"observation": {
"requests": 45000,
"threshold": 300,
"endpoint_hash": "sha256:<hash-of-node-origin>"
},
"evidence_ref": "https://log.example.com/evidence/42817",
"evidence_sha256": "hex-of-blob",
"issuer_nid": "nid:ed25519:<issuer-pubkey>",
"signature": "base64url(Ed25519(canonical-entry-without-sig))"
}
Field semantics:
| Field | Required | Description |
|---|---|---|
v |
yes | Schema version; this RFC defines 1 |
log_id |
yes | NID of the log operator appending this entry |
seq |
yes | Monotonically-increasing per-log sequence number |
timestamp |
yes | RFC 3339 UTC; log operator’s clock |
subject_nid |
yes | NID this entry is about |
incident |
yes | Enum; see §4.2 |
severity |
yes | info / minor / moderate / major / critical |
window |
no | Time window the observation covers |
observation |
no | Free-form machine-readable detail, incident-type-specific |
evidence_ref |
no | URL to richer evidence blob (logs, transcript, etc.) |
evidence_sha256 |
no | SHA-256 of the evidence blob for tamper detection |
issuer_nid |
yes | NID of the party asserting the incident — MAY equal log_id |
signature |
yes | Ed25519 signature by issuer_nid’s private key over canonical form |
Canonicalization for signing: JCS (RFC 8785) applied to the entry
object with signature omitted. The log operator verifies the issuer
signature before appending and re-signs the full entry with their
own key to commit sequence number and timestamp (dual signature:
issuer attests the incident, log operator attests the ordering).
Initial enum (extensible in follow-up RFCs):
| Value | Meaning |
|---|---|
cert-revoked |
CA revoked the NID’s cert; subject_nid matches revocation |
rate-limit-violation |
Sustained violation of published rate limits |
tos-violation |
Violated AaaS gateway’s published terms |
scraping-pattern |
Observed behavior matched scraping heuristics |
payment-default |
CGN / fiat payment default on committed transaction |
contract-dispute |
Contractual breach on an async NOP task, unresolved |
impersonation-claim |
A third party claims subject_nid is impersonating them |
positive-attestation |
Explicit positive signal (e.g., audit passed) |
Unknown values MUST be preserved by log operators and returned to queriers — forward compatibility.
A log operator exposes two HTTP endpoints, discoverable via NDP:
POST /v1/log/entries # submit a new entry (requires issuer auth)
GET /v1/log/entries?nid=<subject_nid>&since=<seq> # query
[Phase 2 — deferred] The following endpoints and Merkle structure are not part of the Phase 1 implementation; they are targeted for v1.0-alpha.5 per §8.1.
GET /v1/log/sth # signed tree head (Merkle root + seq + timestamp)
GET /v1/log/proof?seq=<n>&tree_size=<m> # inclusion proof
The Merkle structure mirrors RFC 9162 (CT 2.0): leaves are canonical
entries; internal nodes are SHA-256 hashes; the signed tree head (STH)
commits to the current root and is signed by log_id. This gives
queriers cryptographic proof that an entry is included without
downloading the full log.
[Phase 2 — deferred] Everything in this section is targeted for v1.0-alpha.5 per §8.1. Neither
/.nid/reputationnorreputation_policyis part of the Phase 1 implementation.
NDP gains an optional well-known path for reputation discovery:
GET /.nid/reputation?nid=<nid>
→ array of log operator URLs that claim to have entries about this NID
This is a discovery hint, not a source of truth — Nodes still fetch entries from the log operators they trust.
NWM optionally declares:
# /.nwm excerpt
reputation_policy:
required_logs: ["log:labacacia-primary", "log:some-industry-consortium"]
reject_on:
- { incident: "cert-revoked", severity: ">=minor" }
- { incident: "scraping-pattern", severity: ">=major", within_days: 30 }
Nodes choosing not to check reputation simply omit the field.
[Phase 3 — v1.0-alpha.5] Enables cross-log consistency verification and fork detection. OQ-1 is resolved in favour of a lightweight NPS-native variant (analogous to RFC 9162 §8.1.4 but without the HTTP-header transport and with NPS error codes).
Each log operator MUST expose:
GET /v1/log/gossip/sth
Response (JSON):
{
"own_sth": {
"tree_size": 42817,
"timestamp": "2026-05-01T10:00:00Z",
"sha256_root_hash": "hex-of-merkle-root",
"log_id": "nid:ed25519:<log-operator-pubkey>",
"signature": "base64url(Ed25519(jcs(own_sth without signature)))"
},
"peer_sths": [
{
"log_id": "nid:ed25519:<peer-pubkey>",
"received_at": "2026-05-01T09:59:30Z",
"sth": { /* same shape as own_sth */ }
}
]
}
peer_sths contains the most recent validated STH received from each
configured peer. Clients querying this endpoint can cross-check peers
without contacting them directly.
Log operators configured with a peers list MUST run a background
gossip cycle:
GET /v1/log/gossip/sth from each peer (default interval: 30 s).own_sth.signature against the peer’s log_id public key.tree_size MUST be ≥ the last accepted tree_size
for that log_id. A decrease is evidence of a fork attempt — log operators
SHOULD emit a LOG-FORK-DETECTED event to local audit and cease transacting
with that peer until manually reviewed.tree_size increases, the
operator SHOULD fetch an RFC 9162 consistency proof from
GET /v1/log/proof?from=<prev_size>&to=<new_size> and verify it before
accepting the new STH. Failure to verify = potential fork./gossip/sth.Log operator configuration gains an optional peers list:
{
"peers": [
{ "log_id": "nid:ed25519:<peer>", "endpoint": "https://log2.example.com" }
],
"gossip_interval_s": 30
}
gossip_interval_s defaults to 30; minimum 10; maximum 3600.
| Error Code | NPS Status | Description |
|---|---|---|
NIP-REPUTATION-GOSSIP-FORK |
NPS-SERVER-INTERNAL |
Cross-peer STH consistency check failed; possible fork detected |
NIP-REPUTATION-GOSSIP-SIG-INVALID |
NPS-CLIENT-BAD-FRAME |
Peer STH signature verification failed |
Reputation-gated admission:
Agent Node Log Operator
│ │ │
│── HelloFrame ──────────→ │ │
│── IdentFrame (NID) ────→ │ │
│ │ ── GET entries?nid=... ─────→ │
│ │ ←── 200 + entries ────────── │
│ │ evaluate reject_on rules │
│ │ │
│ ←── (accept | 403) ───── │ │
For hot paths, Nodes SHOULD cache log results with a short TTL
(default 60 s) and refresh asynchronously. A hard reject_on: cert-revoked
SHOULD be checked synchronously on every connection but can be
satisfied by OCSP-stapling-like pre-fetch.
New entries in spec/error-codes.md:
| Error Code | NPS Status | Description |
|---|---|---|
NWP-AUTH-REPUTATION-BLOCKED |
NPS-AUTH-FORBIDDEN |
Reputation policy matched a reject rule |
NIP-REPUTATION-LOG-UNREACHABLE |
NPS-DOWNSTREAM-UNAVAILABLE |
Required log operator unreachable during policy evaluation |
NIP-REPUTATION-ENTRY-INVALID |
NPS-CLIENT-BAD-FRAME |
Entry signature invalid or canonical form malformed |
reputation_policy)? Fully compatible — log is ignored.spec/rfcs/README.md).Each Node maintains its own blocklist/allowlist of NIDs.
Rely solely on NPS-RFC-0002’s CRL / OCSP for bad-actor signaling.
A single LabAcacia-operated registry.
issuer_nid so forged entries are detectable; the
dual-signature model means the operator can’t silently attribute
entries to third parties.evidence_ref for dispute.evidence_ref strongly recommended.issuer_nid private key
(signature over canonical form). Log operator additionally signs
for ordering commit.seq + timestamp + log_id uniquely identify an entry;
inclusion proof binds to a specific tree head.issuer_nid and require authenticated accounts for writes.| Phase | Scope | Exit criterion |
|---|---|---|
| 1 | .NET reference log operator; entry format; submit + query HTTP API; no Merkle proofs yet | Unit tests green; another SDK can query |
| 2 | Merkle tree + STH + inclusion proofs; NDP /.nid/reputation in all SDKs; NWM reputation_policy parsing |
Interop: 2 independent logs + 2 SDK clients cross-check |
| 3 | Default reputation_policy for AaaS Profile L2 tier; STH gossip between reference logs (see §4.5) |
Logs agree on STH within 60s of commit |
| 4 | Deprecate unsigned-entry experimental flag if any | Clean |
| SDK | Owner | Status | Notes |
|---|---|---|---|
| .NET | Ori Lynn | ✅ Phase 1+2 done; Phase 3 (gossip) in alpha.5 | Reference log operator also in .NET (nps-ledger) |
| Python | TBD | Phase 1+2 pending | Client only |
| TypeScript | TBD | Phase 1+2 pending | — |
| Java | TBD | Phase 1+2 pending | — |
| Rust | TBD | Phase 1+2 pending | — |
| Go | TBD | Phase 1+2 pending | — |
seq=N, verify against STH at
tree_size >= N+1.reject_on matching: severity: ">=major" matches major
and critical, not moderate.NIP-REPUTATION-LOG-UNREACHABLE,
Node falls back per policy (fail-open or fail-closed configurable).None yet. Before Accepted:
scraping-pattern entry;
second gateway queries and applies reject_on.| Metric | Baseline | Proposed | Delta | Method |
|---|---|---|---|---|
| Log query latency (hot cache) | N/A | ≤ 20 ms | — | Wall-clock |
| Entry submit throughput | N/A | ≥ 1000/s | — | Load test |
| Merkle tree size (10M entries) | N/A | ≤ 1 GiB | — | Resident set |
subject_nid post a
dispute entry against an allegation about themselves? Default:
yes, as incident: self-dispute referencing the original seq.evidence_ref.scraping-pattern entries are reproducible.issuer_nid
stakes CGN against an entry; slashed if entry proven false.| Date | Author | Change |
|---|---|---|
| 2026-04-21 | Ori Lynn | Initial draft |
| 2026-04-26 | Ori Lynn | Accepted via pre-1.0 fast-track. Phase 1 spec changes landed: NPS-3 §5.1.2 Reputation Log Entry (12-field signed JSON, 8-value incident enum, 5-step severity enum, JCS dual-signature rule), error codes NIP-REPUTATION-ENTRY-INVALID / NIP-REPUTATION-LOG-UNREACHABLE / NWP-AUTH-REPUTATION-BLOCKED, new NPS-DOWNSTREAM-UNAVAILABLE status code. Phase 1 .NET reference types landed under NPS.NIP.Reputation.*. Phase 2 (Merkle tree + STH + inclusion proofs + NDP /.nid/reputation discovery + NWM reputation_policy parsing) deferred to v1.0-alpha.4 per RFC §8.1. Phase 3 (default policy in AaaS Profile L2 + STH gossip) deferred to alpha.11+. |
| 2026-05-01 | Ori Lynn | Phase 3 spec landed (v1.0-alpha.5): §4.5 STH Gossip Protocol (30s push cycle, /v1/log/gossip/sth endpoint, monotonicity + consistency-proof verification, fork detection); OQ-1 resolved; two new error codes NIP-REPUTATION-GOSSIP-FORK / NIP-REPUTATION-GOSSIP-SIG-INVALID; AaaS-Profile L2 default reputation_policy added in NPS-AaaS-Profile.md. Phase 3 .NET reference implementation in nps-ledger. |