Skip to content

Commit 5038e92

Browse files
authored
Merge pull request #506 from OriginTrail/feat/rfc07-pr5-gossipsub-msgid
feat: Network Relay Registry (Phase 1) + RFC 07 In-Process PeerResolver (PRs #471, #494, #496, #497, #499, #501 bundled)
2 parents 67f6d67 + a4eb9df commit 5038e92

38 files changed

Lines changed: 5336 additions & 249 deletions

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ jobs:
130130
- name: Test audit-create-random lexer
131131
run: node --test scripts/audit-create-random.test.mjs
132132

133+
# RFC 07 §3.2 boundary gate. Bans raw `dialProtocol(` outside the
134+
# network/protocol-router boundary so new protocols can't silently
135+
# bypass the in-process PeerResolver and re-introduce the
136+
# asymmetric-failure class RFC 07 was built to eliminate. See
137+
# `scripts/audit-dial-protocol.mjs` for the full rationale and
138+
# `dkgv10-spec/production_mainnet/07_IN_PROCESS_PEER_RESOLVER.md`
139+
# for the architectural commitment.
140+
- name: Audit dialProtocol boundary (RFC 07 §3.2)
141+
run: node scripts/audit-dial-protocol.mjs
142+
143+
- name: Test audit-dial-protocol lexer
144+
run: node --test scripts/audit-dial-protocol.test.mjs
145+
133146
- name: Install dependencies
134147
run: pnpm install --frozen-lockfile
135148

packages/agent/src/dkg-agent.ts

Lines changed: 241 additions & 85 deletions
Large diffs are not rendered by default.
Lines changed: 13 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
1-
import { multiaddr } from '@multiformats/multiaddr';
21
import type { ProtocolRouter } from '@origintrail-official/dkg-core';
3-
import type { DiscoveryClient } from '../discovery.js';
4-
5-
/**
6-
* Minimal libp2p surface the Messenger needs. Defined locally to keep
7-
* test mocking trivial — production code passes `node.libp2p`.
8-
*/
9-
interface Libp2pLike {
10-
getConnections(peerId?: unknown): Array<unknown>;
11-
peerStore: {
12-
merge(peer: unknown, update: { multiaddrs: unknown[] }): Promise<void>;
13-
};
14-
}
152

163
export interface MessengerDeps {
17-
libp2p: Libp2pLike;
184
router: ProtocolRouter;
19-
discovery: DiscoveryClient;
205
}
216

227
export interface SendOpts {
@@ -26,32 +11,27 @@ export interface SendOpts {
2611
/**
2712
* Single outbound P2P sending primitive.
2813
*
29-
* Two responsibilities:
30-
* 1. Best-effort prime a /p2p-circuit relay route into the libp2p peerStore
31-
* before dialling, so NAT'd peers reachable only via a circuit relay can
32-
* be dialled by `dialProtocol` (which otherwise falls back to direct dial
33-
* and fails for NAT'd peers without an active connection).
34-
* 2. Forward the bytes via `ProtocolRouter.send` (which itself owns
35-
* transport-level retry on recoverable errors — see protocol-router.ts).
14+
* Forwards the bytes to `ProtocolRouter.send`, which (since RFC 07 PR-3)
15+
* consults `PeerResolver` before dialing — populating the libp2p
16+
* peerStore with whatever multiaddrs the resolution order finds
17+
* (live conn → DHT → RFC 04 registry → agents-CG). Centralising every
18+
* outbound send through one entry point removes the "this code path
19+
* forgot to prime the relay route" defect class behind PR #448's
20+
* Laptop B invite failure.
3621
*
37-
* Centralising this in one place removes a class of "this code path forgot to
38-
* call ensureCircuitRelayAddress" defects (the latent bug behind PR #448's
39-
* Laptop B invite failure).
22+
* Note: pre-PR-3 this class held its own `PeerResolver` ref and
23+
* resolved before delegating to the router. After PR-3 the router does
24+
* the same lookup, so doing it here too duplicated the DHT walk on
25+
* every cold send. Codex review on PR #497 caught the duplication;
26+
* the resolver dependency is now owned by `ProtocolRouter` alone.
4027
*
41-
* Discovery is currently SPARQL-first against the agents context graph; this
42-
* is preserved verbatim from the previous DKGAgent.ensureCircuitRelayAddress
43-
* implementation. See dkgv10-spec/production_mainnet/04_NETWORK_RELAY_REGISTRY.md
44-
* for the planned chain-driven replacement.
28+
* See `dkgv10-spec/production_mainnet/07_IN_PROCESS_PEER_RESOLVER.md`.
4529
*/
4630
export class Messenger {
47-
private readonly libp2p: Libp2pLike;
4831
private readonly router: ProtocolRouter;
49-
private readonly discovery: DiscoveryClient;
5032

5133
constructor(deps: MessengerDeps) {
52-
this.libp2p = deps.libp2p;
5334
this.router = deps.router;
54-
this.discovery = deps.discovery;
5535
}
5636

5737
async sendToPeer(
@@ -60,41 +40,6 @@ export class Messenger {
6040
data: Uint8Array,
6141
opts: SendOpts = {},
6242
): Promise<Uint8Array> {
63-
await this.ensureCircuitRelayAddress(peerId);
6443
return this.router.send(peerId, protocolId, data, opts.timeoutMs);
6544
}
66-
67-
/**
68-
* Best-effort: if the peer is not currently connected and the agent
69-
* registry advertises a relay for them, add a /p2p-circuit multiaddr
70-
* to the peer store so dialProtocol can route through the relay.
71-
*
72-
* Failures are swallowed deliberately — the caller's send() will surface
73-
* a proper transport error from dialProtocol if the peer is unreachable.
74-
*
75-
* TODO(follow-up): prefer chain-driven discovery via the network-state
76-
* registry (RFC 04) for freshness; SPARQL profile lookup as last-resort
77-
* fallback.
78-
*/
79-
private async ensureCircuitRelayAddress(peerIdStr: string): Promise<void> {
80-
try {
81-
const { peerIdFromString } = await import('@libp2p/peer-id');
82-
const peerId = peerIdFromString(peerIdStr);
83-
84-
const conns = this.libp2p.getConnections(peerId);
85-
if (conns.length > 0) return;
86-
87-
const agent = await this.discovery.findAgentByPeerId(peerIdStr);
88-
if (!agent?.relayAddress) return;
89-
90-
const circuitAddr = multiaddr(
91-
`${agent.relayAddress}/p2p-circuit/p2p/${peerIdStr}`,
92-
);
93-
await this.libp2p.peerStore.merge(peerId, {
94-
multiaddrs: [circuitAddr],
95-
});
96-
} catch {
97-
// Best-effort — let the caller's send() surface the real error.
98-
}
99-
}
10045
}
Lines changed: 45 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,80 @@
11
import { describe, it, expect, vi } from 'vitest';
22
import { Messenger } from '../src/p2p/messenger.js';
33
import type { ProtocolRouter } from '@origintrail-official/dkg-core';
4-
import type { DiscoveryClient } from '../src/discovery.js';
54

6-
// 12D3Koo... peer ids must be valid base58 to satisfy peerIdFromString. Two
7-
// arbitrary valid ed25519 peer IDs taken from existing tests.
85
const PEER_A = '12D3KooWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
96
const PEER_B = '12D3KooWQz2bQbQueABKRSjV9koF8VYsXk5TdCsUmPf5zAEZg3q6';
10-
const RELAY_ADDR = '/ip4/178.104.54.178/tcp/9090/p2p/12D3KooWSmU3owJvB9sFw8uApDgKrv2VBMecsGGvgAc4Gq6hB57M';
117

128
interface MockSetup {
13-
libp2pConnections: Array<unknown>;
149
routerSendMock: ReturnType<typeof vi.fn>;
15-
peerStoreMergeMock: ReturnType<typeof vi.fn>;
16-
findAgentMock: ReturnType<typeof vi.fn>;
1710
}
1811

19-
function makeMessenger(overrides: Partial<MockSetup> = {}): { messenger: Messenger; mocks: MockSetup; callOrder: string[] } {
20-
const callOrder: string[] = [];
21-
12+
function makeMessenger(overrides: Partial<MockSetup> = {}): {
13+
messenger: Messenger;
14+
mocks: MockSetup;
15+
} {
2216
const mocks: MockSetup = {
23-
libp2pConnections: overrides.libp2pConnections ?? [],
24-
routerSendMock: overrides.routerSendMock ?? vi.fn(async () => {
25-
callOrder.push('router.send');
26-
return new Uint8Array([0x01, 0x02]);
27-
}),
28-
peerStoreMergeMock: overrides.peerStoreMergeMock ?? vi.fn(async () => {
29-
callOrder.push('peerStore.merge');
30-
}),
31-
findAgentMock: overrides.findAgentMock ?? vi.fn(async () => {
32-
callOrder.push('discovery.findAgentByPeerId');
33-
return { peerId: PEER_B, name: 'remote', agentUri: 'urn:agent:remote', relayAddress: RELAY_ADDR };
34-
}),
17+
routerSendMock:
18+
overrides.routerSendMock ??
19+
vi.fn(async () => new Uint8Array([0x01, 0x02])),
3520
};
3621

3722
const messenger = new Messenger({
38-
libp2p: {
39-
getConnections: () => mocks.libp2pConnections,
40-
peerStore: { merge: mocks.peerStoreMergeMock },
41-
},
4223
router: { send: mocks.routerSendMock } as unknown as ProtocolRouter,
43-
discovery: { findAgentByPeerId: mocks.findAgentMock } as unknown as DiscoveryClient,
4424
});
4525

46-
return { messenger, mocks, callOrder };
26+
return { messenger, mocks };
4727
}
4828

4929
describe('Messenger.sendToPeer', () => {
50-
it('skips relay prime when peer is already connected', async () => {
51-
const { messenger, mocks } = makeMessenger({
52-
libp2pConnections: [{ remotePeer: { toString: () => PEER_B } }],
53-
});
54-
55-
await messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff]));
30+
it('delegates to router.send with peerId / protocol / data', async () => {
31+
const { messenger, mocks } = makeMessenger();
5632

57-
expect(mocks.findAgentMock).not.toHaveBeenCalled();
58-
expect(mocks.peerStoreMergeMock).not.toHaveBeenCalled();
59-
expect(mocks.routerSendMock).toHaveBeenCalledOnce();
33+
const out = await messenger.sendToPeer(
34+
PEER_B,
35+
'/dkg/test/1.0.0',
36+
new Uint8Array([0xff]),
37+
);
38+
39+
expect(mocks.routerSendMock).toHaveBeenCalledWith(
40+
PEER_B,
41+
'/dkg/test/1.0.0',
42+
expect.any(Uint8Array),
43+
undefined,
44+
);
45+
expect(out).toEqual(new Uint8Array([0x01, 0x02]));
6046
});
6147

62-
it('primes /p2p-circuit multiaddr when peer not connected and profile advertises a relay', async () => {
48+
it('forwards timeoutMs to router.send', async () => {
6349
const { messenger, mocks } = makeMessenger();
6450

65-
await messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff]));
66-
67-
expect(mocks.findAgentMock).toHaveBeenCalledWith(PEER_B);
68-
expect(mocks.peerStoreMergeMock).toHaveBeenCalledOnce();
69-
const mergeCall = mocks.peerStoreMergeMock.mock.calls[0];
70-
const multiaddrs = (mergeCall[1] as { multiaddrs: Array<{ toString(): string }> }).multiaddrs;
71-
expect(multiaddrs[0].toString()).toContain('/p2p-circuit/p2p/' + PEER_B);
72-
expect(mocks.routerSendMock).toHaveBeenCalledOnce();
73-
});
74-
75-
it('skips relay prime when discovery returns no relayAddress', async () => {
76-
const { messenger, mocks } = makeMessenger({
77-
findAgentMock: vi.fn(async () => ({ peerId: PEER_B, name: 'remote', agentUri: 'urn:agent:remote' })),
51+
await messenger.sendToPeer(PEER_A, '/dkg/test/1.0.0', new Uint8Array([0xff]), {
52+
timeoutMs: 5000,
7853
});
7954

80-
await messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff]));
81-
82-
expect(mocks.findAgentMock).toHaveBeenCalled();
83-
expect(mocks.peerStoreMergeMock).not.toHaveBeenCalled();
84-
expect(mocks.routerSendMock).toHaveBeenCalledOnce();
55+
expect(mocks.routerSendMock).toHaveBeenCalledWith(
56+
PEER_A,
57+
'/dkg/test/1.0.0',
58+
expect.any(Uint8Array),
59+
5000,
60+
);
8561
});
8662

87-
it('tolerates discovery throwing — proceeds to router.send anyway', async () => {
88-
const { messenger, mocks } = makeMessenger({
89-
findAgentMock: vi.fn(async () => { throw new Error('discovery boom'); }),
63+
it('propagates router.send errors to the caller', async () => {
64+
const { messenger } = makeMessenger({
65+
routerSendMock: vi.fn(async () => {
66+
throw new Error('transport boom');
67+
}),
9068
});
9169

92-
await messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff]));
93-
94-
expect(mocks.peerStoreMergeMock).not.toHaveBeenCalled();
95-
expect(mocks.routerSendMock).toHaveBeenCalledOnce();
70+
await expect(
71+
messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff])),
72+
).rejects.toThrow('transport boom');
9673
});
9774

98-
it('regression: ensureCircuitRelayAddress fires BEFORE router.send (the Laptop B bug)', async () => {
99-
const { messenger, callOrder } = makeMessenger();
100-
101-
await messenger.sendToPeer(PEER_B, '/dkg/test/1.0.0', new Uint8Array([0xff]));
102-
103-
// The bug fixed by this primitive: forwardJoinRequest used to call
104-
// router.send directly without first priming the relay route, causing
105-
// "no reachable curator" failures for NAT'd peers. The fix is structural
106-
// — every send through Messenger primes first.
107-
const mergeIdx = callOrder.indexOf('peerStore.merge');
108-
const sendIdx = callOrder.indexOf('router.send');
109-
expect(mergeIdx).toBeGreaterThanOrEqual(0);
110-
expect(sendIdx).toBeGreaterThanOrEqual(0);
111-
expect(mergeIdx).toBeLessThan(sendIdx);
112-
});
113-
114-
it('forwards timeoutMs to router.send', async () => {
115-
const { messenger, mocks } = makeMessenger({
116-
libp2pConnections: [{ remotePeer: { toString: () => PEER_B } }],
117-
});
118-
119-
await messenger.sendToPeer(PEER_A, '/dkg/test/1.0.0', new Uint8Array([0xff]), { timeoutMs: 5000 });
120-
121-
expect(mocks.routerSendMock).toHaveBeenCalledWith(PEER_A, '/dkg/test/1.0.0', expect.any(Uint8Array), 5000);
122-
});
75+
// Note: Messenger no longer holds a PeerResolver — the resolver is
76+
// owned by ProtocolRouter (RFC 07 PR-3) so resolution happens once
77+
// per send rather than twice. The structural property "resolver
78+
// primes peerStore before dialProtocol" still holds; it's just
79+
// verified at the router layer now (see protocol-router-resolver.test.ts).
12380
});

0 commit comments

Comments
 (0)