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:
- claim_upsert — add or replace a claim for a claimant
- claim_remove — remove a claim and drop votes for that claimant
- vote_upsert — set a voter's choice (last event per voter wins)
- 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 initializeddnsPass(fromcore.status) + 1 (local node), falling back to connected swarm peers + 1 if writer set is unknownCONSENSUS_QUORUM_THRESHOLD= 0.5 (default)CONSENSUS_MIN_VOTES= 2 (default)
Example: With 5 active peers and default settings:
ceil(5 * 0.5) = 3max(2, 3) = 3votes 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
- On startup: After connecting to the network
- On peer connection: When new peers join
- On claim discovery: When new claims are replicated
- 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
- Check sidecar health:
GET /api/consensus/status(bootstrapComplete,domainCount) - Check if claim exists:
GET /api/entries - Check consensus state:
GET /api/consensus/{domain} - Verify quorum: Are enough peers connected?
- Force recalculation:
POST /api/consensus/recalculate/{domain}
Sidecar Not Ready
- Confirm dnsPass is initialized (master paired or joiner invited)
- Check logs for
ConsensusAutobaseinit errors orCONSENSUS_INIT_TIMEOUT_MStimeouts - Verify
cache/network.jsonhasconsensusAutobaseKeyon joiners - Restart node; bootstrap/hydration replays from local dnsPass copy
Unexpected Winner
- Check vote counts in consensus state
- Verify tie-breaker strategy matches expectations
- Check claim timestamps if using
timestampstrategy - Review vote validation failures in metrics
High Quorum Failures
- Check peer count:
GET /api/peers - Lower
CONSENSUS_MIN_VOTESfor small networks - Adjust
CONSENSUS_QUORUM_THRESHOLDif needed
Related Documentation
- RFC 0001: Autobase Consensus - Implemented specification
- GLOSSARY.md - Terms and concepts
- ARCHITECTURE.md - System architecture
- RESTAPI.md - API documentation
- README.md - Main documentation