Skip to content

fix(dacs-directory): bounded revocation work, owner-scoped deals and separate retry classes - #94

Merged
randomblocker merged 4 commits into
mainfrom
fix/directory-bounded-work-owner-keyed-deals
Oct 10, 2026
Merged

randomblocker merged 4 commits into
mainfrom
fix/directory-bounded-work-owner-keyed-deals

Conversation

@randomblocker

@randomblocker randomblocker commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Base: Community main (4cdb14c), which includes the indexer containment work from #93. Only reference-implementations/dacs-directory/ changes.

This PR makes the directory's indexer do a fixed amount of work per listing on each pass. It keeps each owner's deal and binding records apart from records other owners write for the same jobId, and the deal-owner lookup reports a deal only from refs bound to its parties, using catalog entries only when the indexer verified them or derived them from the scan. It also schedules rejected content and transient read failures as separate retry classes.

What changes

Revocation candidates live in an indexed store

  • Revocation candidates are stored in a SQLite table (revocation_candidates), keyed by listing content hash and locator. Each row records the candidate's storage-program owner, the content hash it was read with, and its arrival order.
  • Each pass selects at most REVOCATION_VERIFICATIONS_PER_PASS (16) candidates per listing, using two indexed queries of at most 16 rows each. The queue is never loaded or scanned as a whole.
  • Candidates are tried in this order:
    1. markers already verified under RB-4;
    2. pending candidates anchored by the listing's own owner, in arrival order;
    3. the remaining pending candidates, in arrival order.
  • A candidate that is read and fails RB-4 is marked rejected. It becomes pending again only if the chain shows that locator with different content.
  • A candidate that cannot be read keeps its pending state and moves behind the others, so it is retried on a later pass. No unexamined candidate is ever removed.
  • Candidates that arrive later never move ahead of one that is already queued.

Deals are scoped by bundle owner and jobId everywhere

  • Discovered deals are keyed by the storage owner of the buyer bundle plus the jobId. The scanner, persisted scan state, the synthetic registrations that build the catalog, and verified-deal deduplication all use this key, so two buyers' deals that share a jobId stay separate records.
  • This key is an indexing namespace only. Buyer attribution still comes from the validated agreement and its party and role evidence (DACS-3 CA-7, DACS-5 §10.4.2), as before.
  • A seller copy is part of a discovered deal only when the deal has an attributed seller and that seller owns the copy. A deal without an attributed seller carries no seller copy. Any copy stored on such an entry by an earlier release is removed on the next pass.
  • A discovered deal keeps its recorded seller copy when a later scan window does not include one.
  • /api/dacs/deal-owners now does the following:
    • It considers every catalog and scanned entry for the jobId.
    • An entry counts only when each bundle ref it carries is bound to the party it is reported for. Either that party owns the storage program, or that party's verified BundleBinding for the role names the address.
    • A catalog entry also has to sit in its own seller's record, and one of the following must hold:
      • the indexer verified both bundle copies against the entry's buyer and seller; or
      • it is a scanned entry as indexed: the same parties, and each ref is either the scanned one or was resolved through that party's BundleBinding for the role.
    • A scanned entry already represented in the catalog counts once, whichever bundle ref is requested.
    • It returns owners only when exactly one distinct, attributed deal remains. Otherwise it returns owners: null. A lone unattributed entry is not reported as ownership.
    • It accepts an optional bundleRef parameter that narrows the lookup to the deal holding that bundle copy. The narrowing applies after the steps above. The deal verifier and the verify page pass the bundle they already hold.
    • The response shape is unchanged.

Bundle-binding discovery is bounded per signer

  • The discovery ceiling (32) applies per authenticated signer of each (jobId, role), as DACS-5 BB-6 requires. Overflow makes only that signer's side indeterminate.
  • When the indexer verifies a deal, it considers only bindings signed by that deal's buyer or seller. Bindings from other signers for the same jobId neither change how the deal is verified nor add verification work.

Rejected content and transient failures are separate retry classes

  • artifacts has separate counters: rejection_count and retry_count.
    • A locator whose content was rejected stays on a periodic schedule until a read succeeds.
    • Locators that only ever failed transiently keep the existing bounded policy.
  • When content fails the artifact policy on a well-formed storage read, that is a rejection. Two examples are non-object data under a DACS program name and an over-long name or owner. STORAGE_INVALID_RESPONSE is kept for malformed node responses.
  • Any successful read clears the rejected class, including a read of a program that holds no object data.
  • Each pass reads at most 100 retries:
    • Up to 20 are reserved for due rejected locators.
    • Transient retries fill the rest and are read first.
  • At most 1,000 rejected locators are scheduled at once. Overflow waits in arrival order and rotates in. Slots freed by recovered locators are refilled at the start of each pass. No rejected locator is dropped.

Smaller fixes

  • Loaded registrations. The registration and its binding count (256) are checked before any binding is verified. An over-limit registration is refused without verifying anything.
  • Producer-side resolver. resolveOwnedAnchorByName reads search and candidate responses through the same 1 MiB streaming ceiling and artifact preflight as the scanner and indexer.
  • Duplicate programs. An owner and name with duplicate programs are indeterminate everywhere:
    • the artifact route answers 409 with the existing { error } body;
    • the deal page shows no attestation;
    • the indexer never falls back to the derived address.

Persistence and migration

  • Scan state, schema 10. The first pass after upgrade replays the full transaction history from genesis.
    • The replay rebuilds every owner's deal entries and binding sets under the keys above. Existing entries are kept, and the replay adds or refreshes them.
    • The version is recorded only when the replay pass completes and its state is saved. An interrupted or failed replay leaves the state at its earlier version, and the next pass replays again.
    • The replay is idempotent. After it, passes are incremental again.
  • Candidate queue. At startup, a candidate queue held in scan-state JSON by the previous release moves into revocation_candidates in its stored order, with owners taken from the artifact table. This runs in one SQLite transaction and does nothing once the JSON queue is empty.
  • Retry classes. artifacts gains rejection_count and deferred_at in place.
    • Existing rejected rows move their counter into rejection_count.
    • Older rejection dead letters return to periodic retry.
    • At every startup, scheduled rejected rows beyond 1,000 are deferred, latest due first. They return to the schedule in due order. This is idempotent.
  • Every migration step runs in one SQLite transaction and is safe to rerun.

Bounds

Bound Value
Revocation verifications per listing per pass 16 reads and RB-4 checks
Revocation queue work per listing per pass 2 indexed selects of at most 16 rows, 1 sequence lookup, at most 16 row updates; memory for at most 48 locators. Independent of queue length.
Revocation queue work per scan One upsert per candidate observed in that scan window
Retry reads per pass 100, with up to 20 reserved for rejected locators
Scheduled rejected retries 1,000; overflow rotates in
Bundle bindings per signer of a (jobId, role) 32
Registration bindings 256, checked before verification
Deal-owner lookup At most one primary-key artifact lookup per bundle ref, for each catalog and scanned entry with the requested jobId, plus an in-memory comparison of each such catalog entry with the scanned entries for that jobId

At the default 15-minute interval, a full set of 1,000 scheduled rejected locators is re-read about every 12.5 hours.

Wire formats, hashes, signature domains, public response shapes, package pins and vendored files are unchanged. Every valid artifact keeps its classification and hash.

Tests

  • test/scan-fairness.test.ts (28 tests), covering:
    • revocation ordering, per-pass work and re-examination;
    • per-pass SQLite statements and rows that are identical with 40 or 400 queued candidates;
    • deal scoping in the scanner, catalog and deduplication;
    • the schema-10 replay, including an interrupted replay;
    • the seller copy kept across windows, and attached only to a deal whose attributed seller owns it;
    • the deal-owner lookup with catalog and scanned entries, deals declared in signed and unsigned registrations, verified and unverified catalog entries, binding-resolved refs and bundleRef narrowing;
    • retry classes, the reserved rejected share, the ceiling, classification of content-policy failures, and recovery on a successful read;
    • registrations, the producer resolver and duplicate-program consumers.
  • test/startup-migration.test.ts (3 tests): the startup ceiling, the order in which deferred rows return, and the move of the JSON candidate queue.
  • test/retry-class-migration.test.ts (2 tests): retry-class migration, including more than 1,000 rejected rows.
  • test/bundle-binding.test.ts and test/current-indexer.test.ts cover the per-signer binding ceiling, at the unit level and in the indexer.
  • Updated existing tests:
    • malformed-artifacts: the queue is read from the table, and content-policy failures are classified as rejections;
    • scan-robustness: rejection observations are flagged;
    • reindex-chain-reset: schema 10.
Check Result
New and changed tests on the first push of this branch (5a523dc) 19 of 89 fail; 17 on their own assertion, 2 because the candidate table does not exist there
Tests added or changed in this revision, on the previous push 2 of 28 in scan-fairness fail, each on its own assertion
New and changed test files on this branch 8 files, 94/94
Full suite, one file per process 36 files, 273/273
Typecheck tsc --noEmit passes
Mutations 108 caught, 1 survived (a missing test case for the binding-signer check; follow-up), 0 invalid

Not in this PR

  • The schema-10 replay runs in one pass, so it must reach genesis within DACS_SCAN_MAX_TXS. A pass that cannot do so fails with the existing message and leaves the state unmigrated.
  • A listing's status reflects the revocation candidates examined so far. Candidates anchored by the listing's owner are examined first.
  • A deployment-supplied key resolver that returns null for an unavailable key causes that candidate to be recorded as rejected. The default resolver is local.
  • Stored bundle bindings are re-verified on each pass, as before.
  • A deal-owner lookup by jobId alone returns owners: null whenever more than one entry exists for that jobId. Passing bundleRef narrows it. Listing the candidates is a possible follow-up.
  • In the catalog, a registration's declared deal still takes precedence over a discovered deal with the same buyer and jobId. The deal-owner lookup does not depend on that order. Revisiting the precedence is a follow-up.
  • The startup deferral of rejected rows beyond 1,000 updates them one row at a time. A single set-based update is a follow-up.
  • Faster scan-state loading.
  • Attribution for agreements made with the vendored legacy SDK, as described in fix(dacs-directory): contain malformed artifacts in the directory indexer #93's release notes.

🤖 Generated with Claude Code

random block and others added 4 commits October 9, 2026 18:25
… fair retries

- Keep every revocation candidate per listing; 16 is now a per-pass
  verification budget (verified markers, then owner-anchored candidates,
  then arrival order). Unreadable candidates rotate; failed ones are
  re-examined only after a new write.
- Key discovered deals by the bundle's buyer plus jobId in the scanner
  and persisted scan state (rekeyed in place on load, schema stays v9);
  /api/dacs/deal-owners searches across owners with the same response.
- Count rejections and transient failures separately (rejection_count,
  deferred_at migrated in place); transient retries first, at most 20
  rejected reads per pass, 1,000 scheduled rejected locators with
  rotation.
- Check registration binding counts before verifying, verify bindings one
  at a time, bound producer-side resolver reads, and report duplicate
  owner/name programs as indeterminate (artifact route answers 409).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…jobId, separate retry classes

Revocation candidates move into an indexed SQLite table, so each pass
does a fixed amount of work per listing however long the queue is.
Discovered deals are keyed by the buyer bundle's storage owner and the
jobId in the scanner, scan state, catalog and deduplication, so two
owners' deals with the same jobId stay separate. Rejected content and
transient read failures are scheduled as separate retry classes, with a
reserved share of retry reads and a ceiling on scheduled rejections.
Bundle-binding discovery is bounded per signer, as DACS-5 BB-6 requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r parties

An entry counts towards a deal's owners only when each bundle ref it
carries is owned by, or bound through a verified BundleBinding to, the
party it is reported for. A seller copy is attached to a discovered deal
only when the deal has an attributed seller who owns that copy, and the
lookup accepts an optional bundleRef to narrow it to one deal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… verified or scanned

A catalog entry counts only when it sits in its own seller's record and
either the indexer verified both bundle copies against its parties, or
it matches a scanned entry as indexed. A scanned entry already in the
catalog counts once, whichever bundle ref is requested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@randomblocker randomblocker changed the title fix(dacs-directory): bounded verification work, owner-keyed deals and fair retries fix(dacs-directory): bounded revocation work, owner-scoped deals and separate retry classes Oct 10, 2026
@randomblocker
randomblocker marked this pull request as ready for review October 10, 2026 13:10
@randomblocker
randomblocker merged commit 831082f into main Oct 10, 2026
1 check passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T13:11:26.060745Z 38f2f6c Draft marked ready
🔒 Security Review ✅ Completed 2026-10-10T13:12:36.795856Z 38f2f6c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

randomblocker added a commit that referenced this pull request Oct 13, 2026
Brings #92-#94 (bounded verification work, owner-keyed deals, malformed
artifact containment) under #85's SDK-aligned listing admission.

Conflicts, resolved semantically:
- store.ts: keep both startup migrations (#85 listing_rejections
  coordinates; #94 scan_runs omission counters, rejected-retry classes,
  revocation_candidates table).
- chain.ts: keep #94's boundedJson guard on storage reads and #85's
  extended readback fields (storageAddress, metadata, tx refs).
- indexer.ts: #85's admission (verifyListingResult with rejection
  diagnostics, validity window, `revocation`, transactionReadiness) over
  #94's bounded, owner-first revocation candidate batch and progress.
- reindexCore.ts: #94's design (candidates live in SQLite, not scan
  state) with the public `revocation` field name.

Semantic fixes the clean auto-merge did not cover:
- verifyListingResult fails closed instead of throwing, keeping #94's
  "no canonical form" containment for every caller.
- verifyListingRevocation: the pinned SDK's stripSignature now rejects
  non-canonical JSON, so it moves inside #94's existing guard.
- scan.ts: SDK f68fed1 renamed the MVP agreement predicate; use
  isLegacyMvpAgreementDocument (byte-identical to 44d8ff2's
  isAgreementDocument that #94 was written against).
- #94 tests: legacy listing fixtures use the SDK legacy-MVP string
  signature, current fixtures carry a DPA-1 verificationMethod, the
  revocation field is `revocation`, and the depth sweep follows the
  pinned SDK's 64-level canonical bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
randomblocker added a commit that referenced this pull request Oct 15, 2026
…10.5.1)

DACS-5 §10.5.1 says the deriver "MUST collapse the input to one
authoritative bundle per jobId before partitioning". Canonically equal
copies collapse, a fully signed copy takes precedence over lesser-signed
divergent ones, and equal standing voids the jobId (BB-6). #94 keyed the
dedupe on (buyer, jobId), so two buyers' fully signed completed copies for
one seller and jobId counted twice. A conforming deriver gives zero.

- dedupeVerifiedDeals keeps one record per bundle ref and per (jobId,
  bundleContentHash). Among divergent copies of a jobId it keeps a lone
  fully signed one; any other verified copy stays listed with
  reputationEligible false.
- DealRecord gains fullSignatureStanding, set from the BB-6 inspection on
  the current path and from the SDK signature verdicts on the legacy path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
randomblocker added a commit that referenced this pull request Oct 15, 2026
…otes

- README: name the Standard, SDK and node revisions the verdicts depend on
  and the node's storage-read contract; list ARTIFACT_REJECTED and its retry
  class; describe the binding ceiling as it is since #94 (32 per signer of
  each job and role, no total across signers).
- SPEC_PIN: the did:demos:agent profile is Ed25519, so listings signed with
  ecdsa-secp256k1 or sr1-aggregate are not verified.
- The SDK repository is public; the token is only for a private mirror
  (README, setup-sdk.sh, CI).
- The sessionAnchorName import comments: upstream exported it after the
  pinned SDK_REV.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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