-
Notifications
You must be signed in to change notification settings - Fork 6
fix(publisher): drop random fallback wallet + audit Wallet.createRandom #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fef9640
199f385
8ea17f1
8e416a4
f0e6917
3f22513
45cb799
ff796b3
f210e12
d25cc58
d64248e
5cbaec4
2c5c289
c34c82b
5a8355f
fae36ce
95f6238
18f079d
8e6dcee
e6005f2
cccf06c
8fc382e
c779b52
8333e80
feacb6c
956e506
b15b0f9
9318f04
4c41f8b
30f16ed
f91048e
2b9dc8c
324131d
2cb7833
edb9392
986d84b
77f91d6
2751758
3191fd4
dddcef2
7a71ed4
cdeea87
e857d3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -452,6 +452,11 @@ export interface DKGAgentConfig { | |
| chainAdapter?: ChainAdapter; | ||
| /** Private key for the V10 ACK signer. When omitted, falls back to chainConfig.operationalKeys[0]. */ | ||
| ackSignerKey?: string; | ||
| /** | ||
| * Publisher EVM address used when publish signing is delegated to the | ||
| * ChainAdapter instead of an in-process publisherPrivateKey. | ||
| */ | ||
| publisherAddress?: string; | ||
| /** | ||
| * EVM chain configuration. If omitted, publishing won't have on-chain finality. | ||
| * `adminPrivateKey` is the private key for the profile admin wallet used | ||
|
|
@@ -482,6 +487,154 @@ export interface DKGAgentConfig { | |
| contextGraphMembershipStore?: ContextGraphMembershipStore; | ||
| } | ||
|
|
||
| function normalizeAdapterPublisherAddress(value: unknown): string | undefined { | ||
| if (typeof value !== 'string' || !ethers.isAddress(value)) return undefined; | ||
| const address = ethers.getAddress(value); | ||
| return address === ethers.ZeroAddress ? undefined : address; | ||
| } | ||
|
|
||
| function recoverCompactSigner(message: Uint8Array, compact: { r: Uint8Array; vs: Uint8Array }): string { | ||
| const signature = ethers.Signature.from({ | ||
| r: ethers.hexlify(compact.r), | ||
| yParityAndS: ethers.hexlify(compact.vs), | ||
| }).serialized; | ||
| return ethers.verifyMessage(message, signature); | ||
| } | ||
|
|
||
| function adapterOperationalPrivateKeyAddress(chain: ChainAdapter): string | undefined { | ||
| const operationalKeyGetter = (chain as unknown as { getOperationalPrivateKey?: () => unknown }) | ||
| .getOperationalPrivateKey; | ||
| if (typeof operationalKeyGetter !== 'function') return undefined; | ||
| try { | ||
| const privateKey = operationalKeyGetter.call(chain); | ||
| return typeof privateKey === 'string' && privateKey.length > 0 | ||
| ? privateKeyAddress(privateKey) | ||
| : undefined; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| function adapterHasOperationalPrivateKey(chain: ChainAdapter): boolean { | ||
| return adapterOperationalPrivateKeyAddress(chain) !== undefined; | ||
| } | ||
|
|
||
| async function adapterGenericSignMessageMatchesAddress( | ||
| chain: ChainAdapter, | ||
| expectedAddress: string, | ||
| ): Promise<boolean> { | ||
| if (chain.chainId === 'none' || typeof chain.signMessage !== 'function') return false; | ||
| const normalized = normalizeAdapterPublisherAddress(expectedAddress); | ||
| if (!normalized) return false; | ||
|
|
||
| try { | ||
| const challenge = ethers.getBytes(ethers.id(`dkg-agent:chain-signer-probe:${normalized.toLowerCase()}`)); | ||
| const compact = await chain.signMessage(challenge); | ||
| const recovered = normalizeAdapterPublisherAddress(recoverCompactSigner(challenge, compact)); | ||
| return recovered?.toLowerCase() === normalized.toLowerCase(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async function adapterAdvertisesPublisherSigner(chain: ChainAdapter): Promise<boolean> { | ||
| const hasReservingOrMultiAddressProbe = typeof chain.getAuthorizedPublisherAddress === 'function' || | ||
| typeof (chain as unknown as { getSignerAddresses?: unknown }).getSignerAddresses === 'function'; | ||
| const hasSingleAddressProbe = typeof (chain as unknown as { getSignerAddress?: unknown }).getSignerAddress === 'function' || | ||
| Boolean(normalizeAdapterPublisherAddress((chain as unknown as { signerAddress?: unknown }).signerAddress)); | ||
| const hasAnyAddressProbe = hasReservingOrMultiAddressProbe || hasSingleAddressProbe; | ||
|
|
||
| if (typeof chain.signMessageAs === 'function') return hasAnyAddressProbe; | ||
| if (adapterHasOperationalPrivateKey(chain)) return true; | ||
| if (typeof chain.signMessage !== 'function') return false; | ||
|
|
||
| const advertisedAddress = await inferAdapterPublisherAddress(chain, undefined, { | ||
| includeReservingPublisherProbe: false, | ||
| includeGenericSignMessageProbe: false, | ||
| }); | ||
| if (!advertisedAddress) return false; | ||
| return adapterGenericSignMessageMatchesAddress(chain, advertisedAddress); | ||
| } | ||
|
|
||
| function privateKeyAddress(privateKey: string | undefined): string | undefined { | ||
| if (!privateKey) return undefined; | ||
| try { | ||
| return normalizeAdapterPublisherAddress(new ethers.Wallet(privateKey).address); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| async function inferAdapterPublisherAddress( | ||
| chain: ChainAdapter, | ||
| contextGraphId?: bigint, | ||
| options?: { | ||
| includeReservingPublisherProbe?: boolean; | ||
| includeGenericSignMessageProbe?: boolean; | ||
| }, | ||
| ): Promise<string | undefined> { | ||
| if ( | ||
| options?.includeReservingPublisherProbe !== false && | ||
| contextGraphId !== undefined && | ||
| typeof chain.getAuthorizedPublisherAddress === 'function' | ||
| ) { | ||
| try { | ||
| const address = normalizeAdapterPublisherAddress( | ||
| await chain.getAuthorizedPublisherAddress(contextGraphId), | ||
| ); | ||
| if (address) return address; | ||
| } catch { | ||
| // Best-effort probe; the publisher resolver retries on later publish/update attempts. | ||
| } | ||
| } | ||
|
|
||
| const signerAddressGetter = (chain as unknown as { getSignerAddress?: () => unknown }).getSignerAddress; | ||
| if (typeof signerAddressGetter === 'function') { | ||
| try { | ||
| const address = normalizeAdapterPublisherAddress( | ||
| await Promise.resolve(signerAddressGetter.call(chain)), | ||
| ); | ||
| if (address) return address; | ||
| } catch { | ||
| // Best-effort probe; fall through to broader adapter surfaces. | ||
| } | ||
| } | ||
|
|
||
| const signerAddresses = (chain as unknown as { getSignerAddresses?: () => unknown }).getSignerAddresses; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Issue: |
||
| if (typeof signerAddresses === 'function') { | ||
| try { | ||
| const advertised = await Promise.resolve(signerAddresses.call(chain)); | ||
| if (Array.isArray(advertised)) { | ||
| for (const value of advertised) { | ||
| const address = normalizeAdapterPublisherAddress(value); | ||
| if (address) return address; | ||
| } | ||
| } | ||
| } catch { | ||
| // Best-effort probe; the publisher resolver retries on later publish/update attempts. | ||
| } | ||
| } | ||
|
|
||
| const signerAddress = normalizeAdapterPublisherAddress( | ||
| (chain as unknown as { signerAddress?: unknown }).signerAddress, | ||
| ); | ||
| if (signerAddress) return signerAddress; | ||
|
|
||
| const adapterOperationalAddress = adapterOperationalPrivateKeyAddress(chain); | ||
| if (adapterOperationalAddress) return adapterOperationalAddress; | ||
|
|
||
| if (options?.includeGenericSignMessageProbe === false) return undefined; | ||
| if (chain.chainId === 'none' || typeof chain.signMessage !== 'function') return undefined; | ||
|
|
||
| try { | ||
| const challenge = ethers.getBytes(ethers.id('dkg-agent:publisher-address-probe')); | ||
| const compact = await chain.signMessage(challenge); | ||
| return normalizeAdapterPublisherAddress(recoverCompactSigner(challenge, compact)); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * High-level facade that ties together all DKG agent capabilities: | ||
| * identity, networking, publishing, querying, discovery, and messaging. | ||
|
|
@@ -680,12 +833,31 @@ export class DKGAgent { | |
| const node = new DKGNode(nodeConfig); | ||
| const workspaceOwnedEntities = new Map<string, Map<string, string>>(); | ||
| const writeLocks = new Map<string, Promise<void>>(); | ||
| const legacyAdapterOperationalKey = opKeys?.[0]; | ||
| const legacyAdapterOperationalAddress = privateKeyAddress(legacyAdapterOperationalKey); | ||
| const configuredPublisherAddress = normalizeAdapterPublisherAddress(config.publisherAddress); | ||
| const publisherAddressMatchesLegacyKey = Boolean( | ||
| configuredPublisherAddress && | ||
| legacyAdapterOperationalAddress && | ||
| configuredPublisherAddress.toLowerCase() === legacyAdapterOperationalAddress.toLowerCase(), | ||
| ); | ||
| const adapterCanPublishFromAdvertisedSigner = await adapterAdvertisesPublisherSigner(chain); | ||
| const useLegacyAdapterOperationalKeyFallback = Boolean( | ||
| config.chainAdapter && | ||
| legacyAdapterOperationalKey && | ||
| !adapterCanPublishFromAdvertisedSigner && | ||
| (!configuredPublisherAddress || publisherAddressMatchesLegacyKey), | ||
| ); | ||
| const publisher = new DKGPublisher({ | ||
| store, | ||
| chain, | ||
| eventBus, | ||
| keypair, | ||
| publisherPrivateKey: opKeys?.[0], | ||
| publisherPrivateKey: useLegacyAdapterOperationalKeyFallback ? legacyAdapterOperationalKey : undefined, | ||
| publisherAddress: config.publisherAddress, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Bug: |
||
| publisherAddressResolver: config.publisherAddress || useLegacyAdapterOperationalKeyFallback | ||
| ? undefined | ||
| : (contextGraphId?: bigint) => inferAdapterPublisherAddress(chain, contextGraphId), | ||
| sharedMemoryOwnedEntities: workspaceOwnedEntities, | ||
| writeLocks, | ||
| }); | ||
|
|
@@ -4376,7 +4548,7 @@ export class DKGAgent { | |
| ); | ||
| } | ||
| const publishAuthority = publishPolicy === EVM_PUBLISH_CURATED | ||
| ? this.getChainPublishAuthorityAddress() | ||
| ? await this.getChainPublishAuthorityAddress(id) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Bug: This now derives curated registration authority through |
||
| : undefined; | ||
| if ( | ||
| publishPolicy === EVM_PUBLISH_CURATED | ||
|
|
@@ -7103,16 +7275,33 @@ export class DKGAgent { | |
| && ownerAddress.toLowerCase() === this.defaultAgentAddress.toLowerCase(); | ||
| } | ||
|
|
||
| private getChainPublishAuthorityAddress(): string | undefined { | ||
| const chainWithSigner = this.chain as unknown as { | ||
| getSignerAddress?: () => string; | ||
| signerAddress?: string; | ||
| }; | ||
| const rawAddress = chainWithSigner.getSignerAddress?.() ?? chainWithSigner.signerAddress; | ||
| if (rawAddress && ethers.isAddress(rawAddress)) { | ||
| return ethers.getAddress(rawAddress); | ||
| private async getChainPublishAuthorityAddress(contextGraphId?: string): Promise<string | undefined> { | ||
| const configuredPublisherAddress = normalizeAdapterPublisherAddress(this.config.publisherAddress); | ||
| if (configuredPublisherAddress) return configuredPublisherAddress; | ||
|
|
||
| const legacyAdapterOperationalKey = this.config.chainConfig?.operationalKeys?.[0]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Bug: this only looks at |
||
| const legacyAdapterOperationalAddress = privateKeyAddress(legacyAdapterOperationalKey); | ||
| if ( | ||
| this.config.chainAdapter && | ||
| legacyAdapterOperationalAddress && | ||
| !(await adapterAdvertisesPublisherSigner(this.chain)) | ||
| ) { | ||
| return legacyAdapterOperationalAddress; | ||
| } | ||
| return undefined; | ||
|
|
||
| let publisherContextGraphId: bigint | undefined; | ||
| try { | ||
| const parsed = BigInt(contextGraphId ?? ''); | ||
| if (parsed > 0n) publisherContextGraphId = parsed; | ||
| } catch { | ||
| // Local descriptive CG ids cannot be used as adapter context hints. | ||
| } | ||
| // This mirrors the publisher resolver, including the adapter-only | ||
| // `getOperationalPrivateKey()` fallback used by custom ChainAdapters. | ||
| return inferAdapterPublisherAddress(this.chain, publisherContextGraphId, { | ||
| includeReservingPublisherProbe: false, | ||
| includeGenericSignMessageProbe: false, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Issue: This adds async
getSignerAddress()support here, but the agent still readsgetSignerAddress()synchronously ingetChainPublishAuthorityAddress()during curated context-graph registration. A custom adapter that follows this new async pattern will publish fine but can still failregisterContextGraph()with the "without chain signer introspection" path for non-default curators. Either keepgetSignerAddress()sync-only, or update that registration guard to await async results too.