Skip to content
Merged
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
22 changes: 13 additions & 9 deletions packages/agent/src/dkg-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1967,12 +1967,16 @@ export class DKGAgent {
* Registers it in the CG's `_meta` graph and creates the named graph in storage.
* Sub-graphs use convention-based URI partitioning — no on-chain enforcement in V10.0.
*
* V10.0 limitations:
* - Registration triples are stored locally only. Peers discover sub-graphs when
* they receive data via GossipSub or by querying the admin's node.
* - GossipSub broadcasts raw triples without sub-graph context; receivers store
* replicated data in the root data graph. The CG admin manages sub-graph
* organization on their own node.
* V10.0 replication behavior:
* - Registration triples are stored locally by the admin. Peers also auto-register
* sub-graphs on gossip publish, SWM write, and finalization replay paths:
* `gossip-publish-handler.ts`, `workspace-handler.ts`, and
* `finalization-handler.ts` call `ensureSubGraph()` and backfill the full
* `_meta` registration when it is missing.
* - Because `subGraphName` is carried on the wire (in the workspace publish request
* and the N-Quads' named-graph field), replicated data is routed into the correct
* sub-graph named graph on receiving nodes — not into the root data graph.
* - On-chain contracts are unaware of sub-graphs; enforcement remains convention-based.
*/
async createSubGraph(contextGraphId: string, subGraphName: string, opts?: {
description?: string;
Expand Down Expand Up @@ -2035,10 +2039,10 @@ export class DKGAgent {
if (result.type !== 'bindings') return [];
return result.bindings.map(row => ({
uri: row['subGraph'] ?? '',
name: (row['name'] ?? '').replace(/^"|"$/g, ''),
name: stripLiteral(row['name'] ?? ''),
createdBy: row['createdBy'] ?? '',
createdAt: row['createdAt']?.replace(/^"|".*$/g, '') || undefined,
description: row['description']?.replace(/^"|"$/g, '') || undefined,
createdAt: row['createdAt'] ? stripLiteral(row['createdAt']) : undefined,
description: row['description'] ? stripLiteral(row['description']) : undefined,
}));
}

Expand Down
19 changes: 19 additions & 0 deletions packages/agent/src/finalization-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { type ChainAdapter, type EventFilter } from '@origintrail-official/dkg-c
import {
computeFlatKCRootV10 as computeFlatKCRoot, autoPartition,
generateConfirmedFullMetadata, getTentativeStatusQuad,
generateSubGraphRegistration,
type KCMetadata, type KAMetadata, type OnChainProvenance,
} from '@origintrail-official/dkg-publisher';
const DKG_NS = 'http://dkg.io/ontology/';
Expand Down Expand Up @@ -326,6 +327,24 @@ export class FinalizationHandler {
await graphManager.ensureParanet(contextGraphId);
if (subGraphName) {
await graphManager.ensureSubGraph(contextGraphId, subGraphName);
const sgUri = contextGraphSubGraphUri(contextGraphId, subGraphName);
const metaGraph = `did:dkg:context-graph:${assertSafeIri(contextGraphId)}/_meta`;
const alreadyRegistered = await this.store.query(
`ASK { GRAPH <${metaGraph}> {
<${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> ;
<http://schema.org/name> ${JSON.stringify(subGraphName)} ;
<http://dkg.io/ontology/createdBy> ?createdBy .
} }`,
);
if (alreadyRegistered.type !== 'boolean' || !alreadyRegistered.value) {
await this.store.insert(generateSubGraphRegistration({
contextGraphId,
subGraphName,
createdBy: publisherAddress || 'finalization-discovery',
timestamp: new Date(),
}));
this.log.info(ctx, `Finalization: auto-registered sub-graph "${subGraphName}" in context graph "${contextGraphId}"`);
}
}
const dataGraph = subGraphName
? contextGraphSubGraphUri(contextGraphId, subGraphName)
Expand Down
6 changes: 5 additions & 1 deletion packages/agent/src/gossip-publish-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ export class GossipPublishHandler {
const sgUri = contextGraphSubGraphUri(request.paranetId, subGraphName);
const metaGraph = `did:dkg:context-graph:${assertSafeIri(request.paranetId)}/_meta`;
const alreadyRegistered = await this.store.query(
`ASK { GRAPH <${metaGraph}> { <${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> } }`,
`ASK { GRAPH <${metaGraph}> {
<${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> ;
<http://schema.org/name> ${JSON.stringify(subGraphName)} ;
<http://dkg.io/ontology/createdBy> ?createdBy .
} }`,
);
if (alreadyRegistered.type !== 'boolean' || !alreadyRegistered.value) {
const regQuads = generateSubGraphRegistration({
Expand Down
42 changes: 41 additions & 1 deletion packages/agent/test/finalization-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { OxigraphStore } from '@origintrail-official/dkg-storage';
import { encodeFinalizationMessage, type FinalizationMessageMsg, encodePublishRequest } from '@origintrail-official/dkg-core';
import { encodeFinalizationMessage, type FinalizationMessageMsg, encodePublishRequest, createOperationContext } from '@origintrail-official/dkg-core';
import { FinalizationHandler } from '../src/finalization-handler.js';

const PARANET = 'test-paranet';
Expand Down Expand Up @@ -142,4 +142,44 @@ describe('FinalizationHandler', () => {
expect(result.type).toBe('boolean');
if (result.type === 'boolean') expect(result.value).toBe(false);
});

it('backfills full sub-graph registration metadata during finalization promotion', async () => {
const entity = 'urn:test:entity';
const subGraphName = 'code';
const publisherAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const metaGraph = `did:dkg:context-graph:${PARANET}/_meta`;
const subGraphUri = `did:dkg:context-graph:${PARANET}/${subGraphName}`;

await (handler as any).promoteSharedMemoryToCanonical(
PARANET,
[{ subject: entity, predicate: 'http://schema.org/name', object: '"Alice"', graph: '' }],
'did:dkg:evm:31337/0xABC/1',
[entity],
publisherAddress,
'0x' + 'ab'.repeat(32),
100,
1n,
1n,
1n,
createOperationContext('system'),
undefined,
subGraphName,
);

const registration = await store.query(
`ASK { GRAPH <${metaGraph}> {
<${subGraphUri}> a <http://dkg.io/ontology/SubGraph> ;
<http://schema.org/name> "code" ;
<http://dkg.io/ontology/createdBy> <did:dkg:agent:${publisherAddress}> .
} }`,
);
expect(registration.type).toBe('boolean');
if (registration.type === 'boolean') expect(registration.value).toBe(true);

const canonical = await store.query(
`ASK { GRAPH <${subGraphUri}> { <${entity}> <http://schema.org/name> ?o } }`,
);
expect(canonical.type).toBe('boolean');
if (canonical.type === 'boolean') expect(canonical.value).toBe(true);
});
});
47 changes: 40 additions & 7 deletions packages/publisher/src/dkg-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ export class DKGPublisher implements Publisher {
/**
* Read quads from the context graph's shared memory and publish them with full finality (data graph + chain).
* Selection: 'all' or { rootEntities: string[] } to publish only those root entities from shared memory.
*
* @throws Error if `options.subGraphName` is combined with `options.publishContextGraphId`.
* The remap-on-publish flow targets `/context/{id}` URIs, which are incompatible with
* sub-graph URIs of shape `/{contextGraphId}/{subGraphName}`. To publish from a sub-graph,
* omit `publishContextGraphId` (publish remains in the source CG's sub-graph).
*/
async publishFromSharedMemory(
contextGraphId: string,
Expand Down Expand Up @@ -712,10 +717,7 @@ export class DKGPublisher implements Publisher {
if (!sgValidation.valid) throw new Error(`Invalid sub-graph name: ${sgValidation.reason}`);

const sgUri = contextGraphSubGraphUri(options.contextGraphId, options.subGraphName);
const registered = await this.store.query(
`ASK { GRAPH <did:dkg:context-graph:${assertSafeIri(options.contextGraphId)}/_meta> { <${assertSafeIri(sgUri)}> ?p ?o } }`,
);
if (registered.type === 'boolean' && !registered.value) {
if (!(await this.isSubGraphRegistered(options.contextGraphId, options.subGraphName))) {
throw new Error(
`Sub-graph "${options.subGraphName}" has not been registered in context graph "${options.contextGraphId}". ` +
`Call createSubGraph() first.`,
Expand Down Expand Up @@ -1469,14 +1471,45 @@ export class DKGPublisher implements Publisher {
}
}

private async isSubGraphRegistered(contextGraphId: string, subGraphName: string): Promise<boolean> {
const sgUri = contextGraphSubGraphUri(contextGraphId, subGraphName);
const registered = await this.store.query(
`ASK { GRAPH <did:dkg:context-graph:${assertSafeIri(contextGraphId)}/_meta> {
<${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> ;
<http://schema.org/name> ${JSON.stringify(subGraphName)} ;
<http://dkg.io/ontology/createdBy> ?createdBy .
} }`,
);
return registered.type === 'boolean' && registered.value;
}

/**
* Throws if `subGraphName` is provided but not registered in the CG's `_meta` graph.
* Mirrors the registration check in `publish()` for mutation paths that would
* otherwise create new orphaned sub-graph state.
*/
private async ensureSubGraphRegistered(
Comment thread
Jurij89 marked this conversation as resolved.
contextGraphId: string,
subGraphName: string | undefined,
): Promise<void> {
if (subGraphName === undefined) return;
DKGPublisher.validateOptionalSubGraph(subGraphName);
if (!(await this.isSubGraphRegistered(contextGraphId, subGraphName))) {
Comment thread
Jurij89 marked this conversation as resolved.
throw new Error(
`Sub-graph "${subGraphName}" has not been registered in context graph "${contextGraphId}". ` +
Comment thread
Jurij89 marked this conversation as resolved.
`Register it first via DKGAgent.createSubGraph() or by inserting the sub-graph registration into the context graph "_meta" graph.`,
);
}
}

clearSubGraphOwnership(ownershipKey: string): void {
this.sharedMemoryOwnedEntities.delete(ownershipKey);
this.ownedEntities.delete(ownershipKey);
this.privateStore.clearCache(ownershipKey);
}

async assertionCreate(contextGraphId: string, name: string, agentAddress: string, subGraphName?: string): Promise<string> {
DKGPublisher.validateOptionalSubGraph(subGraphName);
await this.ensureSubGraphRegistered(contextGraphId, subGraphName);
const graphUri = contextGraphAssertionUri(contextGraphId, agentAddress, name, subGraphName);
await this.store.createGraph(graphUri);
return graphUri;
Expand All @@ -1489,7 +1522,7 @@ export class DKGPublisher implements Publisher {
input: Quad[] | Array<{ subject: string; predicate: string; object: string }>,
subGraphName?: string,
): Promise<void> {
DKGPublisher.validateOptionalSubGraph(subGraphName);
await this.ensureSubGraphRegistered(contextGraphId, subGraphName);
const graphUri = contextGraphAssertionUri(contextGraphId, agentAddress, name, subGraphName);
const quads = input.map((t) => ({
subject: t.subject, predicate: t.predicate, object: t.object, graph: graphUri,
Expand Down Expand Up @@ -1517,7 +1550,7 @@ export class DKGPublisher implements Publisher {
agentAddress: string,
opts?: { entities?: string[] | 'all'; subGraphName?: string; publisherPeerId?: string },
): Promise<{ promotedCount: number; gossipMessage?: Uint8Array }> {
DKGPublisher.validateOptionalSubGraph(opts?.subGraphName);
await this.ensureSubGraphRegistered(contextGraphId, opts?.subGraphName);
const graphUri = contextGraphAssertionUri(contextGraphId, agentAddress, name, opts?.subGraphName);
const swmGraphUri = this.graphManager.sharedMemoryUri(contextGraphId, opts?.subGraphName);

Expand Down
6 changes: 5 additions & 1 deletion packages/publisher/src/workspace-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ export class SharedMemoryHandler {
const sgUri = contextGraphSubGraphUri(contextGraphId, subGraphName);
const metaGraph = `did:dkg:context-graph:${assertSafeIri(contextGraphId)}/_meta`;
const alreadyRegistered = await this.store.query(
`ASK { GRAPH <${metaGraph}> { <${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> } }`,
`ASK { GRAPH <${metaGraph}> {
<${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> ;
<http://schema.org/name> ${JSON.stringify(subGraphName)} ;
<http://dkg.io/ontology/createdBy> ?createdBy .
} }`,
);
if (alreadyRegistered.type !== 'boolean' || !alreadyRegistered.value) {
const regQuads = generateSubGraphRegistration({
Expand Down
Loading
Loading