Skip to content

Commit d62d29d

Browse files
authored
Merge pull request #502 from OriginTrail/feat/daemon-pca-curated-registration-rebased
feat(context-graph): wire PCA id at registration so PCA agents can publish (rebase of #423)
2 parents 5038e92 + d80b88d commit d62d29d

12 files changed

Lines changed: 1333 additions & 51 deletions

File tree

packages/agent/src/dkg-agent.ts

Lines changed: 278 additions & 25 deletions
Large diffs are not rendered by default.

packages/agent/test/agent.test.ts

Lines changed: 560 additions & 0 deletions
Large diffs are not rendered by default.

packages/chain/src/chain-adapter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,13 @@ export interface ChainAdapter {
581581
extendConvictionLock?(accountId: bigint, additionalEpochs: number): Promise<TxResult>;
582582
getConvictionDiscount?(accountId: bigint): Promise<{ discountBps: number; conviction: bigint }>;
583583
getConvictionAccountInfo?(accountId: bigint): Promise<ConvictionAccountInfo | null>;
584+
/**
585+
* Live owner lookup for a PCA NFT — wraps `DKGPublishingConvictionNFT.ownerOf(accountId)`.
586+
* Used by the daemon's curated-CG registration preflight to enforce
587+
* `local curator == ownerOf(pcaAccountId)` so an agent wallet cannot
588+
* impersonate ownership when tying a CG to a PCA.
589+
*/
590+
getPublishingConvictionAccountOwner?(accountId: bigint): Promise<string>;
584591
/**
585592
* Authorize an EOA to draw down on the PCA's discounted publishing
586593
* allowance. Wraps `PublishingConvictionAccount.addAuthorizedKey(accountId, key)`,

packages/chain/src/evm-adapter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2570,6 +2570,13 @@ export class EVMChainAdapter implements ChainAdapter {
25702570
}
25712571
}
25722572

2573+
async getPublishingConvictionAccountOwner(accountId: bigint): Promise<string> {
2574+
await this.init();
2575+
const nft = await this.resolveContract('DKGPublishingConvictionNFT');
2576+
const owner = await nft.ownerOf(accountId);
2577+
return ethers.getAddress(owner);
2578+
}
2579+
25732580
async getConvictionDiscount(accountId: bigint): Promise<{ discountBps: number; conviction: bigint }> {
25742581
await this.init();
25752582
if (!this.contracts.publishingConvictionAccount) {

packages/chain/src/mock-adapter.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,22 @@ export class MockChainAdapter implements ChainAdapter {
674674
return 0;
675675
}
676676

677+
/**
678+
* Mock owner-lookup for the daemon's curated-CG registration
679+
* preflight (`local curator == ownerOf(pcaAccountId)`).
680+
*
681+
* Mock does not model PCA NFT transfers, so the account `admin`
682+
* doubles as the current "owner" for parity-test purposes. Real-chain
683+
* tests use `EVMChainAdapter`, which queries `DKGPublishingConvictionNFT.ownerOf`.
684+
*/
685+
async getPublishingConvictionAccountOwner(accountId: bigint): Promise<string> {
686+
const acct = this.convictionAccounts.get(accountId);
687+
if (!acct) {
688+
throw new Error(`Mock: PCA account ${accountId} does not exist`);
689+
}
690+
return ethers.getAddress(acct.admin);
691+
}
692+
677693
// --- Staking Conviction ---
678694

679695
private delegatorLocks = new Map<string, { lockTier: number; startEpoch: number }>();
@@ -768,12 +784,15 @@ export class MockChainAdapter implements ChainAdapter {
768784
}
769785
publishAuthority = ethers.getAddress(publishAuthority);
770786
if (publishPolicy === 0) {
771-
if (publishAuthorityAccountId !== 0n) {
772-
throw new Error('Mock: PCA publishAuthorityAccountId is not supported');
773-
}
774787
if (publishAuthority === ethers.ZeroAddress) {
775788
publishAuthority = ethers.getAddress(this.signerAddress);
776789
}
790+
if (publishAuthorityAccountId !== 0n) {
791+
const pcaOwner = await this.getPublishingConvictionAccountOwner(publishAuthorityAccountId);
792+
if (publishAuthority.toLowerCase() !== pcaOwner.toLowerCase()) {
793+
throw new Error('Mock: PCA publishAuthority must match account owner');
794+
}
795+
}
777796
} else {
778797
if (publishAuthority !== ethers.ZeroAddress) {
779798
throw new Error('Mock: open policy requires zero publishAuthority');

packages/chain/src/no-chain-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class NoChainAdapter implements ChainAdapter {
4949
async isOperationalWalletRegistered(_identityId: bigint, _address: string): Promise<boolean> { return false; }
5050
async getKnowledgeAssetsV10Address(): Promise<string> { noChain(); }
5151
async getEvmChainId(): Promise<bigint> { noChain(); }
52+
async getPublishingConvictionAccountOwner(_accountId: bigint): Promise<string> { noChain(); }
5253
isV10Ready(): boolean { return false; }
5354
isRandomSamplingReady(): boolean { return false; }
5455
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,34 @@ describe('MockChainAdapter API parity with EVMChainAdapter [CH-8]', () => {
284284
...base,
285285
publishPolicy: 0,
286286
publishAuthorityAccountId: 1n,
287-
})).rejects.toThrow(/PCA publishAuthorityAccountId is not supported/);
287+
})).rejects.toThrow(/PCA account 1 does not exist/);
288+
289+
const { accountId } = await mock.createConvictionAccount(1n, 1);
288290

289291
await expect(mock.createOnChainContextGraph({
290292
...base,
291293
publishPolicy: 0,
294+
publishAuthority: '0x2222222222222222222222222222222222222222',
295+
publishAuthorityAccountId: accountId,
296+
})).rejects.toThrow(/PCA publishAuthority must match account owner/);
297+
298+
await expect(mock.createOnChainContextGraph({
299+
...base,
300+
publishPolicy: 0,
301+
publishAuthority: '0x1111111111111111111111111111111111111111',
302+
publishAuthorityAccountId: accountId,
292303
})).resolves.toMatchObject({ contextGraphId: 1n });
304+
305+
await expect(mock.createOnChainContextGraph({
306+
...base,
307+
publishPolicy: 0,
308+
publishAuthorityAccountId: accountId,
309+
})).resolves.toMatchObject({ contextGraphId: 2n });
310+
311+
await expect(mock.createOnChainContextGraph({
312+
...base,
313+
publishPolicy: 0,
314+
})).resolves.toMatchObject({ contextGraphId: 3n });
293315
});
294316

295317
it('requires explicit mock support for address-specific V10 publishing', async () => {

packages/cli/src/api-client.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,43 @@ export class ApiClient {
896896
participantAgents?: string[];
897897
participantIdentityIds?: Array<string | number | bigint>;
898898
requiredSignatures?: number;
899+
/**
900+
* Atomic combined-flow flag. When `true`, the daemon registers the
901+
* CG on-chain in the same call after the local create step
902+
* succeeds. Required when `pcaAccountId` is supplied (a standalone
903+
* `createContextGraph` does NOT persist PCA ids — Codex PR #502
904+
* round-3).
905+
*/
906+
register?: boolean;
907+
/**
908+
* Publish policy override forwarded to `registerContextGraph` in
909+
* the combined-flow path. Only meaningful together with
910+
* `register: true`. The agent otherwise defaults
911+
* `publishPolicy = curated (0)` for curated/private CGs and
912+
* `publishPolicy = open (1)` for public CGs — which makes the
913+
* valid `{ accessPolicy: 0 (public), publishPolicy: 0 (curated),
914+
* pcaAccountId }` combo unreachable unless the caller can pin
915+
* `publishPolicy` explicitly. Codex PR #502 round-10 (raised by
916+
* @branarakic).
917+
*/
918+
publishPolicy?: number;
919+
/**
920+
* Publishing Conviction Account id for PCA-curated registration.
921+
* Only meaningful together with `register: true`. The daemon
922+
* rejects the create-only-with-pcaAccountId combo with a 400
923+
* (Codex PR #502 round-5). For a two-step flow, use
924+
* {@link registerContextGraph} instead.
925+
*/
926+
pcaAccountId?: string | number | bigint;
899927
}, allowedPeers?: string[]): Promise<{
900928
created: string;
901929
uri: string;
930+
/** Present only when caller passed `register: true`. */
931+
registered?: boolean;
932+
onChainId?: string;
933+
/** Present when `register: true` was requested but the register leg failed. */
934+
registerError?: string;
935+
hint?: string;
902936
}> {
903937
return this.post('/api/context-graph/create', {
904938
id,
@@ -913,6 +947,9 @@ export class ApiClient {
913947
? { participantIdentityIds: options.participantIdentityIds.map((id) => id.toString()) }
914948
: {}),
915949
...(options?.requiredSignatures != null ? { requiredSignatures: options.requiredSignatures } : {}),
950+
...(options?.register === true ? { register: true } : {}),
951+
...(options?.publishPolicy != null ? { publishPolicy: options.publishPolicy } : {}),
952+
...(options?.pcaAccountId != null ? { pcaAccountId: options.pcaAccountId.toString() } : {}),
916953
});
917954
}
918955

@@ -928,6 +965,7 @@ export class ApiClient {
928965
revealOnChain?: boolean;
929966
accessPolicy?: number;
930967
publishPolicy?: number;
968+
pcaAccountId?: string | number | bigint;
931969
}): Promise<{
932970
registered: string;
933971
onChainId: string;
@@ -937,6 +975,7 @@ export class ApiClient {
937975
id,
938976
...(opts?.accessPolicy != null ? { accessPolicy: opts.accessPolicy } : {}),
939977
...(opts?.publishPolicy != null ? { publishPolicy: opts.publishPolicy } : {}),
978+
...(opts?.pcaAccountId != null ? { pcaAccountId: opts.pcaAccountId.toString() } : {}),
940979
});
941980
}
942981

packages/cli/src/cli.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,12 @@ contextGraphCmd
13271327

13281328
const participantIdentityIds = (opts.participantIdentityId as string[] | undefined) ?? [];
13291329
const allowedAgents = (opts.allowedAgent as string[] | undefined) ?? [];
1330+
// pcaAccountId is intentionally NOT a flag on `create` —
1331+
// `createContextGraph` no longer persists the id, so a
1332+
// create-time flag would silently drop the PCA configuration
1333+
// before `register <id>` is run (Codex PR #502 round-4). Users
1334+
// who want PCA-curated registration pass `--pca-account-id` on
1335+
// `dkg context-graph register <id>` instead.
13301336
const accessPolicy = allowedAgents.length > 0 ? 1 : (opts.accessPolicy as number | undefined);
13311337

13321338
const result = await client.createContextGraph(id, opts.name ?? id, opts.description, {
@@ -1385,19 +1391,28 @@ contextGraphCmd
13851391
.option('--reveal', 'Deprecated: V10 ContextGraphs registration does not reveal cleartext metadata on-chain')
13861392
.option('--access-policy <n>', 'Access policy: 0 = public/discoverable, 1 = private/curated', parseInt)
13871393
.option('--publish-policy <n>', 'Publish policy: 0 = curated, 1 = open', parseInt)
1394+
.option('--pca-account-id <id>', 'Publishing Conviction Account id for PCA-curated registration')
13881395
.action(async (id: string, opts: ActionOpts) => {
13891396
try {
13901397
const client = await ApiClient.connect();
13911398
if (opts.reveal) {
13921399
console.warn('--reveal is deprecated and ignored for V10 ContextGraphs registration.');
13931400
}
1401+
const pcaAccountId = opts.pcaAccountId as string | undefined;
1402+
if (pcaAccountId && !/^[1-9]\d*$/.test(pcaAccountId)) {
1403+
throw new Error('--pca-account-id must be a positive decimal integer');
1404+
}
13941405
const result = await client.registerContextGraph(id, {
13951406
accessPolicy: opts.accessPolicy != null ? Number(opts.accessPolicy) : undefined,
13961407
publishPolicy: opts.publishPolicy != null ? Number(opts.publishPolicy) : undefined,
1408+
pcaAccountId,
13971409
});
13981410
console.log(`Context graph registered on-chain:`);
13991411
console.log(` ID: ${id}`);
14001412
console.log(` On-chain: ${result.onChainId}`);
1413+
if (pcaAccountId) {
1414+
console.log(` PCA account id: ${pcaAccountId}`);
1415+
}
14011416
console.log(` ${result.hint ?? 'You can now publish SWM to Verified Memory.'}`);
14021417
} catch (err) {
14031418
console.error(toErrorMessage(err));

0 commit comments

Comments
 (0)