@bitsocial/pubsub-votes 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +212 -0
- package/dist/chain/bucket.d.ts +13 -0
- package/dist/chain/bucket.js +24 -0
- package/dist/chain/ticker.d.ts +15 -0
- package/dist/chain/ticker.js +25 -0
- package/dist/chain/types.d.ts +90 -0
- package/dist/chain/types.js +1 -0
- package/dist/checkpoint/codec.d.ts +54 -0
- package/dist/checkpoint/codec.js +99 -0
- package/dist/client/root-puller.d.ts +49 -0
- package/dist/client/root-puller.js +140 -0
- package/dist/client/voter.d.ts +225 -0
- package/dist/client/voter.js +1195 -0
- package/dist/crdt/codec.d.ts +41 -0
- package/dist/crdt/codec.js +137 -0
- package/dist/crdt/crdt.d.ts +22 -0
- package/dist/crdt/crdt.js +127 -0
- package/dist/crdt/store.d.ts +8 -0
- package/dist/crdt/store.js +23 -0
- package/dist/crdt/types.d.ts +87 -0
- package/dist/crdt/types.js +1 -0
- package/dist/encoding/canonical.d.ts +22 -0
- package/dist/encoding/canonical.js +26 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +111 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +39 -0
- package/dist/rules/constant.d.ts +14 -0
- package/dist/rules/constant.js +18 -0
- package/dist/rules/erc20-balance.d.ts +30 -0
- package/dist/rules/erc20-balance.js +44 -0
- package/dist/rules/erc721-min-balance.d.ts +18 -0
- package/dist/rules/erc721-min-balance.js +56 -0
- package/dist/rules/registry.d.ts +42 -0
- package/dist/rules/registry.js +61 -0
- package/dist/rules/types.d.ts +74 -0
- package/dist/rules/types.js +1 -0
- package/dist/schema/common.d.ts +25 -0
- package/dist/schema/common.js +24 -0
- package/dist/schema/criteria.d.ts +78 -0
- package/dist/schema/criteria.js +76 -0
- package/dist/schema/directory.d.ts +42 -0
- package/dist/schema/directory.js +52 -0
- package/dist/schema/votes.d.ts +55 -0
- package/dist/schema/votes.js +116 -0
- package/dist/signer/eip712.d.ts +103 -0
- package/dist/signer/eip712.js +85 -0
- package/dist/signer/types.d.ts +31 -0
- package/dist/signer/types.js +1 -0
- package/dist/storage/browser.d.ts +3 -0
- package/dist/storage/browser.js +110 -0
- package/dist/storage/memory.d.ts +10 -0
- package/dist/storage/memory.js +56 -0
- package/dist/storage/node.d.ts +5 -0
- package/dist/storage/node.js +106 -0
- package/dist/storage/types.d.ts +46 -0
- package/dist/storage/types.js +1 -0
- package/dist/store/indexeddb.d.ts +9 -0
- package/dist/store/indexeddb.js +72 -0
- package/dist/store/memory.d.ts +15 -0
- package/dist/store/memory.js +22 -0
- package/dist/store/select.d.ts +15 -0
- package/dist/store/select.js +64 -0
- package/dist/store/sqlite.d.ts +11 -0
- package/dist/store/sqlite.js +68 -0
- package/dist/store/types.d.ts +57 -0
- package/dist/store/types.js +1 -0
- package/dist/tally/tally.d.ts +44 -0
- package/dist/tally/tally.js +89 -0
- package/dist/tally/types.d.ts +51 -0
- package/dist/tally/types.js +13 -0
- package/dist/topic.d.ts +20 -0
- package/dist/topic.js +28 -0
- package/dist/transport/accepted-dedup.d.ts +30 -0
- package/dist/transport/accepted-dedup.js +34 -0
- package/dist/transport/announce/browser.d.ts +9 -0
- package/dist/transport/announce/browser.js +14 -0
- package/dist/transport/announce/node.d.ts +38 -0
- package/dist/transport/announce/node.js +162 -0
- package/dist/transport/announce/types.d.ts +74 -0
- package/dist/transport/announce/types.js +16 -0
- package/dist/transport/bundle-store.d.ts +11 -0
- package/dist/transport/bundle-store.js +34 -0
- package/dist/transport/chase.d.ts +83 -0
- package/dist/transport/chase.js +88 -0
- package/dist/transport/gossip-validator.d.ts +107 -0
- package/dist/transport/gossip-validator.js +99 -0
- package/dist/transport/helia.d.ts +41 -0
- package/dist/transport/helia.js +98 -0
- package/dist/transport/integration/harness.d.ts +60 -0
- package/dist/transport/integration/harness.js +262 -0
- package/dist/transport/messages.d.ts +97 -0
- package/dist/transport/messages.js +117 -0
- package/dist/transport/rate-limit.d.ts +11 -0
- package/dist/transport/rate-limit.js +20 -0
- package/dist/transport/transport.d.ts +20 -0
- package/dist/transport/transport.js +35 -0
- package/dist/transport/types.d.ts +165 -0
- package/dist/transport/types.js +1 -0
- package/dist/verify/background.d.ts +81 -0
- package/dist/verify/background.js +236 -0
- package/dist/verify/bundle.d.ts +58 -0
- package/dist/verify/bundle.js +84 -0
- package/dist/verify/cache.d.ts +48 -0
- package/dist/verify/cache.js +62 -0
- package/dist/verify/constraints.d.ts +16 -0
- package/dist/verify/constraints.js +35 -0
- package/dist/verify/gate-result-cache.d.ts +65 -0
- package/dist/verify/gate-result-cache.js +91 -0
- package/dist/verify/name-resolution-cache.d.ts +59 -0
- package/dist/verify/name-resolution-cache.js +64 -0
- package/dist/verify/signature.d.ts +9 -0
- package/dist/verify/signature.js +55 -0
- package/dist/verify/types.d.ts +101 -0
- package/dist/verify/types.js +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-record announcer types (see DESIGN.md "Deferred pkc-js work", provider-record
|
|
3
|
+
* announces). The announcer is the write side of the delegated-routing seam: querying rides the
|
|
4
|
+
* injected node's `libp2p.contentRouting` (the host wires its routers into `libp2p.services`), but
|
|
5
|
+
* the pinned js stack has NO working announce path — `@helia/delegated-routing-v1-http-api-client`'s
|
|
6
|
+
* `provide()` is a literal noop — so provider records only get written by a direct
|
|
7
|
+
* `PUT /routing/v1/providers` to each router. This library is the node that actually holds the
|
|
8
|
+
* state (topic membership, fetch responder, blockstore), so its records are always truthful and
|
|
9
|
+
* current; `PubsubVoterOptions.httpRouterUrls` exists ONLY because announcing cannot ride the
|
|
10
|
+
* `contentRouting` seam.
|
|
11
|
+
*
|
|
12
|
+
* The implementation is Node-only: the browser build swaps `node.js` for the inert `browser.js`
|
|
13
|
+
* stub via the package.json `browser` field (the same remap as `src/storage/`), because a browser
|
|
14
|
+
* peer is not dialable and must never announce.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The slice of the injected node's libp2p the announcer reads, declared structurally (a running
|
|
18
|
+
* `Libp2p` satisfies it). `self:peer:update` fires when the node's address set changes — an
|
|
19
|
+
* AutoTLS certificate landing, a WebRTC-Direct certhash rotation — and triggers a re-announce so
|
|
20
|
+
* records never carry stale addresses for more than a debounce window.
|
|
21
|
+
*/
|
|
22
|
+
export interface AnnouncerLibp2p {
|
|
23
|
+
peerId: {
|
|
24
|
+
toString(): string;
|
|
25
|
+
};
|
|
26
|
+
getMultiaddrs(): Array<{
|
|
27
|
+
toString(): string;
|
|
28
|
+
}>;
|
|
29
|
+
addEventListener(type: "self:peer:update", listener: () => void): void;
|
|
30
|
+
removeEventListener(type: "self:peer:update", listener: () => void): void;
|
|
31
|
+
}
|
|
32
|
+
export interface AnnouncerOptions {
|
|
33
|
+
/** Delegated Routing V1 base URLs to PUT provider records to. Empty means never announce. */
|
|
34
|
+
routerUrls: readonly string[];
|
|
35
|
+
/** The injected node's libp2p (peer id, current addresses, address-change events). */
|
|
36
|
+
libp2p: AnnouncerLibp2p;
|
|
37
|
+
/**
|
|
38
|
+
* The CIDs to announce, collected fresh per tick: every joined contest's criteria CID plus its
|
|
39
|
+
* current checkpoint root + chunk CIDs, batched into ONE record (`Keys` is an array) so a
|
|
40
|
+
* directory host costs one request per router per tick. An empty list skips the tick.
|
|
41
|
+
*/
|
|
42
|
+
keys(): Promise<string[]>;
|
|
43
|
+
/** Re-announce cadence (ms). Test seam; defaults to hourly (see `ANNOUNCE_INTERVAL_MS`). */
|
|
44
|
+
intervalMs?: number;
|
|
45
|
+
/** Change-coalescing window (ms). Test seam; defaults to `ANNOUNCE_DEBOUNCE_MS`. */
|
|
46
|
+
debounceMs?: number;
|
|
47
|
+
/** Per-router PUT deadline (ms). Test seam; defaults to `ANNOUNCE_ROUTER_TIMEOUT_MS`. */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Per-router failure hook (timeout, non-2xx, network error). Purely observational — a failing
|
|
51
|
+
* router never throws into the voter and is not retried; the next tick covers it.
|
|
52
|
+
*/
|
|
53
|
+
onError?(url: string, error: unknown): void;
|
|
54
|
+
}
|
|
55
|
+
/** The voter-facing announcer handle. All methods are synchronous and idempotent. */
|
|
56
|
+
export interface Announcer {
|
|
57
|
+
/**
|
|
58
|
+
* Begin announcing: subscribe to address changes and arm the periodic re-announce. Called when
|
|
59
|
+
* the voter's first topic joins (symmetric with the lazy fetch-responder lifecycle). Does not
|
|
60
|
+
* announce by itself — the join that triggered it also calls {@link notifyChange}.
|
|
61
|
+
*/
|
|
62
|
+
start(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Stop announcing and drop timers/listeners. Called when the last topic is left. Records
|
|
65
|
+
* already written age out by the router's TTL; there is no un-announce.
|
|
66
|
+
*/
|
|
67
|
+
stop(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Coalesced re-announce trigger: a contest joined, a checkpoint root changed, or the address
|
|
70
|
+
* set changed. The first call arms one debounce timer; further calls within the window ride
|
|
71
|
+
* it, so a gossip burst (or a directory-wide join) costs one announce.
|
|
72
|
+
*/
|
|
73
|
+
notifyChange(): void;
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-record announcer types (see DESIGN.md "Deferred pkc-js work", provider-record
|
|
3
|
+
* announces). The announcer is the write side of the delegated-routing seam: querying rides the
|
|
4
|
+
* injected node's `libp2p.contentRouting` (the host wires its routers into `libp2p.services`), but
|
|
5
|
+
* the pinned js stack has NO working announce path — `@helia/delegated-routing-v1-http-api-client`'s
|
|
6
|
+
* `provide()` is a literal noop — so provider records only get written by a direct
|
|
7
|
+
* `PUT /routing/v1/providers` to each router. This library is the node that actually holds the
|
|
8
|
+
* state (topic membership, fetch responder, blockstore), so its records are always truthful and
|
|
9
|
+
* current; `PubsubVoterOptions.httpRouterUrls` exists ONLY because announcing cannot ride the
|
|
10
|
+
* `contentRouting` seam.
|
|
11
|
+
*
|
|
12
|
+
* The implementation is Node-only: the browser build swaps `node.js` for the inert `browser.js`
|
|
13
|
+
* stub via the package.json `browser` field (the same remap as `src/storage/`), because a browser
|
|
14
|
+
* peer is not dialable and must never announce.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BundleStore } from "../crdt/types.js";
|
|
2
|
+
import type { BlockstoreLike } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A {@link BundleStore} backed by the host's Helia blockstore. `put` writes a bundle as a
|
|
5
|
+
* dag-cbor block locally; `get` reads by CID — and because bitswap retrieves through the
|
|
6
|
+
* blockstore, an unknown CID is fetched from peers. A fetch that fails or times out surfaces
|
|
7
|
+
* as `undefined` (not a throw), which the forward-gate reads as "unfetchable -> ignore"
|
|
8
|
+
* rather than a provable fault. Lives in transport/ (not crdt/) because it touches the
|
|
9
|
+
* blockstore; the CRDT stays libp2p-free.
|
|
10
|
+
*/
|
|
11
|
+
export declare function makeBlockstoreBundleStore(blockstore: BlockstoreLike): BundleStore;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { encodeBundle, decodeBundle, bundleCid } from "../crdt/codec.js";
|
|
2
|
+
/**
|
|
3
|
+
* A {@link BundleStore} backed by the host's Helia blockstore. `put` writes a bundle as a
|
|
4
|
+
* dag-cbor block locally; `get` reads by CID — and because bitswap retrieves through the
|
|
5
|
+
* blockstore, an unknown CID is fetched from peers. A fetch that fails or times out surfaces
|
|
6
|
+
* as `undefined` (not a throw), which the forward-gate reads as "unfetchable -> ignore"
|
|
7
|
+
* rather than a provable fault. Lives in transport/ (not crdt/) because it touches the
|
|
8
|
+
* blockstore; the CRDT stays libp2p-free.
|
|
9
|
+
*/
|
|
10
|
+
export function makeBlockstoreBundleStore(blockstore) {
|
|
11
|
+
return {
|
|
12
|
+
async put(bundle) {
|
|
13
|
+
const cid = await bundleCid(bundle);
|
|
14
|
+
await blockstore.put(cid, encodeBundle(bundle));
|
|
15
|
+
return cid;
|
|
16
|
+
},
|
|
17
|
+
async get(cid, options) {
|
|
18
|
+
try {
|
|
19
|
+
return decodeBundle(await blockstore.get(cid, options));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined; // unfetchable / undecodable / aborted — gate treats as ignore
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
async has(cid) {
|
|
26
|
+
try {
|
|
27
|
+
return await blockstore.has(cid);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { VotesBundle } from "../schema/votes.js";
|
|
3
|
+
import type { VerdictCache } from "../verify/cache.js";
|
|
4
|
+
import type { VerifyResult } from "../verify/types.js";
|
|
5
|
+
import type { PendingBundle } from "../verify/background.js";
|
|
6
|
+
/**
|
|
7
|
+
* The divergence chase: act on an advertised checkpoint root that differs from our own (a
|
|
8
|
+
* heartbeat or fetch-protocol hint — see DESIGN.md "Checkpoints", "Block pull"). Pull the
|
|
9
|
+
* blocks behind the root by CID (directed bitswap at the connected advertisers, through the
|
|
10
|
+
* injected `getBlock`) and admit each inlined bundle in two stages:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Offline, synchronous, before admit** (µs each): signature + criteria constraints via
|
|
13
|
+
* `verifyOffline` — a forged or malformed bundle dies here and is never admitted.
|
|
14
|
+
* 2. **Network, deferred**: the on-chain gate read and name resolution run in the background
|
|
15
|
+
* chain verifier (`deferVerify`, one batch per chased root so the gate reads batch into
|
|
16
|
+
* as few RPC round trips as the rule allows), which confirms or evicts each bundle after
|
|
17
|
+
* the fact. This is what keeps a cold join non-blocking: a 100-vote checkpoint admits in
|
|
18
|
+
* milliseconds instead of serializing 100 chain reads (see DESIGN.md "Background chain
|
|
19
|
+
* verification").
|
|
20
|
+
*
|
|
21
|
+
* So a single liar cannot inject a forged vote (offline check) nor hide an honest one
|
|
22
|
+
* (union-only merge); an *ineligible-wallet* bundle it injects is admitted provisionally,
|
|
23
|
+
* surfaces only as a `chainVerified: false` tally row, is never re-served in our checkpoint,
|
|
24
|
+
* and is evicted as soon as its batched gate read lands.
|
|
25
|
+
*
|
|
26
|
+
* Bounded like everything at the gate: a shared concurrency cap (`limit`), a per-root
|
|
27
|
+
* deadline whose `AbortSignal` cancels in-flight block wants, and in-flight dedup so a spray
|
|
28
|
+
* of the same root queues one chase, not many. A failed chase (unfetchable blocks, malformed
|
|
29
|
+
* chunks, all-invalid bundles) contributes nothing and throws nothing — the hint was never
|
|
30
|
+
* trusted. No libp2p import; pure seams, unit-testable offline.
|
|
31
|
+
*/
|
|
32
|
+
export interface RootChaserDeps {
|
|
33
|
+
/**
|
|
34
|
+
* Fetch one checkpoint block by CID (blockstore + directed bitswap); `undefined` or a
|
|
35
|
+
* throw means unavailable. `signal` aborts the want when the per-root deadline fires.
|
|
36
|
+
*/
|
|
37
|
+
getBlock: (cid: CID, signal: AbortSignal) => Promise<Uint8Array | undefined>;
|
|
38
|
+
/** Stage 1 only — signature + constraints, local and synchronous (the gate's same stage). */
|
|
39
|
+
verifyOffline: (bundle: VotesBundle) => Promise<VerifyResult>;
|
|
40
|
+
/** The gate's per-CID verdict cache, shared so chased bundles reuse (and feed) it. */
|
|
41
|
+
cache: VerdictCache;
|
|
42
|
+
/** The gate's freshness guard (see gossip-validator.ts); omitted ⇒ no check. */
|
|
43
|
+
isEvaluableNow?: (bundle: VotesBundle) => Promise<boolean>;
|
|
44
|
+
/** Skip bundles we already hold (their CID is in the store) without re-verifying. */
|
|
45
|
+
hasBundle: (cid: CID) => Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Store an offline-valid bundle's block bytes and admit its CID into the CRDT (idempotent).
|
|
48
|
+
* `verified: true` means a cached terminal verdict already covers the FULL pipeline (the
|
|
49
|
+
* bundle was verified before, e.g. by the forward-gate); `verified: false` is a provisional
|
|
50
|
+
* admit whose deferred checks ride `deferVerify`.
|
|
51
|
+
*/
|
|
52
|
+
admit: (args: {
|
|
53
|
+
cid: CID;
|
|
54
|
+
bytes: Uint8Array;
|
|
55
|
+
bundle: VotesBundle;
|
|
56
|
+
verified: boolean;
|
|
57
|
+
}) => Promise<void>;
|
|
58
|
+
/** Hand one chased root's provisionally admitted bundles to the background chain verifier. */
|
|
59
|
+
deferVerify: (entries: PendingBundle[]) => void;
|
|
60
|
+
/** Called once per chase that admitted at least one bundle (drives tally updates). */
|
|
61
|
+
onMerged?: () => void;
|
|
62
|
+
/** Concurrency limiter shared across chases (a root spray queues, never floods). */
|
|
63
|
+
limit: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
64
|
+
/** Per-root deadline (ms); on expiry the abort signal fires and the chase yields nothing. */
|
|
65
|
+
timeoutMs: number;
|
|
66
|
+
}
|
|
67
|
+
export interface RootChaser {
|
|
68
|
+
/**
|
|
69
|
+
* Chase one advertised root, fire-and-forget: never throws, never blocks the caller
|
|
70
|
+
* (the validator hands hints here without awaiting). A root already being chased is
|
|
71
|
+
* dropped — the in-flight run covers it.
|
|
72
|
+
*
|
|
73
|
+
* `chunks` is the optional piggybacked chunk-CID index from a fetch-protocol root record
|
|
74
|
+
* (see DESIGN.md "Block pull"): when supplied and it re-derives to `root`, the chase skips
|
|
75
|
+
* the root-manifest bitswap round-trip and pulls the chunks directly. A heartbeat hint (no
|
|
76
|
+
* index) omits it and takes the manifest-fetch path. The index is verified against `root`,
|
|
77
|
+
* so a bad one simply falls back — never a trust vector.
|
|
78
|
+
*/
|
|
79
|
+
chase(root: CID, chunks?: CID[]): void;
|
|
80
|
+
/** Roots currently being chased (for tests/introspection). */
|
|
81
|
+
inFlight(): number;
|
|
82
|
+
}
|
|
83
|
+
export declare function makeRootChaser(deps: RootChaserDeps): RootChaser;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { decodeCheckpoint } from "../checkpoint/codec.js";
|
|
2
|
+
import { encodeBundle, bundleCidForBytes } from "../crdt/codec.js";
|
|
3
|
+
export function makeRootChaser(deps) {
|
|
4
|
+
const { getBlock, verifyOffline, cache, isEvaluableNow, hasBundle, admit, deferVerify, onMerged, limit, timeoutMs } = deps;
|
|
5
|
+
const inFlight = new Set();
|
|
6
|
+
async function runChase(root, chunks) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
|
+
try {
|
|
10
|
+
// Race the decode against the deadline so even a `getBlock` that ignores its abort
|
|
11
|
+
// signal cannot pin this chase slot past `timeoutMs` — the slot is always freed.
|
|
12
|
+
const winners = await Promise.race([
|
|
13
|
+
decodeCheckpoint(root, async (cid) => {
|
|
14
|
+
if (controller.signal.aborted)
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return await getBlock(cid, controller.signal);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined; // unfetchable/aborted — decode throws "unavailable", chase yields nothing
|
|
21
|
+
}
|
|
22
|
+
}, chunks),
|
|
23
|
+
new Promise((resolve) => {
|
|
24
|
+
controller.signal.addEventListener("abort", () => resolve(undefined), { once: true });
|
|
25
|
+
})
|
|
26
|
+
]);
|
|
27
|
+
if (winners === undefined)
|
|
28
|
+
return; // deadline hit — the hint contributed nothing
|
|
29
|
+
let merged = false;
|
|
30
|
+
const pending = [];
|
|
31
|
+
for (const bundle of winners) {
|
|
32
|
+
if (controller.signal.aborted)
|
|
33
|
+
break;
|
|
34
|
+
// Re-encoding the decoded bundle reproduces the exact block bytes (the codec is
|
|
35
|
+
// canonical), so the CID matches the advertiser's block and dedups everywhere.
|
|
36
|
+
const bytes = encodeBundle(bundle);
|
|
37
|
+
const cid = await bundleCidForBytes(bytes);
|
|
38
|
+
if (await hasBundle(cid))
|
|
39
|
+
continue; // already held — nothing to verify
|
|
40
|
+
const cached = cache.get(cid);
|
|
41
|
+
if (cached) {
|
|
42
|
+
if (!cached.valid)
|
|
43
|
+
continue; // known bad — skip
|
|
44
|
+
await admit({ cid, bytes, bundle, verified: true }); // full pipeline already passed
|
|
45
|
+
merged = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (isEvaluableNow && !(await isEvaluableNow(bundle)))
|
|
49
|
+
continue; // transient, uncached
|
|
50
|
+
const offline = await verifyOffline(bundle);
|
|
51
|
+
if (!offline.valid) {
|
|
52
|
+
// A liar's forged/malformed bundle dies here, before admit. A provable
|
|
53
|
+
// offline reject (bad signature, constraints) is terminal — cache it so a
|
|
54
|
+
// re-served copy short-circuits; a transient `ignore` stays uncached.
|
|
55
|
+
if (offline.disposition === "reject")
|
|
56
|
+
cache.set(cid, offline);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
await admit({ cid, bytes, bundle, verified: false });
|
|
60
|
+
pending.push({ cid, bundle });
|
|
61
|
+
merged = true;
|
|
62
|
+
}
|
|
63
|
+
// One batch per chased root: the background verifier groups these by sample block
|
|
64
|
+
// and batches the gate reads (see verify/background.ts).
|
|
65
|
+
if (pending.length > 0)
|
|
66
|
+
deferVerify(pending);
|
|
67
|
+
if (merged)
|
|
68
|
+
onMerged?.();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
chase(root, chunks) {
|
|
76
|
+
const key = root.toString();
|
|
77
|
+
if (inFlight.has(key))
|
|
78
|
+
return; // the in-flight run covers this hint (chunks derive from root)
|
|
79
|
+
inFlight.add(key);
|
|
80
|
+
void limit(() => runChase(root, chunks))
|
|
81
|
+
.catch(() => { }) // a failed chase contributes nothing; the hint was never trusted
|
|
82
|
+
.finally(() => inFlight.delete(key));
|
|
83
|
+
},
|
|
84
|
+
inFlight() {
|
|
85
|
+
return inFlight.size;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { VotesBundle } from "../schema/votes.js";
|
|
3
|
+
import { type VerdictCache } from "../verify/cache.js";
|
|
4
|
+
import type { BundleVerifier } from "../verify/types.js";
|
|
5
|
+
import type { AcceptedDedup } from "./accepted-dedup.js";
|
|
6
|
+
import type { RootRecord, VoteMessage } from "./messages.js";
|
|
7
|
+
/**
|
|
8
|
+
* The forward-gate: the async gossipsub topic validator's decision core, written as a pure
|
|
9
|
+
* function over injected seams (no libp2p import) so it is fully unit-testable. It runs the
|
|
10
|
+
* FULL validity pipeline on the bundle INLINED in a received message BEFORE the message is
|
|
11
|
+
* re-forwarded, so an invalid bundle never crosses an honest hop and gossipsub's `reject`
|
|
12
|
+
* scores the sender for semantic — not just byte-level — badness. See DESIGN.md "Transport".
|
|
13
|
+
*
|
|
14
|
+
* The payload is a two-kind discriminated union (see transport/messages.ts):
|
|
15
|
+
* - a **bundle delta** — the wallet's own bundle as exact block bytes, validated straight
|
|
16
|
+
* from the message (no fetch toward the publisher exists on this path);
|
|
17
|
+
* - a **root record** — the checkpoint heartbeat, a fixed-shape unverifiable *hint* that
|
|
18
|
+
* short-circuits at layer 1: forwarded within its own per-peer rate and handed to the
|
|
19
|
+
* chase logic, never verified here (only equality with our own root is checkable) and
|
|
20
|
+
* never trusted.
|
|
21
|
+
*
|
|
22
|
+
* Verdicts (who gets blamed):
|
|
23
|
+
* - "accept": the inlined bundle verified (or is a known-valid re-publish), or a
|
|
24
|
+
* well-formed within-rate root record — deliver, merge (bundles), forward.
|
|
25
|
+
* - "reject": something is PROVABLY invalid (malformed message, over the derived size cap,
|
|
26
|
+
* a bundle that fails verification) — drop, do not forward, penalize the sender.
|
|
27
|
+
* - "ignore": no verdict reachable through no provable fault of the sender — either a
|
|
28
|
+
* transient/local condition (over the per-peer rate, internal error, deadline) or a
|
|
29
|
+
* view-/clock-dependent verdict two honest peers can disagree on right now: a bundle
|
|
30
|
+
* bucketed ahead of our chain head (`isEvaluableNow`), or a `community.name` resolved at
|
|
31
|
+
* head during a re-point window. Drop, do not forward, do NOT penalize, and do NOT cache
|
|
32
|
+
* (it can change) — using `reject` here would punish honest relayers for transient
|
|
33
|
+
* conditions and hand attackers a grief vector.
|
|
34
|
+
*
|
|
35
|
+
* Cheap-to-expensive with early exit: size/decode/rate first, then hash + caches, then the
|
|
36
|
+
* verify pipeline — so a re-published known bundle costs one hash and zero chain work.
|
|
37
|
+
*/
|
|
38
|
+
export type MessageVerdict = "accept" | "reject" | "ignore";
|
|
39
|
+
export interface GossipGateDeps {
|
|
40
|
+
/** Decode a payload to its message kind; throws on malformed (see transport/messages.ts). */
|
|
41
|
+
decodeMessage: (data: Uint8Array) => VoteMessage;
|
|
42
|
+
/**
|
|
43
|
+
* Decode inlined bundle-block bytes and hash them to the bundle CID (the verdict-cache
|
|
44
|
+
* key). Throws on malformed block bytes — provable layer-1 badness.
|
|
45
|
+
*/
|
|
46
|
+
parseBundle: (blockBytes: Uint8Array) => Promise<{
|
|
47
|
+
cid: CID;
|
|
48
|
+
bundle: VotesBundle;
|
|
49
|
+
}>;
|
|
50
|
+
/** The full validity pipeline for one bundle (see verify/bundle.ts). */
|
|
51
|
+
verifier: BundleVerifier;
|
|
52
|
+
/**
|
|
53
|
+
* Clock-aware freshness guard, kept OUT of the pure (cacheable) verifier: is this bundle's
|
|
54
|
+
* bucket sample block already reachable from our chain head? A bundle dated to a future
|
|
55
|
+
* bucket (the voter's head ahead of ours, clock skew, or an absurd `blockNumber`) is not
|
|
56
|
+
* yet evaluable — transient, so the gate `ignore`s it (no penalty, uncached) until our head
|
|
57
|
+
* catches up. Omitted ⇒ no freshness check. Steady-state votes resolve `true` with no chain
|
|
58
|
+
* read; only a look-ahead bundle costs a (memoized) head read.
|
|
59
|
+
*/
|
|
60
|
+
isEvaluableNow?: (bundle: VotesBundle) => Promise<boolean>;
|
|
61
|
+
/** Per-CID verdict cache — dedups re-published bundles and known-bad blocks. */
|
|
62
|
+
cache: VerdictCache;
|
|
63
|
+
/**
|
|
64
|
+
* Optional dedup of already-accepted votes by `(wallet, bucket, votes)`. A same-bucket,
|
|
65
|
+
* same-choice re-sign under fresh bytes is dropped — not verified, not merged, not
|
|
66
|
+
* forwarded — so a re-sign flood costs no gate read and does not amplify. Omitted ⇒ every
|
|
67
|
+
* fresh bundle is verified on its own.
|
|
68
|
+
*/
|
|
69
|
+
acceptedDedup?: AcceptedDedup;
|
|
70
|
+
/**
|
|
71
|
+
* Store the verified bundle's exact block bytes and admit its CID into the CRDT
|
|
72
|
+
* (idempotent). Exact bytes preserve byte-identity with the sender's block.
|
|
73
|
+
*/
|
|
74
|
+
admit: (args: {
|
|
75
|
+
cid: CID;
|
|
76
|
+
bytes: Uint8Array;
|
|
77
|
+
bundle: VotesBundle;
|
|
78
|
+
}) => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Concurrency limiter (p-limit) shared across in-flight verifications — the gate chain
|
|
81
|
+
* read and name resolution are network calls, so concurrent RPC work stays bounded.
|
|
82
|
+
*/
|
|
83
|
+
limit: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
84
|
+
/** Per-peer rate gate for bundle-kind messages; `false` means over-rate this window. */
|
|
85
|
+
allowBundlePeer: (peer: string) => boolean;
|
|
86
|
+
/** Per-peer rate gate for root-kind messages (heartbeats are ~1 per 10 min when honest). */
|
|
87
|
+
allowRootPeer: (peer: string) => boolean;
|
|
88
|
+
/** Called after a fresh bundle is verified and merged (drives tally-update notifications). */
|
|
89
|
+
onAccept?: (cid: CID, bundle: VotesBundle, from: string) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Called with every well-formed, within-rate root record (an unverifiable hint). NOT
|
|
92
|
+
* awaited — acting on a hint (the directed-bitswap chase) is lazy and bounded elsewhere,
|
|
93
|
+
* never on the validator's critical path.
|
|
94
|
+
*/
|
|
95
|
+
onRootRecord?: (record: RootRecord, from: string) => void;
|
|
96
|
+
/** The criteria-derived cap for a bundle-kind message (see messages.ts). Over ⇒ reject. */
|
|
97
|
+
maxBundleMessageBytes: number;
|
|
98
|
+
/** The fixed cap for a root-kind message (~100 B record). Over ⇒ reject. */
|
|
99
|
+
maxRootMessageBytes: number;
|
|
100
|
+
/** Hard per-message validation deadline; on expiry the verdict is `ignore`. */
|
|
101
|
+
timeoutMs: number;
|
|
102
|
+
}
|
|
103
|
+
export interface GossipGate {
|
|
104
|
+
/** Validate one received message; the returned verdict maps to gossipsub accept/reject/ignore. */
|
|
105
|
+
validate(data: Uint8Array, from: string): Promise<MessageVerdict>;
|
|
106
|
+
}
|
|
107
|
+
export declare function makeGossipGate(deps: GossipGateDeps): GossipGate;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { isCacheableVerdict } from "../verify/cache.js";
|
|
2
|
+
/** Resolve `p`, or `onTimeout` if it does not settle within `ms`; a rejection also yields `onTimeout`. */
|
|
3
|
+
function withDeadline(p, ms, onTimeout) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
let settled = false;
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
if (!settled) {
|
|
8
|
+
settled = true;
|
|
9
|
+
resolve(onTimeout);
|
|
10
|
+
}
|
|
11
|
+
}, ms);
|
|
12
|
+
p.then((v) => {
|
|
13
|
+
if (!settled) {
|
|
14
|
+
settled = true;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
resolve(v);
|
|
17
|
+
}
|
|
18
|
+
}, () => {
|
|
19
|
+
if (!settled) {
|
|
20
|
+
settled = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(onTimeout);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function makeGossipGate(deps) {
|
|
28
|
+
const { decodeMessage, parseBundle, verifier, isEvaluableNow, cache, acceptedDedup, admit, limit, allowBundlePeer, allowRootPeer, onAccept, onRootRecord, maxBundleMessageBytes, maxRootMessageBytes, timeoutMs } = deps;
|
|
29
|
+
async function doValidate(data, from) {
|
|
30
|
+
// Layer 1: size and decode — the cheapest, pre-verify checks.
|
|
31
|
+
if (data.length > Math.max(maxBundleMessageBytes, maxRootMessageBytes))
|
|
32
|
+
return "reject";
|
|
33
|
+
let message;
|
|
34
|
+
try {
|
|
35
|
+
message = decodeMessage(data);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "reject"; // malformed bytes / not one of the two kinds — provable layer-1 badness
|
|
39
|
+
}
|
|
40
|
+
if (message.kind === "root") {
|
|
41
|
+
if (data.length > maxRootMessageBytes)
|
|
42
|
+
return "reject";
|
|
43
|
+
if (!allowRootPeer(from))
|
|
44
|
+
return "ignore";
|
|
45
|
+
// An unverifiable hint: forward within rate, hand to the (lazy, bounded) chase.
|
|
46
|
+
// Never `reject`ed on content — only equality with our own root is checkable.
|
|
47
|
+
onRootRecord?.(message.record, from);
|
|
48
|
+
return "accept";
|
|
49
|
+
}
|
|
50
|
+
if (data.length > maxBundleMessageBytes)
|
|
51
|
+
return "reject";
|
|
52
|
+
if (!allowBundlePeer(from))
|
|
53
|
+
return "ignore";
|
|
54
|
+
let cid;
|
|
55
|
+
let bundle;
|
|
56
|
+
try {
|
|
57
|
+
({ cid, bundle } = await parseBundle(message.bundle));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "reject"; // malformed bundle block bytes — provable
|
|
61
|
+
}
|
|
62
|
+
// A known bundle short-circuits on the verdict cache for the cost of one hash: a
|
|
63
|
+
// re-published valid bundle (e.g. a client refreshing its vote or re-announcing a
|
|
64
|
+
// withdrawal) is forwarded so late peers converge, without re-verifying or re-merging;
|
|
65
|
+
// a known-bad one rejects without work.
|
|
66
|
+
const cached = cache.get(cid);
|
|
67
|
+
if (cached)
|
|
68
|
+
return cached.valid ? "accept" : cached.disposition;
|
|
69
|
+
// Freshness guard (transient, so it runs here — not in the cacheable verifier — and
|
|
70
|
+
// its verdict is never cached): a bundle bucketed ahead of our head is not yet
|
|
71
|
+
// evaluable, so `ignore` it without penalty until our head advances. Also defuses an
|
|
72
|
+
// absurd-future `blockNumber` (e.g. uint256.max) — it never merges into the set.
|
|
73
|
+
if (isEvaluableNow && !(await isEvaluableNow(bundle)))
|
|
74
|
+
return "ignore";
|
|
75
|
+
// Re-sign flood guard: a same-bucket, same-choice re-sign of a vote we already accepted
|
|
76
|
+
// is inert under LWW, so drop it WITHOUT verifying (no gate read), merging, or
|
|
77
|
+
// forwarding — the anti-amplification win. Safe on the untrusted `address` because a
|
|
78
|
+
// hit only ever suppresses something an honest peer already forwarded.
|
|
79
|
+
if (acceptedDedup?.isResignDuplicate(bundle))
|
|
80
|
+
return "ignore";
|
|
81
|
+
const verdict = await limit(() => verifier.verify(bundle));
|
|
82
|
+
if (isCacheableVerdict(verdict))
|
|
83
|
+
cache.set(cid, verdict); // only terminal verdicts (accept / provable reject)
|
|
84
|
+
if (!verdict.valid)
|
|
85
|
+
return verdict.disposition;
|
|
86
|
+
await admit({ cid, bytes: message.bundle, bundle });
|
|
87
|
+
acceptedDedup?.record(bundle);
|
|
88
|
+
onAccept?.(cid, bundle, from);
|
|
89
|
+
return "accept";
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
validate(data, from) {
|
|
93
|
+
// Whole-message deadline: an internal hang or slow RPC/name resolution yields
|
|
94
|
+
// `ignore`, never a stuck validator (which would strand the message in gossipsub's
|
|
95
|
+
// mcache).
|
|
96
|
+
return withDeadline(doValidate(data, from), timeoutMs, "ignore");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { BlockstoreLike, FetchServiceLike, HeliaInstance, PubsubService } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The host's raw blockstore surface. `get` may return EITHER a `Promise<Uint8Array>` (a plain
|
|
5
|
+
* `interface-blockstore`) OR an async generator yielding the block's bytes — Helia's real
|
|
6
|
+
* `BlockStorage` implements the streaming `Blocks` interface and yields the block (one chunk in
|
|
7
|
+
* practice), not a bare promise. The library works against the simpler {@link BlockstoreLike}
|
|
8
|
+
* contract, so {@link adaptBlockstore} normalises either shape at this one boundary.
|
|
9
|
+
*/
|
|
10
|
+
interface RawBlockstore {
|
|
11
|
+
get(cid: CID, options?: {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): AsyncIterable<Uint8Array> | Promise<Uint8Array>;
|
|
14
|
+
put(cid: CID, block: Uint8Array, options?: {
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}): Promise<CID>;
|
|
17
|
+
has(cid: CID, options?: {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}): Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Adapt a raw blockstore to the library's {@link BlockstoreLike} contract: normalise `get` to a
|
|
23
|
+
* single `Uint8Array`. Helia's `BlockStorage.get` yields the block over an async generator (a
|
|
24
|
+
* single chunk for the sub-1 MiB blocks this library stores; chunks are concatenated defensively),
|
|
25
|
+
* while a plain blockstore returns a promise — both are handled. `put`/`has` already return
|
|
26
|
+
* promises, so they pass through. Exported so the transport (and the integration harness) adapt the
|
|
27
|
+
* injected node the same way.
|
|
28
|
+
*/
|
|
29
|
+
export declare function adaptBlockstore(raw: RawBlockstore): BlockstoreLike;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve and validate the gossipsub service, blockstore, and fetch service on an injected
|
|
32
|
+
* Helia node, throwing {@link MissingPubsubError} / {@link MissingBlockstoreError} /
|
|
33
|
+
* {@link MissingFetchError} if any is absent or malformed. Returns the narrowed handles so
|
|
34
|
+
* callers do not re-check.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requireHeliaServices(helia: HeliaInstance): {
|
|
37
|
+
pubsub: PubsubService;
|
|
38
|
+
blockstore: BlockstoreLike;
|
|
39
|
+
fetch: FetchServiceLike;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MissingBlockstoreError, MissingFetchError, MissingPubsubError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The helia/libp2p-touching glue. Like the rest of `transport/`, this is the only place
|
|
4
|
+
* that reaches into the host node; the core never imports it.
|
|
5
|
+
*
|
|
6
|
+
* `requireHeliaServices` is the one piece live today: the host injects its running Helia
|
|
7
|
+
* node directly (no adapter), and we cannot trust the type — `libp2p.services.pubsub` is
|
|
8
|
+
* `unknown`, a plain Helia node has none, and a malformed object may lack a blockstore —
|
|
9
|
+
* so we validate both at construction and fail fast (`MissingPubsubError` /
|
|
10
|
+
* `MissingBlockstoreError`) instead of letting a later publish/subscribe/fetch throw
|
|
11
|
+
* obscurely.
|
|
12
|
+
*
|
|
13
|
+
* Note "bitswap" is not separately checkable: it is a block broker wired *beneath*
|
|
14
|
+
* `blockstore`, not a property of the Helia node, so validating the blockstore (the
|
|
15
|
+
* surface bitswap retrieves through) is the closest honest guarantee.
|
|
16
|
+
*/
|
|
17
|
+
/** Does `value` look like a pubsub service we can drive (the methods we depend on)? */
|
|
18
|
+
function isPubsubService(value) {
|
|
19
|
+
if (value === null || typeof value !== "object")
|
|
20
|
+
return false;
|
|
21
|
+
const candidate = value;
|
|
22
|
+
return (typeof candidate.publish === "function" &&
|
|
23
|
+
typeof candidate.subscribe === "function" &&
|
|
24
|
+
typeof candidate.unsubscribe === "function");
|
|
25
|
+
}
|
|
26
|
+
/** Does `value` look like a blockstore we can fetch/store blocks through? */
|
|
27
|
+
function isRawBlockstore(value) {
|
|
28
|
+
if (value === null || typeof value !== "object")
|
|
29
|
+
return false;
|
|
30
|
+
const candidate = value;
|
|
31
|
+
return (typeof candidate.get === "function" &&
|
|
32
|
+
typeof candidate.put === "function" &&
|
|
33
|
+
typeof candidate.has === "function");
|
|
34
|
+
}
|
|
35
|
+
function isAsyncIterable(value) {
|
|
36
|
+
return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Adapt a raw blockstore to the library's {@link BlockstoreLike} contract: normalise `get` to a
|
|
40
|
+
* single `Uint8Array`. Helia's `BlockStorage.get` yields the block over an async generator (a
|
|
41
|
+
* single chunk for the sub-1 MiB blocks this library stores; chunks are concatenated defensively),
|
|
42
|
+
* while a plain blockstore returns a promise — both are handled. `put`/`has` already return
|
|
43
|
+
* promises, so they pass through. Exported so the transport (and the integration harness) adapt the
|
|
44
|
+
* injected node the same way.
|
|
45
|
+
*/
|
|
46
|
+
export function adaptBlockstore(raw) {
|
|
47
|
+
return {
|
|
48
|
+
async get(cid, options) {
|
|
49
|
+
const result = raw.get(cid, options);
|
|
50
|
+
if (!isAsyncIterable(result))
|
|
51
|
+
return result; // a plain Promise<Uint8Array> blockstore
|
|
52
|
+
const chunks = [];
|
|
53
|
+
for await (const chunk of result)
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
if (chunks.length === 1)
|
|
56
|
+
return chunks[0];
|
|
57
|
+
if (chunks.length === 0)
|
|
58
|
+
throw new Error(`block ${cid.toString()} yielded no bytes`);
|
|
59
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
60
|
+
const out = new Uint8Array(total);
|
|
61
|
+
let offset = 0;
|
|
62
|
+
for (const chunk of chunks) {
|
|
63
|
+
out.set(chunk, offset);
|
|
64
|
+
offset += chunk.length;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
},
|
|
68
|
+
put: (cid, block) => raw.put(cid, block),
|
|
69
|
+
has: (cid) => raw.has(cid)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Does `value` look like a libp2p fetch service (the request + responder registration)? */
|
|
73
|
+
function isFetchService(value) {
|
|
74
|
+
if (value === null || typeof value !== "object")
|
|
75
|
+
return false;
|
|
76
|
+
const candidate = value;
|
|
77
|
+
return (typeof candidate.fetch === "function" &&
|
|
78
|
+
typeof candidate.registerLookupFunction === "function" &&
|
|
79
|
+
typeof candidate.unregisterLookupFunction === "function");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve and validate the gossipsub service, blockstore, and fetch service on an injected
|
|
83
|
+
* Helia node, throwing {@link MissingPubsubError} / {@link MissingBlockstoreError} /
|
|
84
|
+
* {@link MissingFetchError} if any is absent or malformed. Returns the narrowed handles so
|
|
85
|
+
* callers do not re-check.
|
|
86
|
+
*/
|
|
87
|
+
export function requireHeliaServices(helia) {
|
|
88
|
+
const pubsub = helia.libp2p?.services?.pubsub;
|
|
89
|
+
if (!isPubsubService(pubsub))
|
|
90
|
+
throw new MissingPubsubError();
|
|
91
|
+
const blockstore = helia.blockstore;
|
|
92
|
+
if (!isRawBlockstore(blockstore))
|
|
93
|
+
throw new MissingBlockstoreError();
|
|
94
|
+
const fetch = helia.libp2p?.services?.fetch;
|
|
95
|
+
if (!isFetchService(fetch))
|
|
96
|
+
throw new MissingFetchError();
|
|
97
|
+
return { pubsub, blockstore: adaptBlockstore(blockstore), fetch };
|
|
98
|
+
}
|