Skip to content

Commit 269e8a2

Browse files
Branimir Rakiccursoragent
authored andcommitted
fix: Codex PR #595 round-4 — sharding-table eligibility, fail-closed VerifyCollector, reject deprecated fields
Three legitimate findings from round-4 review addressed in this commit. (D) packages/cli/src/daemon/routes/context-graph.ts The previous round-2 fix tried to preserve backward compatibility by stripping `participantIdentityIds` / `requiredSignatures` from request bodies and warning. Round-4 (and the project owner) made clear that silent stripping lets callers believe they created a roster-constrained CG when those constraints were actually discarded. We now reject outright any request body that carries either deprecated field — code `DEPRECATED_CONTEXT_GRAPH_FIELDS`, structured `deprecatedFields` array, HTTP 400. No backward-compat strip path. (C) packages/publisher/src/verify-collector.ts `VerifyCollector.collect` used to silently default `requiredSignatures` to 1 when the caller omitted it and the `getMinimumRequiredSignatures` probe failed (missing probe / RPC outage / garbage value). Since `chain.verify()` does NOT submit collected signatures on-chain, the local quorum check is the only enforcement; defaulting to 1 lets the proposer self-approve and pass. Now all three failure modes throw with actionable errors pointing at the explicit `requiredSignatures` override knob. 4 new unit tests pin the behaviour. (B) packages/chain/src/{chain-adapter,evm-adapter,mock-adapter}.ts + packages/agent/src/dkg-agent.ts SPEC_CG_MEMORY_MODEL §4.3 promises that only sharding-table members can ACK a VM publish, but the post-LU2 verify path counted every resolved signer regardless of membership. Added an optional `isShardingTableMember(identityId)` to the ChainAdapter interface, wired EVMChainAdapter to `ShardingTableStorage.nodeExists(uint72)`, and made the mock return true for any non-zero identityId (mocks don't model the sharding table). The agent verify path now probes membership for every resolved signer (including the proposer) and drops approvals from non-members with a fail-closed per-signer log. Results are cached per batch to avoid hammering the RPC. Tests: - cli/test/daemon-http-behavior-extra.test.ts: replace round-2 strip- tolerance tests with reject tests covering every deprecated-field combination (with and without id/name). - publisher/test/verify-collector.test.ts: four new tests covering missing-probe / probe-throws / probe-returns-garbage / probe-honoured paths. - chain/test/mock-adapter-parity.test.ts: extend the parity manifest + add a direct positive/negative assertion for the new method. All targeted suites pass locally: ✓ publisher/verify-collector (10/10) ✓ chain/mock-adapter-parity (13/13) ✓ chain/abi-pinning (13/13) ✓ cli/daemon-http-behavior-extra (5/5 in-scope subset) ✓ agent/{e2e-publish-protocol, e2e-context-graph, v10-ack-provider, swm-ack-quorum, swm-ack-quorum-integration} (73/73) Holds the line on round-4 Bug A (`KnowledgeAssetsLib.sol:71` packed- struct layout): ContextGraphStorage is not behind an upgradeable proxy; the deploy pattern is "deploy fresh + register in Hub by name". File itself states `// Storage layout — fresh design (no prior deployments to preserve)`. V10 is brand-new on-chain (only testnet artifact: base_sepolia_v10_contracts.json). No storage-slot reservation needed. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8f75c6d commit 269e8a2

9 files changed

Lines changed: 232 additions & 72 deletions

File tree

packages/agent/src/dkg-agent.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11806,9 +11806,47 @@ export class DKGAgent {
1180611806
const isEligibleParticipant = (identityId: bigint): boolean =>
1180711807
participantIdentityIds === null || participantIdentityIds.has(identityId);
1180811808

11809+
// Sharding-table eligibility (Codex PR #595 round-4):
11810+
// SPEC_CG_MEMORY_MODEL §4.3 + §6 promise that only sharding-table
11811+
// members can ACK a VM publish. Pre-LU2 the per-CG hosting-committee
11812+
// allowlist did this gating implicitly; post-LU2 that allowlist is
11813+
// gone and we have to check membership at signer-resolution time.
11814+
// Failure modes:
11815+
// - adapter doesn't implement `isShardingTableMember` → log once
11816+
// and fall back to legacy behavior (test mocks / no-chain envs).
11817+
// - membership probe throws for a specific signer → drop that
11818+
// approval (fail-closed per signer, not per batch).
11819+
// Cache decisions per batch so we don't hammer the RPC for repeated
11820+
// approvers across re-tries.
11821+
const shardingMembershipCache = new Map<string, boolean>();
11822+
const probeShardingTableMembership = async (identityId: bigint): Promise<boolean> => {
11823+
if (identityId <= 0n) return false;
11824+
if (typeof this.chain.isShardingTableMember !== 'function') return true;
11825+
const key = identityId.toString();
11826+
const cached = shardingMembershipCache.get(key);
11827+
if (cached !== undefined) return cached;
11828+
try {
11829+
const ok = await this.chain.isShardingTableMember(identityId);
11830+
shardingMembershipCache.set(key, ok);
11831+
return ok;
11832+
} catch (err: any) {
11833+
this.log.warn(
11834+
ctx,
11835+
`[verify] isShardingTableMember(${identityId}) probe failed (${err?.message ?? err}); ` +
11836+
`dropping that signer's approval as fail-closed`,
11837+
);
11838+
shardingMembershipCache.set(key, false);
11839+
return false;
11840+
}
11841+
};
11842+
1180911843
const resolvedSignatures: Array<{ identityId: bigint; r: Uint8Array; vs: Uint8Array }> = [];
1181011844
const resolvedSignerAddresses: string[] = [];
11811-
if (this.identityId > 0n && isEligibleParticipant(this.identityId)) {
11845+
if (
11846+
this.identityId > 0n
11847+
&& isEligibleParticipant(this.identityId)
11848+
&& await probeShardingTableMembership(this.identityId)
11849+
) {
1181211850
resolvedSignatures.push({
1181311851
identityId: this.identityId,
1181411852
r: ethers.getBytes(proposerSig.r),
@@ -11824,6 +11862,7 @@ export class DKGAgent {
1182411862
);
1182511863
if (!id || id === 0n) continue;
1182611864
if (!isEligibleParticipant(id)) continue;
11865+
if (!(await probeShardingTableMembership(id))) continue;
1182711866
resolvedSignatures.push({ identityId: id, r: a.signatureR, vs: a.signatureVS });
1182811867
resolvedSignerAddresses.push(a.approverAddress);
1182911868
if (participantIdentityIds !== null && participantIdentityIds.has(id)) {

packages/chain/src/chain-adapter.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,20 @@ export interface ChainAdapter {
725725
/** Read minimumRequiredSignatures from ParametersStorage. Used by ACKCollector. */
726726
getMinimumRequiredSignatures?(): Promise<number>;
727727

728+
/**
729+
* Sharding-table membership check (Codex PR #595 round-4 follow-up).
730+
*
731+
* SPEC_CG_MEMORY_MODEL §4.3 promises that only sharding-table members
732+
* can ACK a VM publish. The agent's verify path calls this on every
733+
* resolved signer identityId and DROPS any approval whose signer is
734+
* not in the sharding table. Adapters that can't probe membership
735+
* (test mocks, legacy in-tree fakes) return `true` for any non-zero
736+
* identityId — those environments aren't security-sensitive.
737+
*
738+
* Implementation maps to `ShardingTableStorage.nodeExists(uint72)`.
739+
*/
740+
isShardingTableMember?(identityId: bigint): Promise<boolean>;
741+
728742
/** Verify that a recovered signer address is a registered operational key for the given identity. */
729743
verifyACKIdentity?(recoveredAddress: string, claimedIdentityId: bigint): Promise<boolean>;
730744

packages/chain/src/evm-adapter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2315,6 +2315,19 @@ export class EVMChainAdapter implements ChainAdapter {
23152315
return Number(await this.contracts.parametersStorage.minimumRequiredSignatures());
23162316
}
23172317

2318+
async isShardingTableMember(identityId: bigint): Promise<boolean> {
2319+
if (identityId <= 0n) return false;
2320+
await this.init();
2321+
const storage = await this.resolveContract('ShardingTableStorage');
2322+
if (!storage) {
2323+
throw new Error(
2324+
'isShardingTableMember: ShardingTableStorage contract is not resolvable. ' +
2325+
'Verify path cannot enforce sharding-table eligibility without it.',
2326+
);
2327+
}
2328+
return Boolean(await storage.nodeExists(identityId));
2329+
}
2330+
23182331
async verifyACKIdentity(recoveredAddress: string, claimedIdentityId: bigint): Promise<boolean> {
23192332
await this.init();
23202333
const identityStorage = await this.resolveContract('IdentityStorage');

packages/chain/src/mock-adapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,14 @@ export class MockChainAdapter implements ChainAdapter {
760760
return this.minimumRequiredSignatures;
761761
}
762762

763+
// Codex PR #595 round-4: mock environments don't model the sharding
764+
// table, so any registered (non-zero) identity counts as a member.
765+
// Tests that need to exercise non-membership rejection should
766+
// override this with a vi.spyOn / monkey-patch.
767+
async isShardingTableMember(identityId: bigint): Promise<boolean> {
768+
return identityId > 0n;
769+
}
770+
763771
async verifyACKIdentity(recoveredAddress: string, claimedIdentityId: bigint): Promise<boolean> {
764772
// Strict binding: recovered address must match the identity's registered address
765773
const normalizedAddress = recoveredAddress.toLowerCase();

packages/chain/test/mock-adapter-parity.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const NO_CHAIN_EXEMPT_FROM_EVM = new Set<string>([
151151
'signACKDigest',
152152
'getACKSignerKey',
153153
'getMinimumRequiredSignatures',
154+
'isShardingTableMember',
154155
'verifyACKIdentity',
155156
'verifySyncIdentity',
156157
'ensureOperationalWalletsRegistered',
@@ -218,6 +219,17 @@ describe('MockChainAdapter API parity with EVMChainAdapter [CH-8]', () => {
218219
expect(mock.isV10Ready()).toBe(true);
219220
});
220221

222+
// Codex PR #595 round-4: isShardingTableMember gates VM ACK eligibility.
223+
// The mock can't model a real sharding table, so it treats every
224+
// registered (non-zero) identity as a member; tests needing
225+
// non-membership behaviour spy/monkey-patch the method.
226+
it('isShardingTableMember returns true for any non-zero identityId and false for 0n', async () => {
227+
const mock = new MockChainAdapter();
228+
expect(await mock.isShardingTableMember(0n)).toBe(false);
229+
expect(await mock.isShardingTableMember(1n)).toBe(true);
230+
expect(await mock.isShardingTableMember(99999n)).toBe(true);
231+
});
232+
221233
it('getEvmChainId returns a bigint (not a namespaced string like "mock:31337")', async () => {
222234
const mock = new MockChainAdapter();
223235
const id = await mock.getEvmChainId();

packages/cli/src/daemon/routes/context-graph.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -424,39 +424,29 @@ export async function handleContextGraphRoutes(ctx: RequestContext): Promise<voi
424424

425425

426426
// POST /api/context-graph/create — context graph definition create.
427-
// LU-2: per SPEC_CG_MEMORY_MODEL the legacy multisig-creation branch
428-
// (driven by a `participantIdentityIds`-only body, no id/name) is gone.
429-
// CGs no longer carry per-CG hosting committees or quorum overrides;
430-
// hosts are picked from the network sharding table at publish time
431-
// and the ACK quorum is `parametersStorage.minimumRequiredSignatures()`.
432-
//
433-
// Two client groups exist for these deprecated fields:
434-
// (a) modern clients that send the full `{ id, name, participantIdentityIds, requiredSignatures }`
435-
// body — we strip the dead fields and continue.
436-
// (b) legacy multisig-only clients that send `{ participantIdentityIds, requiredSignatures }`
437-
// with no `id`/`name` — there is no automatic translation that
438-
// preserves caller intent (we can't synthesise a meaningful slug),
439-
// so we fail with an explicit 410-style deprecation error pointing
440-
// at the new combined flow instead of letting the request fall
441-
// through to a generic "Missing id or name" 400.
427+
// SPEC_CG_MEMORY_MODEL / Codex PR #595 round-4: per-CG hosting
428+
// committees and per-CG quorum overrides were removed end-to-end.
429+
// The on-chain contract no longer accepts those args, so silently
430+
// stripping `participantIdentityIds` / `requiredSignatures` from the
431+
// request body would let callers believe they created an M-of-N /
432+
// roster-constrained CG when those constraints were actually
433+
// discarded. We reject any body that still carries either field with
434+
// a structured 400 + machine-readable `code`, forcing callers to
435+
// migrate. The on-chain semantics those fields requested no longer
436+
// exist; there is no faithful translation.
442437
if (req.method === "POST" && path === "/api/context-graph/create") {
443438
const body = await readBody(req, SMALL_BODY_BYTES);
444439
const parsed = JSON.parse(body);
445440
if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) {
446-
const isLegacyMultisigOnly =
447-
typeof parsed.id !== 'string' && typeof parsed.name !== 'string';
448-
if (isLegacyMultisigOnly) {
449-
return jsonResponse(res, 400, {
450-
error:
451-
'The multisig-only POST /api/context-graph/create flow (participantIdentityIds without id/name) was removed in SPEC_CG_MEMORY_MODEL. Per-CG hosting committees and per-CG quorum overrides no longer exist. Send a regular `{ id, name, accessPolicy?, publishPolicy? }` body and (if you want chain registration) follow up with POST /api/context-graph/register.',
452-
code: 'DEPRECATED_MULTISIG_CREATE_FLOW',
453-
});
454-
}
455-
console.warn(
456-
'[DKG-Daemon] WARN: POST /api/context-graph/create — `participantIdentityIds` and `requiredSignatures` are deprecated and ignored; per-CG hosting committees and quorums were removed in SPEC_CG_MEMORY_MODEL.',
457-
);
458-
delete parsed.participantIdentityIds;
459-
delete parsed.requiredSignatures;
441+
return jsonResponse(res, 400, {
442+
error:
443+
'`participantIdentityIds` and `requiredSignatures` were removed in SPEC_CG_MEMORY_MODEL. Per-CG hosting committees and per-CG quorum overrides no longer exist on-chain — every CG uses the system-wide ACK quorum (parametersStorage.minimumRequiredSignatures()) and the network sharding table for hosting. Remove these fields from the request body and use `{ id, name, accessPolicy?, publishPolicy?, allowedAgents? }` instead.',
444+
code: 'DEPRECATED_CONTEXT_GRAPH_FIELDS',
445+
deprecatedFields: [
446+
...(parsed.participantIdentityIds !== undefined ? ['participantIdentityIds'] : []),
447+
...(parsed.requiredSignatures !== undefined ? ['requiredSignatures'] : []),
448+
],
449+
});
460450
}
461451
// Body has `id` + `name` → context-graph-style context graph definition create (handled below)
462452
const { id, name, description, allowedAgents, allowedPeers, participantAgents, publishPolicy, accessPolicy, register } = parsed;

packages/cli/test/daemon-http-behavior-extra.test.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -725,37 +725,41 @@ describe('CLI-7 — SPARQL endpoint 4xx matrix', () => {
725725
expect(body.error).not.toMatch(/^0x[0-9a-fA-F]+$/);
726726
});
727727

728-
// SPEC_CG_MEMORY_MODEL / Codex PR #595 round-2: per-CG hosting
729-
// committees and per-CG quorum overrides were removed. Two paths
730-
// exist for legacy clients:
731-
// (a) modern `{ id, name, ...deprecated }` body — the daemon
732-
// strips the dead fields and the request succeeds.
733-
// (b) legacy multisig-only `{ participantIdentityIds, requiredSignatures }`
734-
// body with no `id`/`name` — pre-LU2 there was a dedicated
735-
// branch that bypassed id/name; post-LU2 we MUST fail with an
736-
// explicit deprecation error pointing at the new combined flow
737-
// rather than silently falling through to the generic
738-
// "Missing id or name" 400 (which doesn't tell the caller
739-
// their request shape was retired).
740-
it('strips deprecated participantIdentityIds/requiredSignatures from a modern body and still creates the CG', async () => {
741-
const d = daemon!;
742-
const cgId = 'depr-fields-modern-' + Math.random().toString(36).slice(2, 8);
743-
const res = await fetch(urlFor(d, '/api/context-graph/create'), {
744-
method: 'POST',
745-
headers: { 'Content-Type': 'application/json', ...authHeaders(d) },
746-
body: JSON.stringify({
747-
id: cgId,
748-
name: cgId,
749-
participantIdentityIds: ['1', '2', '3'],
750-
requiredSignatures: 2,
751-
}),
728+
// SPEC_CG_MEMORY_MODEL / Codex PR #595 round-4: per-CG hosting
729+
// committees and per-CG quorum overrides were removed end-to-end.
730+
// The on-chain contract no longer accepts those args, so silently
731+
// stripping them from the request body would let callers believe
732+
// they created a roster-constrained / M-of-N CG when those
733+
// constraints were actually discarded. We reject any body that
734+
// carries either field, regardless of whether `id`/`name` are also
735+
// present — there is no faithful translation.
736+
for (const fields of [
737+
{ participantIdentityIds: ['1', '2'], requiredSignatures: 1 },
738+
{ participantIdentityIds: ['1', '2'] },
739+
{ requiredSignatures: 1 },
740+
] as const) {
741+
const presentKeys = Object.keys(fields).sort().join('+');
742+
it(`rejects POST /api/context-graph/create with deprecated fields (${presentKeys}) — even alongside valid id/name`, async () => {
743+
const d = daemon!;
744+
const cgId = 'depr-reject-' + Math.random().toString(36).slice(2, 8);
745+
const res = await fetch(urlFor(d, '/api/context-graph/create'), {
746+
method: 'POST',
747+
headers: { 'Content-Type': 'application/json', ...authHeaders(d) },
748+
body: JSON.stringify({ id: cgId, name: cgId, ...fields }),
749+
});
750+
expect(res.status).toBe(400);
751+
const body = (await res.json()) as {
752+
error?: string;
753+
code?: string;
754+
deprecatedFields?: string[];
755+
};
756+
expect(body.code).toBe('DEPRECATED_CONTEXT_GRAPH_FIELDS');
757+
expect(body.error ?? '').toMatch(/SPEC_CG_MEMORY_MODEL/);
758+
expect(body.deprecatedFields).toEqual(Object.keys(fields).sort());
752759
});
753-
expect([200, 201]).toContain(res.status);
754-
const body = (await res.json()) as { created?: string };
755-
expect(body.created).toBe(cgId);
756-
});
760+
}
757761

758-
it('rejects legacy multisig-only POST /api/context-graph/create (no id/name) with an explicit deprecation 400', async () => {
762+
it('rejects POST /api/context-graph/create with deprecated fields (no id/name) — naming the missing fields explicitly', async () => {
759763
const d = daemon!;
760764
const res = await fetch(urlFor(d, '/api/context-graph/create'), {
761765
method: 'POST',
@@ -767,9 +771,7 @@ describe('CLI-7 — SPARQL endpoint 4xx matrix', () => {
767771
});
768772
expect(res.status).toBe(400);
769773
const body = (await res.json()) as { error?: string; code?: string };
770-
expect(body.code).toBe('DEPRECATED_MULTISIG_CREATE_FLOW');
771-
expect(body.error ?? '').toMatch(/SPEC_CG_MEMORY_MODEL/);
772-
expect(body.error ?? '').toMatch(/id.*name/);
774+
expect(body.code).toBe('DEPRECATED_CONTEXT_GRAPH_FIELDS');
773775
});
774776
});
775777

packages/publisher/src/verify-collector.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,37 @@ export class VerifyCollector {
9999
batchId, merkleRoot, entities, proposerSignature,
100100
timeoutMs, allowPartial = false,
101101
} = params;
102+
// FAIL-CLOSED (Codex PR #595 round-4): a caller that omits an
103+
// explicit `requiredSignatures` MUST get the system-parameter
104+
// quorum. Defaulting to 1 on lookup failure would let the
105+
// proposer self-approve and pass quorum, and `chain.verify()`
106+
// doesn't re-check signatures on-chain, so this local count is
107+
// the only enforcement gate. If we can't determine the system
108+
// minimum (no probe wired, RPC fails, garbage value), refuse to
109+
// proceed.
102110
let requiredSignatures = params.requiredSignatures ?? 0;
103-
if (requiredSignatures <= 0 && this.deps.getMinimumRequiredSignatures) {
111+
if (requiredSignatures <= 0) {
112+
if (!this.deps.getMinimumRequiredSignatures) {
113+
throw new Error(
114+
'VerifyCollector: requiredSignatures was omitted and no `getMinimumRequiredSignatures` probe was wired. ' +
115+
'Pass `params.requiredSignatures` explicitly or supply a probe at construction.',
116+
);
117+
}
118+
let sysMin: number;
104119
try {
105-
const sysMin = await this.deps.getMinimumRequiredSignatures();
106-
if (Number.isInteger(sysMin) && sysMin >= 1) {
107-
requiredSignatures = sysMin;
108-
}
109-
} catch {
110-
// Fall through to the 1-of-1 default below.
120+
sysMin = await this.deps.getMinimumRequiredSignatures();
121+
} catch (err: any) {
122+
throw new Error(
123+
`VerifyCollector: getMinimumRequiredSignatures() failed (${err?.message ?? err}). ` +
124+
`Pass params.requiredSignatures explicitly or fix the probe.`,
125+
);
111126
}
112-
}
113-
if (requiredSignatures <= 0) {
114-
requiredSignatures = 1;
127+
if (!Number.isInteger(sysMin) || sysMin < 1) {
128+
throw new Error(
129+
`VerifyCollector: getMinimumRequiredSignatures() returned invalid value ${sysMin} (must be a positive integer).`,
130+
);
131+
}
132+
requiredSignatures = sysMin;
115133
}
116134
const boundedTimeoutMs = Math.min(
117135
assertVerifyCollectionTimeoutMs(timeoutMs),

packages/publisher/test/verify-collector.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,68 @@ describe('VerifyCollector', () => {
190190

191191
expect(vi.getTimerCount()).toBe(0);
192192
});
193+
194+
// Codex PR #595 round-4: a caller that omits `requiredSignatures` MUST
195+
// get the system minimum, never a silent default of 1. `chain.verify()`
196+
// does not revalidate signatures on-chain, so this local count is the
197+
// only enforcement gate — defaulting to 1 would let the proposer
198+
// self-approve and pass quorum on a single signature.
199+
describe('fail-closed when requiredSignatures is omitted', () => {
200+
const baseCollectArgs = {
201+
contextGraphId: 'test-cg',
202+
contextGraphIdOnChain: 42n,
203+
verifiedMemoryId: 1n,
204+
batchId: 100n,
205+
merkleRoot: new Uint8Array(32),
206+
entities: [],
207+
proposerSignature: { r: new Uint8Array(32), vs: new Uint8Array(32) },
208+
timeoutMs: 1000,
209+
} as const;
210+
211+
it('throws when no `getMinimumRequiredSignatures` probe is wired', async () => {
212+
const collector = new VerifyCollector({
213+
sendP2P: async () => new Uint8Array(0),
214+
getParticipantPeers: () => ['peer-a'],
215+
// no getMinimumRequiredSignatures
216+
});
217+
await expect(collector.collect(baseCollectArgs)).rejects.toThrow(
218+
/requiredSignatures was omitted and no `getMinimumRequiredSignatures` probe/,
219+
);
220+
});
221+
222+
it('throws when the probe rejects (RPC outage)', async () => {
223+
const collector = new VerifyCollector({
224+
sendP2P: async () => new Uint8Array(0),
225+
getParticipantPeers: () => ['peer-a'],
226+
getMinimumRequiredSignatures: async () => { throw new Error('RPC down'); },
227+
});
228+
await expect(collector.collect(baseCollectArgs)).rejects.toThrow(
229+
/getMinimumRequiredSignatures\(\) failed.*RPC down/,
230+
);
231+
});
232+
233+
it('throws when the probe returns garbage (non-positive integer)', async () => {
234+
const collector = new VerifyCollector({
235+
sendP2P: async () => new Uint8Array(0),
236+
getParticipantPeers: () => ['peer-a'],
237+
getMinimumRequiredSignatures: async () => 0,
238+
});
239+
await expect(collector.collect(baseCollectArgs)).rejects.toThrow(
240+
/returned invalid value 0/,
241+
);
242+
});
243+
244+
it('honours the system minimum when the probe is wired correctly', async () => {
245+
const collector = new VerifyCollector({
246+
sendP2P: async () => new Uint8Array(0),
247+
getParticipantPeers: () => [],
248+
getMinimumRequiredSignatures: async () => 3,
249+
});
250+
// remoteRequired = 3 - 1 = 2 > 0 peers → verify_no_peers (proves
251+
// the probe value was used, not the silent fallback of 1).
252+
await expect(collector.collect(baseCollectArgs)).rejects.toThrow(
253+
/verify_no_peers/,
254+
);
255+
});
256+
});
193257
});

0 commit comments

Comments
 (0)