444 lines
14 KiB
Markdown
444 lines
14 KiB
Markdown
# 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
|
|
|