# P2NS Consensus Mechanism This document provides a deep dive into the P2NS consensus mechanism for domain ownership resolution. ## Overview P2NS uses a quorum-based consensus mechanism to resolve domain ownership disputes. When multiple peers claim the same domain, the network votes to determine the legitimate owner. This enables decentralized domain management without a central authority. Resolution is computed by an **Autobase apply-based consensus sidecar** ([RFC 0001](rfcs/0001-autobase-consensus.md)). Claim and vote KV writes in Autopass dual-append typed events to the sidecar; the apply handler replays events in linearized order and `getConsensusState` reads from that view. **Key goals:** - Prevent domain squatting through voting - Handle network partitions gracefully - Resolve ties deterministically - Support single-node operation (local claims) ## Architecture Consensus uses a dual-store design: | Store | Role | |-------|------| | **Autopass KV** | Durable write store for `claim:*` and `vote:*` entries; replicated across peers | | **Consensus Autobase sidecar** | Apply-based read model; sole source for `getConsensusState` | **Write path:** `dns-pass-queue.js` writes to Autopass, then dual-appends typed events (`claim_upsert`, `claim_remove`, `vote_upsert`, `vote_remove`) to the sidecar. **Read path:** `consensus-view.js` reads the in-memory apply view, runs `consensus-resolver.js`, and returns consensus state. There is no KV-scan fallback. **Bootstrap:** On first startup (or empty sidecar), claim/vote entries are replayed from the local dnsPass copy into the sidecar. The network manifest stores `consensusAutobaseKey` so joiners open and replicate the same sidecar. Read-only joiners hydrate the view from dnsPass without appending bootstrap events. **Lifecycle:** Sidecar init runs in the background after Autopass pairing (`startConsensusForNetwork`) so swarm join is not blocked. Corestore namespace: `p2ns-consensus`. See [RFC 0001](rfcs/0001-autobase-consensus.md) for the full specification. ## Data Model Domain claims and votes are stored in the Autopass distributed ledger using a key-value format. ### Claims Claims assert ownership of a domain: ``` Key: claim:{domain}:{claimant} Value: {"hash": "hs://...", "clients": [...], "timestamp": 1704067200000, "ssl": false} ``` | Field | Type | Description | |-------|------|-------------| | `hash` | string | Holesail connection hash for the domain | | `clients` | array | Service definitions (serviceName, key, port, protocol) | | `timestamp` | number | Unix timestamp when claim was created | | `ssl` | boolean | Whether the Holesail connection uses SSL/TLS | **Legacy format:** Older claims may store just the hash string without JSON wrapper. ### Votes Votes support a specific claimant for a domain: ``` Key: vote:{domain}:{claimant}:{voter} Value: "1" ``` | Component | Description | |-----------|-------------| | `domain` | The domain being voted on | | `claimant` | Public key of the claim being supported | | `voter` | Public key of the peer casting the vote | ## Consensus Algorithm ### Resolution Flow ```mermaid flowchart TD Start[getConsensusState] --> WaitReady{Sidecar ready or hydrated?} WaitReady -->|No| Wait[waitForConsensusReady / ensureViewHydrated] Wait --> ViewReady[Read apply view for domain] WaitReady -->|Yes| ViewReady ViewReady --> CheckClaims{Any claims in view?} CheckClaims -->|No| NoClaims[Status: no_claims] CheckClaims -->|Yes| Resolver[consensus-resolver.js] Resolver --> CalcQuorum[Calculate quorum requirement] CalcQuorum --> CheckQuorum{Quorum met?} CheckQuorum -->|No| CheckSingleLocal{Single local claim?} CheckSingleLocal -->|Yes| ResolvedLocal[Status: resolved] CheckSingleLocal -->|No| InsufficientQuorum[Status: insufficient_quorum] CheckQuorum -->|Yes| FindWinner[Find claimant with most votes] FindWinner --> CheckTie{Multiple winners?} CheckTie -->|No| Resolved[Status: resolved] CheckTie -->|Yes| TieBreaker[Apply tie-breaker strategy] TieBreaker --> TieResolved[Status: tie - resolved via tie-breaker] ``` ### Vote Collection Votes are not scanned from the Autopass ledger at read time. The apply handler (`consensus-apply.js`) maintains per-domain state as sidecar events arrive: 1. **claim_upsert** — add or replace a claim for a claimant 2. **claim_remove** — remove a claim and drop votes for that claimant 3. **vote_upsert** — set a voter's choice (last event per voter wins) 4. **vote_remove** — remove a voter's vote At read time, `consensus-resolver.js` validates votes against claims in the view, counts votes per claimant, and applies quorum and tie-break rules. ### Quorum Calculation The minimum votes required for consensus: ```javascript minVotes = max(CONSENSUS_MIN_VOTES, ceil(activePeers * CONSENSUS_QUORUM_THRESHOLD)) ``` Where: - `activePeers` = peers with initialized `dnsPass` (from `core.status`) + 1 (local node), falling back to connected swarm peers + 1 if writer set is unknown - `CONSENSUS_QUORUM_THRESHOLD` = 0.5 (default) - `CONSENSUS_MIN_VOTES` = 2 (default) **Example:** With 5 active peers and default settings: - `ceil(5 * 0.5) = 3` - `max(2, 3) = 3` votes required ### Resolution States | Status | Description | |--------|-------------| | `resolved` | Single winner determined by vote count or tie-breaker | | `conflict` | Domain resolved to another claimant, but local peer has a competing claim (computed in admin/UI layers, not returned by `getConsensusState`) | | `insufficient_quorum` | Not enough votes to reach consensus | | `tie` | Multiple claimants tied; resolved via tie-breaker | | `no_claims` | No claims exist for this domain | | `error` | System error (e.g., dnsPass not initialized) | ### Special Case: Single Local Claim When there are no votes but only the local peer has claimed a domain, it's automatically resolved to the local claimant. This enables single-node operation without requiring external votes. ## Tie-Breaking Strategies When multiple claimants have equal votes, a tie-breaker determines the winner. ### timestamp (Default) First-come-first-served: the oldest claim wins. ```javascript candidates.sort((a, b) => claimTimestamps[a] - claimTimestamps[b])[0] ``` **Rationale:** Rewards early adopters and prevents hostile takeovers of established domains. ### claimant_age Prefers the local writer if they're a candidate, otherwise falls back to lexicographic ordering. ```javascript if (candidates.includes(localWriter)) return localWriter; return candidates.sort((a, b) => a.localeCompare(b))[0]; ``` **Rationale:** Gives local claims priority while maintaining determinism. ### lexicographic Prefers local writer, then sorts candidates alphabetically by public key. ```javascript if (candidates.includes(localWriter)) return localWriter; return candidates.sort((a, b) => a.localeCompare(b))[0]; ``` **Rationale:** Ensures all nodes reach the same conclusion deterministically. ## Auto-Voting P2NS automatically casts votes under certain conditions. ### When Auto-Votes Occur 1. **On startup:** After connecting to the network 2. **On peer connection:** When new peers join 3. **On claim discovery:** When new claims are replicated 4. **On manual trigger:** Via `/api/consensus/recalculate` ### Auto-Vote Logic ```javascript async function autoVoteForDomain(domain, entries) { const claims = getClaimsForDomain(domain, entries); if (claims.has(localWriter)) { await castVote(domain, localWriter); return; } if (Object.keys(claims).length === 1) { await castVote(domain, Object.keys(claims)[0]); return; } // Multiple claimants: use configured tie-breaker among claimants const winner = applyTieBreaker(Object.keys(claims), claimTimestamps, localWriter); await castVote(domain, winner); } ``` ### Vote Validation Votes must reference an existing claim to be counted: ```javascript function validateVote(claimant, claims) { if (!CONSENSUS_VOTE_VALIDATION) return true; if (!claims.hasOwnProperty(claimant)) { consensusMetrics.validationFailures++; return false; } return true; } ``` Invalid votes are logged and excluded from vote counts. ## Configuration Configure consensus behavior via environment variables: | Variable | Default | Description | |----------|---------|-------------| | `CONSENSUS_QUORUM_THRESHOLD` | `0.5` | Percentage of peers required (0.0-1.0) | | `CONSENSUS_MIN_VOTES` | `2` | Minimum votes regardless of peer count | | `CONSENSUS_TIE_BREAKER` | `timestamp` | Strategy: `timestamp`, `claimant_age`, `lexicographic` | | `CONSENSUS_VOTE_VALIDATION` | `true` | Validate votes reference existing claims | | `CONSENSUS_IMMEDIATE_UPDATE` | `true` | Update resolution immediately on new votes | | `CONSENSUS_INIT_TIMEOUT_MS` | `30000` | Timeout for sidecar ready/update during init | ### Tuning Recommendations **High-security deployments:** ```env CONSENSUS_QUORUM_THRESHOLD=0.67 CONSENSUS_MIN_VOTES=3 CONSENSUS_VOTE_VALIDATION=true ``` **Small networks (2-3 peers):** ```env CONSENSUS_QUORUM_THRESHOLD=0.5 CONSENSUS_MIN_VOTES=1 ``` **Single-node operation:** ```env CONSENSUS_MIN_VOTES=0 ``` ## Caching ### Entries Cache (Autopass reads) `getAllEntries()` caches Autopass ledger entries for auto-voting and admin views: - **TTL:** 5 seconds - **Scope:** All entries in Autopass - **Invalidation:** On write operations via `invalidateEntriesCache()` ### Consensus State Cache (read results) `getConsensusState()` caches resolver output per domain: - **TTL:** 10 seconds - **Scope:** Individual domain consensus state - **Invalidation:** On sidecar apply updates, write operations, or manual recalculation via `invalidateConsensusCache()` The apply view itself is the live read model; it is updated incrementally as sidecar events are applied, not rebuilt from a full KV scan on each query. ## Metrics Consensus operations are tracked for monitoring: | Metric | Description | |--------|-------------| | `resolutions` | Total successful domain resolutions | | `quorumFailures` | Times quorum was not met | | `ties` | Ties requiring tie-breaker | | `validationFailures` | Invalid votes rejected | | `totalVotes` | Total votes cast across all domains | | `avgVotesPerDomain` | Average votes per domain | | `domainResolutions` | Per-domain resolution/failure counts | | `sidecar` | Sidecar health object (see status endpoint) | | `bootstrapComplete` | Whether bootstrap/hydration has finished | ### Accessing Metrics **Via API:** ```bash curl https://p2ns.admin/api/consensus/metrics ``` **Via SDK:** ```javascript const metrics = sdk.dns.getConsensusMetrics(); ``` ## API Endpoints ### Get Sidecar Status ```bash GET /api/consensus/status ``` Returns sidecar health: `open`, `ready`, `writable`, `bootstrapComplete`, `eventCount`, `domainCount`, `indexedLength`, `length`, and sidecar `key` (hex). ### Get Consensus State ```bash GET /api/consensus/{domain} ``` Returns current consensus state for a domain including vote counts, quorum status, and resolved claimant. ### Get Metrics ```bash GET /api/consensus/metrics ``` Returns aggregate consensus metrics across all domains. ### Force Recalculation ```bash POST /api/consensus/recalculate POST /api/consensus/recalculate/{domain} ``` Invalidates cache and triggers fresh consensus calculation. Useful after network changes or debugging. ## Example Scenarios ### Scenario 1: Single Owner ``` Peers: A (local) Claims: A claims example.tld Votes: (none) Result: resolved -> A Reason: Single local claim, no quorum needed ``` ### Scenario 2: Clear Winner ``` Peers: A, B, C (3 total) Claims: A claims example.tld, B claims example.tld Votes: A votes for A, B votes for A, C votes for A Quorum: max(2, ceil(3 * 0.5)) = 2 Total votes for A: 3 Result: resolved -> A Reason: A has majority votes, quorum met ``` ### Scenario 3: Tie with Timestamp Resolution ``` Peers: A, B, C, D (4 total) Claims: - A claims example.tld at T=1000 - B claims example.tld at T=2000 Votes: A->A, B->B, C->A, D->B Vote counts: A=2, B=2 (tie) Tie-breaker: timestamp Winner: A (older claim) Result: tie -> A ``` ### Scenario 4: Insufficient Quorum ``` Peers: A, B, C, D, E (5 total) Claims: A claims example.tld Votes: A votes for A Quorum: max(2, ceil(5 * 0.5)) = 3 Total votes: 1 Result: insufficient_quorum Reason: Only 1 vote, need 3 ``` ## Troubleshooting ### Domain Not Resolving 1. Check sidecar health: `GET /api/consensus/status` (`bootstrapComplete`, `domainCount`) 2. Check if claim exists: `GET /api/entries` 3. Check consensus state: `GET /api/consensus/{domain}` 4. Verify quorum: Are enough peers connected? 5. Force recalculation: `POST /api/consensus/recalculate/{domain}` ### Sidecar Not Ready 1. Confirm dnsPass is initialized (master paired or joiner invited) 2. Check logs for `ConsensusAutobase` init errors or `CONSENSUS_INIT_TIMEOUT_MS` timeouts 3. Verify `cache/network.json` has `consensusAutobaseKey` on joiners 4. Restart node; bootstrap/hydration replays from local dnsPass copy ### Unexpected Winner 1. Check vote counts in consensus state 2. Verify tie-breaker strategy matches expectations 3. Check claim timestamps if using `timestamp` strategy 4. Review vote validation failures in metrics ### High Quorum Failures 1. Check peer count: `GET /api/peers` 2. Lower `CONSENSUS_MIN_VOTES` for small networks 3. Adjust `CONSENSUS_QUORUM_THRESHOLD` if needed ## Related Documentation - [RFC 0001: Autobase Consensus](rfcs/0001-autobase-consensus.md) - Implemented specification - [GLOSSARY.md](GLOSSARY.md) - Terms and concepts - [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture - [RESTAPI.md](RESTAPI.md) - API documentation - [README.md](../README.md) - Main documentation