@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<RootRecord>;
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
- /** Build one real node with the full forward-gate wired to real gossipsub. */
42
- export async function makeVoteNode(topic, options = {}) {
43
- const timeoutMs = options.timeoutMs ?? 10_000;
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/pubsub-voting",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Trustless pubsub voting over a shared libp2p/Helia node.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",