Files
p2ns/docs/CONSENSUS.md
T

14 KiB

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). 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 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

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:

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.

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.

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.

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

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:

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:

CONSENSUS_QUORUM_THRESHOLD=0.67
CONSENSUS_MIN_VOTES=3
CONSENSUS_VOTE_VALIDATION=true

Small networks (2-3 peers):

CONSENSUS_QUORUM_THRESHOLD=0.5
CONSENSUS_MIN_VOTES=1

Single-node operation:

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:

curl https://p2ns.admin/api/consensus/metrics

Via SDK:

const metrics = sdk.dns.getConsensusMetrics();

API Endpoints

Get Sidecar Status

GET /api/consensus/status

Returns sidecar health: open, ready, writable, bootstrapComplete, eventCount, domainCount, indexedLength, length, and sidecar key (hex).

Get Consensus State

GET /api/consensus/{domain}

Returns current consensus state for a domain including vote counts, quorum status, and resolved claimant.

Get Metrics

GET /api/consensus/metrics

Returns aggregate consensus metrics across all domains.

Force Recalculation

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