Updates
This commit is contained in:
@@ -1,43 +1,35 @@
|
||||
# hyper-p2p-causal-consensus
|
||||
|
||||
HyperP2PCausalConsensus Novel BFT causal consensus primitive for Bare/Pear P2P. Key Innovations (never-before-seen in Bare ecosystem): - Hybrid causal + total ordering: Uses vector clocks for causality + cryptographic
|
||||
**BFT-style causal consensus** with vector clocks, signed proposals/votes, quorum finalization, optional Hyperbee persistence.
|
||||
|
||||
**Category:** Consensus & coordination
|
||||
|
||||
**Composes with:** `hyper-p2p-distributed-lock`, `hyper-p2p-quorum-pool`
|
||||
|
||||
**Protocol:** `hyper-p2p-causal-consensus/v1`
|
||||
**Category:** Consensus & coordination · **Protocol:** `hyper-p2p-causal-consensus/v1`
|
||||
|
||||
## When to use
|
||||
|
||||
Multi-peer apps that need consensus & coordination over a shared Hyperswarm topic.
|
||||
Ordered multi-writer logs, decentralized event sourcing, or microservice command batches needing quorum + signatures.
|
||||
|
||||
## When not to use
|
||||
|
||||
Single-process tools with no P2P topic (use local APIs only or skip `ready()`).
|
||||
Simple leader lease, lightweight append-only gossip (`hyper-p2p-raft-lite`), or single-writer LWW state.
|
||||
|
||||
## Quick start
|
||||
|
||||
```js
|
||||
const { HyperP2PCausalConsensus } = require('hyper-p2p-causal-consensus')
|
||||
const topic = process.argv[2] // 64-char hex or string
|
||||
const mod = new HyperP2PCausalConsensus({ topic, enableBackgroundTimers: false })
|
||||
await mod.ready() // joins swarm when topic set
|
||||
// ... application logic ...
|
||||
await mod.close()
|
||||
const HyperP2PCausalConsensus = require('hyper-p2p-causal-consensus')
|
||||
const c = new HyperP2PCausalConsensus({
|
||||
localId: 'node-1',
|
||||
quorumThreshold: 0.67,
|
||||
enableGossip: true,
|
||||
topic: process.argv[2]
|
||||
})
|
||||
c.addPeer('node-2', '<hex-public-key>')
|
||||
const id = await c.propose({ event: 'hello' })
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
|
||||
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
|
||||
- [../_shared/PRODUCTION.md](../../_shared/PRODUCTION.md) — production checklist
|
||||
- [../_shared/DOC_STANDARDS.md](../../_shared/DOC_STANDARDS.md) — documentation standards
|
||||
|
||||
- Integration: [`../../real_tests/integration/`](../../../real_tests/integration/) — `causal-consensus-two-node.js`
|
||||
[docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
npm install && npm test
|
||||
```
|
||||
`npm install && npm test` · `bare examples/basic.js`
|
||||
|
||||
@@ -1,117 +1,137 @@
|
||||
# API: hyper-p2p-causal-consensus
|
||||
|
||||
**Protocol:** `hyper-p2p-causal-consensus/v1`
|
||||
**Protocol:** `hyper-p2p-causal-consensus/v1` (`CONSENSUS_PROTOCOL`)
|
||||
|
||||
**Export:** `HyperP2PCausalConsensus`
|
||||
**Export:** `HyperP2PCausalConsensus` (default class export from `index.js`)
|
||||
|
||||
## Overview
|
||||
|
||||
HyperP2PCausalConsensus Novel BFT causal consensus primitive for Bare/Pear P2P. Key Innovations (never-before-seen in Bare ecosystem): - Hybrid causal + total ordering: Uses vector clocks for causality + cryptographic
|
||||
`HyperP2PCausalConsensus` proposes opaque `data` payloads with an embedded vector clock, collects signed votes, and finalizes when accept votes reach `ceil(peers.size * quorumThreshold)`. Decided events are stored in `decidedOrders` and optionally persisted to an injected `hyperbee`. With `enableGossip: true` and `topic`, proposals and votes can flow over `initModuleSwarm`; otherwise use local `propose` / `vote` / `receive*` for simulation.
|
||||
|
||||
Extends `bare-events` `EventEmitter`. Call `close()` to stop timers and mark the instance closed.
|
||||
|
||||
## Constructor
|
||||
|
||||
```js
|
||||
const mod = new HyperP2PCausalConsensus(opts)
|
||||
const HyperP2PCausalConsensus = require('hyper-p2p-causal-consensus')
|
||||
const c = new HyperP2PCausalConsensus(options)
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `topic` | `string` | `Buffer` | `null` | Hyperswarm topic; required for P2P `ready()` |
|
||||
| `keyPair` | KeyPair | random | Ed25519 key pair |
|
||||
| `localId` | `string` \| `Buffer` | random 8 bytes (encoded) | Logical node id |
|
||||
| `keyPair` | `KeyPair` | `hypercore-crypto.keyPair()` | Signing + swarm identity |
|
||||
| `quorumThreshold` | `number` | `0.67` | Fraction of `peers.size` for quorum |
|
||||
| `maxPeers` | `number` | `32` | Documented cap (not enforced in hot path) |
|
||||
| `proposalTimeoutMs` | `number` | `10000` | Pending → `expired` timer |
|
||||
| `voteTimeoutMs` | `number` | `5000` | Reserved for future use |
|
||||
| `enableSigning` | `boolean` | `true` | Ed25519 sign/verify proposals and votes |
|
||||
| `persistDecided` | `boolean` | `true` | Write decided events to `hyperbee` when set |
|
||||
| `idEncoding` | `string` | `'hex'` | Buffer id encoding |
|
||||
| `topic` | `string` \| `Buffer` \| `null` | `null` | Hyperswarm topic when gossip enabled |
|
||||
| `enableGossip` | `boolean` | `false` | Starts gossip timer + swarm when `true` |
|
||||
| `hyperbee` | `Hyperbee` \| `null` | `null` | Optional persistence |
|
||||
| `swarm` / `protomux` | — | `null` | Injection hooks for tests |
|
||||
|
||||
## Methods
|
||||
|
||||
### `propose(data, causalDeps = {})`
|
||||
### `async propose(data, causalDeps = {})`
|
||||
|
||||
- **Returns:** `Promise`
|
||||
- **Throws:**
|
||||
- `Error: Consensus instance closed`
|
||||
Creates a signed proposal, self-votes, starts expiry timer.
|
||||
|
||||
### `vote(proposalId, accept = true)`
|
||||
- **Returns:** `Promise<string | null>` — `proposalId` or `null` if local fork detected
|
||||
- **Throws:** `Error: Consensus instance closed`
|
||||
- **Emits:** `proposal`, optionally `error` on fork
|
||||
|
||||
- **Returns:** `Promise`
|
||||
- **Throws:** — (none documented in method body)
|
||||
### `async vote(proposalId, accept = true)`
|
||||
|
||||
### `receiveProposal(proposal, fromPeerId)`
|
||||
Cast local vote; may finalize quorum.
|
||||
|
||||
- **Returns:** `Promise`
|
||||
- **Throws:** — (none documented in method body)
|
||||
- **Returns:** `Promise<boolean>`
|
||||
- **Emits:** `vote`, `consensus` / `order-decided` on quorum
|
||||
|
||||
### `receiveVote(proposalId, vote)`
|
||||
### `async receiveProposal(proposal, fromPeerId)`
|
||||
|
||||
- **Returns:** `Promise`
|
||||
- **Throws:** — (none documented in method body)
|
||||
Inbound proposal path (wire or tests). Dedups by id, verifies signature, auto-votes if causal check passes.
|
||||
|
||||
### `getDecidedOrder(order)`
|
||||
- **Returns:** `Promise<boolean>`
|
||||
- **Emits:** `proposal-received`, `invalid-signature` on bad sig
|
||||
|
||||
- **Returns:** `value`
|
||||
- **Throws:** — (none documented in method body)
|
||||
### `async receiveVote(proposalId, vote)`
|
||||
|
||||
### `getAllDecided(—)`
|
||||
Merge vote; finalize on quorum.
|
||||
|
||||
- **Returns:** `value`
|
||||
- **Throws:** — (none documented in method body)
|
||||
|
||||
### `getMetrics(—)`
|
||||
|
||||
- **Returns:** `value`
|
||||
- **Throws:** — (none documented in method body)
|
||||
- **Returns:** `Promise<boolean>`
|
||||
|
||||
### `addPeer(peerId, publicKey)`
|
||||
|
||||
- **Returns:** `value`
|
||||
- **Throws:** — (none documented in method body)
|
||||
Registers peer for quorum size and signature verification.
|
||||
|
||||
### `getStats(—)`
|
||||
- **Returns:** `boolean`
|
||||
|
||||
- **Returns:** `object`
|
||||
- **Throws:** — (none documented in method body)
|
||||
### `getDecidedOrder(order)` / `getAllDecided()`
|
||||
|
||||
### `close(—)`
|
||||
Query decided log by sequence or sorted array.
|
||||
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:** — (none documented in method body)
|
||||
### `getMetrics()`
|
||||
|
||||
### `integrateVectorClock(vcModule)`
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `proposals`, `votesReceived`, `quorumsAchieved`, `forksDetected`, `decided`, `signed`, `verified` | Counters |
|
||||
| `peers`, `pendingProposals` | Derived sizes |
|
||||
|
||||
- **Returns:** `Promise`
|
||||
- **Throws:** — (none documented in method body)
|
||||
### `getStats()`
|
||||
|
||||
Returns `{ ...this._stats }` (compatibility stub; prefer `getMetrics()`).
|
||||
|
||||
### `async integrateVectorClock(vcModule)`
|
||||
|
||||
Stores optional `hyper-p2p-crdt-version-vector` module reference.
|
||||
|
||||
### `async close()`
|
||||
|
||||
Sets `_isClosed`, clears gossip and proposal timers, emits `closed`.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `closed` | no payload |
|
||||
| `consensus` | decidedEvent |
|
||||
| `error` | err |
|
||||
| `fork-detected` | proposals |
|
||||
| `gossip` | type |
|
||||
| `hyperswarm-gossip` | topic |
|
||||
| `invalid-signature` | proposalId, from |
|
||||
| `order-decided` | event |
|
||||
| `proposal` | proposal |
|
||||
| `proposal-expired` | payload object |
|
||||
| `proposal-received` | proposalId, from |
|
||||
| `protomux-send` | payload |
|
||||
| `vote` | payload object |
|
||||
| Event | Payload | When |
|
||||
|-------|---------|------|
|
||||
| `proposal` | proposal object | Local propose |
|
||||
| `vote` | `{ proposalId, voter, accept }` | Vote cast |
|
||||
| `consensus` | decided event | Quorum reached |
|
||||
| `order-decided` | `{ order, event }` | Same as finalize |
|
||||
| `proposal-expired` | `{ proposalId }` | Timeout |
|
||||
| `fork-detected` | `{ peerId, proposals }` | Conflicting issuer data |
|
||||
| `invalid-signature` | `{ proposalId, from }` | Verify failed |
|
||||
| `gossip` | `{ type, proposal }` | Gossip timer (pending) |
|
||||
| `error` | `Error` | Persistence or fork errors |
|
||||
| `closed` | — | `close()` |
|
||||
|
||||
## getStats()
|
||||
## getStats() / getMetrics()
|
||||
|
||||
Returns `{ ...this._stats }` — typically `ops`, `errors`, and module-specific counters (`created`, `relays`, `open`, `peers`, etc.).
|
||||
Library-only modules may include `mode: 'local'`.
|
||||
Use **`getMetrics()`** for operational counters. `getStats()` spreads `_stats` (may be empty).
|
||||
|
||||
## Wire (enableGossip + topic)
|
||||
|
||||
| type | fields | direction | behavior |
|
||||
|------|--------|-----------|----------|
|
||||
| `proposal` | `proposal` object | inbound | `receiveProposal` |
|
||||
| `vote` | `proposalId`, `vote` | inbound | `receiveVote` |
|
||||
|
||||
Outbound proposals also use `gossipSend` from `_sendProposalViaProtomux`.
|
||||
|
||||
## Errors
|
||||
|
||||
Stable message substrings: see [`../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
|
||||
|
||||
## P2P
|
||||
|
||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `hyper-p2p-causal-consensus/v1`.
|
||||
| Message | Source |
|
||||
|---------|--------|
|
||||
| `Consensus instance closed` | `propose` when closed |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cd modules/consensus-coordination/hyper-p2p-causal-consensus
|
||||
npm install && npm test
|
||||
bare examples/basic.js
|
||||
```
|
||||
|
||||
Integration: [`../../real_tests/integration/causal-consensus-two-node.js`](../../../real_tests/integration/causal-consensus-two-node.js)
|
||||
Register enough `addPeer` entries to satisfy quorum in unit tests without a live swarm.
|
||||
|
||||
@@ -1,44 +1,33 @@
|
||||
# Architecture: hyper-p2p-causal-consensus
|
||||
|
||||
**Category:** Consensus & coordination
|
||||
**Protocol:** `hyper-p2p-causal-consensus/v1`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
App[Application] --> Mod[HyperP2PCausalConsensus]
|
||||
Mod --> Mux[Protomux hyper-p2p-causal-consensus/v1]
|
||||
Mux --> Swarm[Hyperswarm]
|
||||
```
|
||||
|
||||
## Sequence (P2P)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App
|
||||
participant Mod as Module
|
||||
participant SW as Hyperswarm
|
||||
participant Peer
|
||||
App->>Mod: ready(topic)
|
||||
Mod->>SW: join(topic)
|
||||
SW->>Peer: connection
|
||||
Mod->>Peer: gossip / Protomux
|
||||
Peer-->>Mod: onmessage
|
||||
Mod-->>App: emit(event)
|
||||
flowchart TB
|
||||
App --> CC[HyperP2PCausalConsensus]
|
||||
CC --> Proposals[proposals Map]
|
||||
CC --> Decided[decidedOrders Map]
|
||||
CC --> VC[vectorClock Map]
|
||||
CC --> HB[optional hyperbee]
|
||||
CC --> SW[initModuleSwarm when enableGossip]
|
||||
```
|
||||
|
||||
## Wire messages
|
||||
|
||||
| type | fields | direction | behavior |
|
||||
|------|--------|-----------|----------|
|
||||
| `proposal` | proposal, proposalId, type, vote | gossip | Handled in onmessage / gossipSend |
|
||||
| `vote` | proposalId, vote | gossip | Handled in onmessage / gossipSend |
|
||||
| `proposal` | `proposal` | inbound | Dedup, verify, vote, maybe finalize |
|
||||
| `vote` | `proposalId`, `vote` | inbound | Merge vote map, quorum check |
|
||||
|
||||
## State model
|
||||
|
||||
- In-memory `Map` / `Set` structures for hot path
|
||||
- Optional Hyperbee/Hypercore persistence when `storageDir` or `memoryOnly` is configured
|
||||
- `close()` tears down swarm, timers, and clears ephemeral state
|
||||
| Store | Content |
|
||||
|-------|---------|
|
||||
| `proposals` | Pending/decided proposal + votes Map |
|
||||
| `decidedOrders` | Total order sequence → event |
|
||||
| `vectorClock` | Per-peer counters |
|
||||
| `forksDetected` | Set of peer ids |
|
||||
|
||||
## Composition
|
||||
|
||||
Composes with: `hyper-p2p-distributed-lock`, `hyper-p2p-quorum-pool`.
|
||||
|
||||
Integrate `hyper-p2p-crdt-version-vector` via `integrateVectorClock`; use `hyper-p2p-presence` for peer discovery before `addPeer`.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const HyperP2PCausalConsensus = require('../index.js')
|
||||
|
||||
async function main () {
|
||||
const keyPair = require('hypercore-crypto').keyPair()
|
||||
const consensus = new HyperP2PCausalConsensus({
|
||||
localId: 'example-peer',
|
||||
keyPair,
|
||||
quorumThreshold: 0.6,
|
||||
enableSigning: true,
|
||||
topic: process.argv[2] || null,
|
||||
enableGossip: !!process.argv[2]
|
||||
})
|
||||
|
||||
consensus.addPeer('peer-alpha', require('hypercore-crypto').keyPair().publicKey.toString('hex'))
|
||||
consensus.addPeer('peer-beta', require('hypercore-crypto').keyPair().publicKey.toString('hex'))
|
||||
|
||||
consensus.on('consensus', (d) => {
|
||||
console.log('[consensus]', d.order, d.data)
|
||||
})
|
||||
|
||||
const p1 = await consensus.propose({ type: 'demo', n: 1 })
|
||||
await consensus.vote(p1, true)
|
||||
|
||||
console.log('metrics:', consensus.getMetrics())
|
||||
console.log('decided:', consensus.getAllDecided())
|
||||
|
||||
await consensus.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user