Skip to content

fix(random-sampling): resolve CG names with "/" so v9-style <owner>/<slug> CGs sync#377

Merged
branarakic merged 3 commits intomainfrom
fix/rs-extractor-cgname-slash-resolve
May 4, 2026
Merged

fix(random-sampling): resolve CG names with "/" so v9-style <owner>/<slug> CGs sync#377
branarakic merged 3 commits intomainfrom
fix/rs-extractor-cgname-slash-resolve

Conversation

@branarakic
Copy link
Copy Markdown
Contributor

@branarakic branarakic commented May 4, 2026

Summary

Hotfix for an issue that suppressed every Random Sampling proof on testnet despite challenges firing correctly.

resolveContextGraphNameFromOnChainId in kc-extractor.ts was rejecting any context-graph name containing /. Real CGs are commonly registered with owner-prefixed names like 0xb08A0F66d5A225D57Dee5fFa6C442e4DC2a4794c/laptop-smoke — the v9 <owner_address>/<slug> namespacing pattern. The resolver returned null, so extractV10KCFromStore threw KCNotFoundError for every KC in the CG, even after FinalizationHandler had already promoted the SWM snapshot to canonical.

Reproduction (live testnet, beacon-04)

After publishing KC #6 + #7 to context graph 12 (cgName 0xb08…4794c/laptop-smoke):

08:19:35 [FinalizationHandler] Finalization: promoted SWM snapshot to context graph 12 for did:dkg:base:84532/0xb08…4794c/5000001 (tx=0x2a7328ba…)
08:22:27 [FinalizationHandler] Finalization: promoted SWM snapshot to context graph 12 for did:dkg:base:84532/0xb08…4794c/6000001 (tx=0x79b10bdd…)
08:22:48 [DKGAgent] [rs.tick.kc-not-synced] {"kcId":"6","cgId":"12","err":"KCNotFoundError"} [WARN]
   …repeats every ~30s for 40+ minutes…

A 10-minute on-chain event watcher across all four beacons observed:

Fix

Replace the over-tight name-level allowlist with assertSafeIri on the derived meta-graph URI, which is the actual SPARQL-injection surface:

// Before
if (!name || name.includes('/') || name.includes(' ')) return null;

// After
if (!name) return null;
try {
  assertSafeIri(contextGraphMetaUri(name, '0'));
} catch {
  return null;
}

This still rejects <>"{}|\^ and control chars (poisoned ontology entries also can't land in OxigraphStore in the first place), but admits legitimate path-style names. Both ends of the writer/reader handshake (FinalizationHandler.promoteSharedMemoryToCanonical writes via contextGraphMetaUri(contextGraphId, ctxGraphId); the extractor now reads via the same helper with the resolved name) end up at the same URI.

Regression coverage

Two layers of test, both reverse-validated by temporarily restoring the name.includes('/') rejection — both fail loud:

  1. Unit (kc-extractor.test.ts) — seeds a CG named 0xb08…4794c/laptop-smoke and asserts extractV10KCFromStore returns triples. With the bug present this throws the EXACT error from testnet: KCNotFoundError: KC 6 not found in _meta for context graph 12.
  2. E2E hardhat (e2e-hardhat-chain.test.ts) — switches the local cgName from cg-<cgIdStr> to 0xb08…4794c/cg-<cgIdStr> so the chain-publish → local-store → prover-tick path is run with the v9-style namespacing pattern. Pre-fix the prover returns kc-not-synced; post-fix it returns submitted.

Devnet validation

Brought up local devnet (./scripts/devnet.sh start 6) with the fixed code, ran the existing baseline ./scripts/devnet-test-random-sampling.sh (passes against the no-/ devnet-test CG), then created an additional CG with a /-name via POST /api/context-graph/create:

$ curl -X POST .../api/context-graph/create -d '{"id":"0xdeadbeef…/devnet-slash-test","name":"slash test","register":true}'
{"created":"0xdeadbeef…/devnet-slash-test","registered":true,"onChainId":"3"}

Published a KC into it (became kcId=2), advanced the chain past the proofing-period boundary, and tailed each prover's WAL:

=== node 1 ===  kc-not-synced count: 0
  status=challenge  cgId=3 kcId=2 tx=
  status=extracted  cgId=3 kcId=2 tx=
  status=built      cgId=3 kcId=2 tx=
  status=submitted  cgId=3 kcId=2 tx=0xe3f45c0f1ffd3ad7…   ← slash-CG proof submitted!
=== node 3 ===  kc-not-synced count: 0
  status=challenge  cgId=3 kcId=2 tx=
  status=extracted  cgId=3 kcId=2 tx=
  status=built      cgId=3 kcId=2 tx=
  status=submitted  cgId=3 kcId=2 tx=0x0fbf607383774826…   ← slash-CG proof submitted!

getNodeChallenge(idId).solved == true for all four identities; identities 1 and 3 picked the slash-CG (cgId=3 / kcId=2). Zero kc-not-synced warnings on any node — the bug signature is fully gone in a real-daemon environment.

Devnet-side fix bundled in

To get devnet running, this PR also fixes three unrelated quoting bugs in scripts/devnet.sh introduced in c09e0261 (Codex round 7 on PR #368) where embedded "..." substrings inside JS comments inside multi-line node -e "…" blocks made bash truncate the script body — ./scripts/devnet.sh start was failing 100% of the time with SyntaxError: Unexpected end of input. Pure quoting refactor; comment meaning unchanged. Without this, the random-sampling devnet validation above can't be run.

Test plan

  • Unit + e2e regression tests reverse-validated (both fail without fix, pass with fix).
  • All 47 random-sampling tests pass (pnpm --filter @origintrail-official/dkg-random-sampling test).
  • Local devnet end-to-end: /-named CG submits proof through full RS pipeline.
  • After merge, redeploy to a beacon and confirm rs.tick.kc-not-synced clears + ValidProofSubmitted lands on chain for the existing testnet 0xb08…4794c/laptop-smoke CG.

Notes for reviewers

…> CGs resolve

`resolveContextGraphNameFromOnChainId` was rejecting any context-graph name
containing "/", returning `null` and causing `extractV10KCFromStore` to throw
`KCNotFoundError` for every KC in the CG — even after `FinalizationHandler`
had already promoted the SWM snapshot to the canonical store.

Real CGs are commonly registered with owner-prefixed names like
`0xb08…4794c/laptop-smoke` (the v9 `<owner_address>/<slug>` namespacing
pattern). The FinalizationHandler writes the canonical data to
`did:dkg:context-graph:<owner>/<slug>/context/<cgId>/_meta` correctly, but the
extractor never reaches the meta query because the resolver short-circuits.
On testnet beacon-04 this surfaced as the persistent log pair:

    [FinalizationHandler] Finalization: promoted SWM snapshot to context graph 12 …
    [DKGAgent] [rs.tick.kc-not-synced] {"kcId":"6","cgId":"12","err":"KCNotFoundError"}

…with zero `ValidProofSubmitted` events on chain across a 10-minute on-chain
event watch window despite all four beacons emitting `ChallengeGenerated`.

Replace the over-tight name-level allowlist with `assertSafeIri` on the
*derived* meta-graph URI, which is the actual SPARQL-injection surface.
This still rejects `<>"{}|\^` and control chars (poisoned ontology entries
also can't even land in OxigraphStore in the first place), but admits
legitimate path-style names. Adds a regression test that seeds a CG named
`0xb08…4794c/laptop-smoke` and asserts the extractor returns triples.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review completed — no issues found.

… for PR #377)

Switch the e2e-hardhat test's local cgName from `cg-<cgIdStr>` to
`0xb08…4794c/cg-<cgIdStr>` so the chain-publish → local-store →
prover-tick path is run with the v9-style `<owner_address>/<slug>`
namespacing pattern that real beacons register on testnet. A pre-PR-#377
build short-circuits in `resolveContextGraphNameFromOnChainId`, making
`prover.tick()` return `kc-not-synced` instead of `submitted` —
the exact failure mode that suppressed every RS proof on testnet
beacon-04 for 40+ minutes despite chain challenges firing within
seconds of each `Finalization: promoted SWM snapshot`.

Verified locally: with the resolver fix reverted, this test fails;
with the fix in place, it passes (alongside the existing unit-level
regression in `kc-extractor.test.ts`). No new test fixtures or
beforeAll cost — just a one-line cgName change that pins the bug at
the integration level for free.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review completed — no issues found.

….sh starts

Three JS comments inside multi-line, double-quoted `node -e "..."` blocks
in `scripts/devnet.sh` contained unescaped `"..."` substrings. Bash split
the JS body on those quotes, leaving Node with a truncated script that
exited with `SyntaxError: Unexpected end of input` and prevented
`./scripts/devnet.sh start` from completing. Reproduces 100% on a clean
clone since `c09e0261` (Codex round 7 on PR #368).

Rephrase the offending comments to avoid embedded double-quotes — the
explanations are unchanged in meaning. No behavioural change to the
underlying logic; this is purely a quoting fix that unblocks local
devnet bring-up (without it, the random-sampling/`/`-cgname end-to-end
validation for PR #377 cannot be run on devnet).

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review completed — no issues found.

@branarakic branarakic merged commit c0b5543 into main May 4, 2026
19 of 32 checks passed
branarakic pushed a commit that referenced this pull request May 4, 2026
Hotfix release on top of v10.0.0-rc.3. No contract redeploy on Base
Sepolia beyond the Profile/Identity upgrade landed in #372 — the
chainResetMarker is intentionally unchanged so per-node state
(oxigraph store, RS WAL, publish journals) is preserved across the
operator update.

Ships:
- PR #377: random-sampling kc-extractor accepts v9-style <owner>/<slug>
  context graph names again — restores ValidProofSubmitted on testnet
  for slash-named CGs (which is most of them). Includes unit + e2e
  regression tests and a devnet.sh quoting fix that was the precondition
  for validating the fix on a local devnet.
- PR #372: Base Sepolia Profile v1.0.0 → v1.1.0 + Identity re-deploy in
  lockstep, so the post-PR-#366 ensureProfile flow can finally register
  op[1]/op[2] ACK signers via the new addOperationalWallets external
  function. Both contracts are pure logic; no migration; existing
  identities 1-14 unaffected.

See CHANGELOG.md for the full per-PR breakdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
branarakic pushed a commit that referenced this pull request May 4, 2026
Hotfix release on top of v10.0.0-rc.3. No contract redeploy on Base
Sepolia beyond the Profile/Identity upgrade landed in #372 — the
chainResetMarker is intentionally unchanged so per-node state
(oxigraph store, RS WAL, publish journals) is preserved across the
operator update.

Ships:
- PR #377: random-sampling kc-extractor accepts v9-style <owner>/<slug>
  context graph names again — restores ValidProofSubmitted on testnet
  for slash-named CGs (which is most of them). Includes unit + e2e
  regression tests and a devnet.sh quoting fix that was the precondition
  for validating the fix on a local devnet.
- PR #372: Base Sepolia Profile v1.0.0 → v1.1.0 + Identity re-deploy in
  lockstep, so the post-PR-#366 ensureProfile flow can finally register
  op[1]/op[2] ACK signers via the new addOperationalWallets external
  function. Both contracts are pure logic; no migration; existing
  identities 1-14 unaffected.

See CHANGELOG.md for the full per-PR breakdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant