Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions packages/agent/src/dkg-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2993,11 +2993,83 @@
/**
* Register a new M/N signature-gated context graph on-chain.
*/
/**
* Refuse on-chain context-graph registration when the proposed
* configuration cannot satisfy the global
* `ParametersStorage.minimumRequiredSignatures` floor that
* `KnowledgeAssetsV10.publishDirect` enforces at publish time.
*
* Fail-closed semantics: if the adapter exposes
* `getMinimumRequiredSignatures` but the RPC call fails, we throw rather
* than continue. Silently skipping on a transient RPC failure would let
* an operator mint a CG that is never publishable — exactly the bug this
* pre-flight exists to prevent. The previous `try { ... } catch {
* warn-and-continue }` shape reintroduced that footgun.
*
* Skip silently only when the adapter does not implement the getter at
* all — i.e. legacy / pre-V10 adapters or test fixtures that don't
* model the floor. The chain's own validation still applies in that
* case (and the `createOnChainContextGraph` call may still revert on
* its own internal invariants).
*/
private async assertGlobalQuorumOrThrow(
hostingNodeCount: number,
requiredSignatures: number,
contextLabel: string,
): Promise<void> {
if (typeof this.chain.getMinimumRequiredSignatures !== 'function') return;

let globalMin: number;
try {
globalMin = await this.chain.getMinimumRequiredSignatures();
} catch (err) {
throw new Error(
`${contextLabel} cannot be registered on-chain: failed to read ` +
`minimumRequiredSignatures floor from ParametersStorage ` +
`(${(err as Error).message}). Refusing to proceed (fail-closed) — ` +
`creating the CG without verifying the global quorum could leave it ` +
`permanently unpublishable. Retry once chain RPC is healthy.`,
);
}

if (globalMin <= 0) return;

if (hostingNodeCount < globalMin) {
throw new Error(

Check failure on line 3038 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [2/10]

test/e2e-publish-protocol.test.ts > E2E: Publish rejected with insufficient receiver signatures > publish fails gracefully when no peers provide receiver sigs

Error: Context graph "publish-protocol-e2e" cannot be registered on-chain: it has 1 hosting node but the global minimum quorum (ParametersStorage.minimumRequiredSignatures) is 2. Recreate the CG with at least 2 hosting nodes — otherwise every publish would revert with MinSignaturesRequirementNotMet(2, 1). ❯ DKGAgent.assertGlobalQuorumOrThrow src/dkg-agent.ts:3038:13 ❯ DKGAgent.registerContextGraph src/dkg-agent.ts:4271:5 ❯ test/e2e-publish-protocol.test.ts:404:5
`${contextLabel} cannot be registered on-chain: it has ` +
`${hostingNodeCount} hosting node${hostingNodeCount === 1 ? '' : 's'} ` +
`but the global minimum quorum (ParametersStorage.minimumRequiredSignatures) ` +
`is ${globalMin}. Recreate the CG with at least ${globalMin} hosting nodes — ` +
`otherwise every publish would revert with MinSignaturesRequirementNotMet(` +
`${globalMin}, ${hostingNodeCount}).`,
);
}
if (requiredSignatures < globalMin) {
throw new Error(
`${contextLabel} cannot be registered on-chain: requiredSignatures=` +
`${requiredSignatures} is below the global minimum quorum of ${globalMin}. ` +
`Recreate the CG with --required-signatures ${globalMin} (or higher) before registering — ` +
`otherwise every publish would revert with MinSignaturesRequirementNotMet(` +
`${globalMin}, ${requiredSignatures}).`,
);
}
}

async registerContextGraphOnChain(params: CreateOnChainContextGraphParams): Promise<CreateOnChainContextGraphResult> {
const ctx = createOperationContext('system');
if (typeof this.chain.createOnChainContextGraph !== 'function') {
throw new Error('createOnChainContextGraph not available on chain adapter');
}
// Defense-in-depth: also gate this lower-level entry point so the
// daemon's HTTP/API path (`/api/context-graph` → `registerContextGraphOnChain`)
// can't bypass the high-level pre-flight by submitting bad params
// directly. The high-level `registerContextGraph` runs the same check
// earlier in its own flow (with the local `id` for nicer error context).
await this.assertGlobalQuorumOrThrow(
params.participantIdentityIds.length,
params.requiredSignatures,
'Context graph (on-chain registration)',
);
const result = await this.chain.createOnChainContextGraph(params);
const contextGraphId = result.contextGraphId.toString();
for (const identityId of params.participantIdentityIds) {
Expand Down Expand Up @@ -4164,11 +4236,44 @@
);
}
effectiveParticipantIdentityIds = [selfIdentityId];
// Silent fallback to [self] is preserved for backwards-compatible solo
// dev/test flows, but operators on production chains hit this path by
// accident and end up with a CG that's permanently broken (every publish
// reverts with MinSignaturesRequirementNotMet because the global quorum
// floor exceeds 1). Surface the fallback explicitly so log scraping
// catches it, and let the global-quorum pre-flight below decide whether
// to refuse outright.
this.log.warn(
ctx,
`Context graph "${id}" has no recorded participant identity IDs in local _meta — ` +
`falling back to [self=${selfIdentityId}] for on-chain registration. ` +
`If you intended a multi-host CG, recreate it with explicit participants ` +
`before registering (existing on-chain registration will not be reused).`,
);
}

const effectiveRequiredSignatures = Number.isInteger(storedRequiredSignatures) && storedRequiredSignatures > 0
? storedRequiredSignatures
: 1;

// Global-quorum pre-flight. ContextGraphStorage.createContextGraph itself
// only enforces `requiredSignatures <= hostingNodes.length` — it does NOT
// check the global ParametersStorage.minimumRequiredSignatures floor. That
// check happens later, inside KnowledgeAssetsV10.publishDirect. The
// result: a CG can be successfully created on-chain with hostingNodes=1
// and requiredSignatures=1, after which every publish reverts with
// `MinSignaturesRequirementNotMet(globalMin, hostingNodes.length)`.
//
// Validate here AND inside `registerContextGraphOnChain`: this call gives
// operators an early failure with the local `id` for context, the lower
// call closes the bypass for any caller that hits it directly (e.g. the
// daemon's `/api/context-graph` route).
await this.assertGlobalQuorumOrThrow(
effectiveParticipantIdentityIds.length,
effectiveRequiredSignatures,
`Context graph "${id}"`,
);

const participantAgents = await this.getContextGraphParticipantAgentAddresses(id);
if (participantAgents.length > MAX_CONTEXT_GRAPH_PARTICIPANT_AGENTS) {
throw new Error(
Expand Down Expand Up @@ -6702,7 +6807,7 @@
}

get peerId(): string {
return this.node.peerId;

Check failure on line 6810 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows queries with PREFIX declarations

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:6810:22 ❯ DKGAgent.query src/dkg-agent.ts:3302:27 ❯ test/e2e-flows.test.ts:459:28

Check failure on line 6810 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows DESCRIBE queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:6810:22 ❯ DKGAgent.query src/dkg-agent.ts:3302:27 ❯ test/e2e-flows.test.ts:446:28

Check failure on line 6810 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows ASK queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:6810:22 ❯ DKGAgent.query src/dkg-agent.ts:3302:27 ❯ test/e2e-flows.test.ts:433:28

Check failure on line 6810 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows CONSTRUCT queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:6810:22 ❯ DKGAgent.query src/dkg-agent.ts:3302:27 ❯ test/e2e-flows.test.ts:420:28

Check failure on line 6810 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows SELECT queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:6810:22 ❯ DKGAgent.query src/dkg-agent.ts:3302:27 ❯ test/e2e-flows.test.ts:407:28
}

get nodeName(): string {
Expand Down
Loading
Loading