@bitsocial/pubsub-voting 0.1.1 → 0.1.3
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.
|
@@ -6,7 +6,7 @@ import type { VoteCrdt } from "../../crdt/types.js";
|
|
|
6
6
|
import { type VerdictCache } from "../../verify/cache.js";
|
|
7
7
|
import { type RootChaser } from "../chase.js";
|
|
8
8
|
import type { PubsubService, VoteTransport } from "../types.js";
|
|
9
|
-
import { type RootRecord } from "../messages.js";
|
|
9
|
+
import { type FetchRootRecord, type RootRecord } from "../messages.js";
|
|
10
10
|
/** The gossipsub peer-score methods used for assertions — on the concrete class, not the interface. */
|
|
11
11
|
interface ScoreOps {
|
|
12
12
|
getScore(peer: string): number;
|
|
@@ -41,13 +41,37 @@ export interface VoteNode {
|
|
|
41
41
|
/** Replace the injected verifier (e.g. a rejecter, or a slow one, for a specific assertion). */
|
|
42
42
|
setVerifier(verify: (bundle: VotesBundle) => Promise<BundleVerdict>): void;
|
|
43
43
|
/** Encode this node's current winner-set to a checkpoint (blocks written to its blockstore). */
|
|
44
|
-
checkpointRootRecord(): Promise<
|
|
44
|
+
checkpointRootRecord(): Promise<FetchRootRecord>;
|
|
45
|
+
/**
|
|
46
|
+
* Pull `from`'s root record over the REAL libp2p fetch protocol — the cold-start pull a
|
|
47
|
+
* joiner makes before chasing (voter.ts `#fetchRootWithRetry`). `undefined` when the peer
|
|
48
|
+
* serves nothing for the key.
|
|
49
|
+
*/
|
|
50
|
+
fetchRootRecord(from: VoteNode): Promise<FetchRootRecord | undefined>;
|
|
45
51
|
/** Publish this node's own root record on the topic (a heartbeat). */
|
|
46
52
|
publishOwnRoot(): Promise<void>;
|
|
47
53
|
/** Seed a bundle straight into this node's state (store block + CRDT merge), no network. */
|
|
48
54
|
admitBundle(bundle: VotesBundle): Promise<void>;
|
|
49
55
|
stop(): Promise<void>;
|
|
50
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One real loopback libp2p + Helia node carrying gossipsub (topic-scoped score params) and
|
|
59
|
+
* `@libp2p/fetch` — the raw host node with nothing else wired. {@link makeVoteNode} builds the
|
|
60
|
+
* harness transport on top of it; the three-node relay test instead hands it straight to the
|
|
61
|
+
* REAL `PubsubVoter` as the injected host node (the `PubsubVoterOptions.helia` seam).
|
|
62
|
+
*/
|
|
63
|
+
export declare function makeBareNode(topic: string): Promise<{
|
|
64
|
+
libp2p: Libp2p<{
|
|
65
|
+
identify: import("@libp2p/identify").Identify;
|
|
66
|
+
fetch: import("@libp2p/fetch").Fetch;
|
|
67
|
+
pubsub: import("@libp2p/gossipsub").GossipSub;
|
|
68
|
+
}>;
|
|
69
|
+
helia: Helia<Libp2p<{
|
|
70
|
+
identify: import("@libp2p/identify").Identify;
|
|
71
|
+
fetch: import("@libp2p/fetch").Fetch;
|
|
72
|
+
pubsub: import("@libp2p/gossipsub").GossipSub;
|
|
73
|
+
}>>;
|
|
74
|
+
}>;
|
|
51
75
|
/** Build one real node with the full forward-gate wired to real gossipsub. */
|
|
52
76
|
export declare function makeVoteNode(topic: string, options?: VoteNodeOptions): Promise<VoteNode>;
|
|
53
77
|
/**
|
|
@@ -17,7 +17,7 @@ import { encodeCheckpoint } from "../../checkpoint/codec.js";
|
|
|
17
17
|
import { makeGossipGate } from "../gossip-validator.js";
|
|
18
18
|
import { makeRootChaser, toChaseSession } from "../chase.js";
|
|
19
19
|
import { makeVoteTransport } from "../transport.js";
|
|
20
|
-
import { decodeVoteMessage, maxBundleMessageBytes, MAX_ROOT_MESSAGE_BYTES, ROOT_RECORD_VERSION } from "../messages.js";
|
|
20
|
+
import { decodeVoteMessage, decodeRootRecord, encodeRootRecord, maxBundleMessageBytes, rootFetchKey, MAX_ROOT_MESSAGE_BYTES, ROOT_RECORD_VERSION } from "../messages.js";
|
|
21
21
|
/**
|
|
22
22
|
* Test harness for the two-node gossipsub integration test. It stands up ONE real libp2p +
|
|
23
23
|
* Helia node carrying `@libp2p/gossipsub` (>= 15.0.23, the CVE-2026-46679 floor) and
|
|
@@ -38,9 +38,13 @@ const BLOCKS_PER_BUCKET = 43_200;
|
|
|
38
38
|
const VOTE_EXPIRY_BUCKETS = 30;
|
|
39
39
|
/** A permissive default verifier; individual tests swap it via {@link VoteNode.setVerifier}. */
|
|
40
40
|
const okVerifier = () => ({ valid: true, ruleScore: 1n, resolvedNames: {} });
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* One real loopback libp2p + Helia node carrying gossipsub (topic-scoped score params) and
|
|
43
|
+
* `@libp2p/fetch` — the raw host node with nothing else wired. {@link makeVoteNode} builds the
|
|
44
|
+
* harness transport on top of it; the three-node relay test instead hands it straight to the
|
|
45
|
+
* REAL `PubsubVoter` as the injected host node (the `PubsubVoterOptions.helia` seam).
|
|
46
|
+
*/
|
|
47
|
+
export async function makeBareNode(topic) {
|
|
44
48
|
const libp2p = await createLibp2p({
|
|
45
49
|
addresses: { listen: ["/ip4/127.0.0.1/tcp/0"] },
|
|
46
50
|
transports: [tcp()],
|
|
@@ -87,6 +91,12 @@ export async function makeVoteNode(topic, options = {}) {
|
|
|
87
91
|
}
|
|
88
92
|
});
|
|
89
93
|
const helia = await createHelia({ libp2p });
|
|
94
|
+
return { libp2p, helia };
|
|
95
|
+
}
|
|
96
|
+
/** Build one real node with the full forward-gate wired to real gossipsub. */
|
|
97
|
+
export async function makeVoteNode(topic, options = {}) {
|
|
98
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
99
|
+
const { libp2p, helia } = await makeBareNode(topic);
|
|
90
100
|
const pubsub = libp2p.services.pubsub;
|
|
91
101
|
// Adapt Helia's async-generator `get` to the library's Promise-returning BlockstoreLike, the
|
|
92
102
|
// same bridge `requireHeliaServices` applies to an injected host node.
|
|
@@ -117,16 +127,30 @@ export async function makeVoteNode(topic, options = {}) {
|
|
|
117
127
|
let matchedOwnRoot = false;
|
|
118
128
|
async function checkpointRootRecord() {
|
|
119
129
|
const winners = crdt.current(CURRENT_BUCKET);
|
|
120
|
-
const { root, blocks } = await encodeCheckpoint(winners);
|
|
130
|
+
const { root, chunks, blocks } = await encodeCheckpoint(winners);
|
|
121
131
|
for (const block of blocks)
|
|
122
132
|
await blockstore.put(block.cid, block.bytes);
|
|
123
133
|
return {
|
|
124
134
|
version: ROOT_RECORD_VERSION,
|
|
125
135
|
root,
|
|
136
|
+
chunks,
|
|
126
137
|
count: winners.length,
|
|
127
138
|
sizeBytes: blocks.reduce((total, block) => total + block.bytes.length, 0)
|
|
128
139
|
};
|
|
129
140
|
}
|
|
141
|
+
// Mirror the production fetch responder (voter.ts `#rootLookup`): answer `<topic>/root` with
|
|
142
|
+
// this node's current root record, encoded on demand — the surface a cold joiner pulls
|
|
143
|
+
// before chasing. `@libp2p/fetch` hands the lookup the requested key as raw bytes.
|
|
144
|
+
libp2p.services.fetch.registerLookupFunction(topic, async (keyBytes) => {
|
|
145
|
+
if (new TextDecoder().decode(keyBytes) !== rootFetchKey(topic))
|
|
146
|
+
return undefined;
|
|
147
|
+
try {
|
|
148
|
+
return encodeRootRecord(await checkpointRootRecord());
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
130
154
|
const openedSessions = [];
|
|
131
155
|
const chaseLimit = pLimit(2);
|
|
132
156
|
const chaser = makeRootChaser({
|
|
@@ -228,6 +252,10 @@ export async function makeVoteNode(topic, options = {}) {
|
|
|
228
252
|
verifyImpl = verify;
|
|
229
253
|
},
|
|
230
254
|
checkpointRootRecord,
|
|
255
|
+
fetchRootRecord: async (from) => {
|
|
256
|
+
const bytes = await libp2p.services.fetch.fetch(from.libp2p.peerId, rootFetchKey(topic));
|
|
257
|
+
return bytes == null ? undefined : decodeRootRecord(bytes);
|
|
258
|
+
},
|
|
231
259
|
publishOwnRoot: async () => {
|
|
232
260
|
await transport.publishRootRecord(await checkpointRootRecord());
|
|
233
261
|
},
|