Skip to content

trust: parc — selective disclosure via Merkle inclusion proofs (stacked on #63)#66

Draft
Sharathvc23 wants to merge 2 commits into
projnanda:mainfrom
Sharathvc23:hackathon/stellarminds-ai-parc-selective-disclosure
Draft

trust: parc — selective disclosure via Merkle inclusion proofs (stacked on #63)#66
Sharathvc23 wants to merge 2 commits into
projnanda:mainfrom
Sharathvc23:hackathon/stellarminds-ai-parc-selective-disclosure

Conversation

@Sharathvc23

Copy link
Copy Markdown
Contributor

Stacked on #63 — review the top commit only (123c8d9). The base commit here is #63's trust: parc unchanged; this PR will be rebased to a single commit once #63 merges.

What this adds

Selective disclosure for trust: parc credentials: a holder reveals only chosen receipts from their ledger, each with a Merkle inclusion proof against the credential's signed behavioral_merkle_root — a verifier confirms every revealed receipt is genuinely part of the committed history without seeing the rest of the ledger.

  • inclusion_proof(receipts, *, receipt) / verify_inclusion(receipt, proof, root) — proofs over the exact tree merkle_root already builds (lexicographically sorted leaf digests, odd-node duplication). Stdlib-only, no new dependencies.
  • ParcTrust.build_presentation(credential, receipts, *, disclose){credential, disclosed: [{receipt, proof}]}, byte-deterministic.
  • ParcTrust.verify_presentation(presentation, *, identity, expected_issuer=None) — verifies the credential's Ed25519 proof (same as-of key-window path as admit()), each inclusion proof, and that every proof's leaf_count equals the signed receipt_count (the credential's own bound on undisclosed scope). Typed failure reasons: malformed_presentation, issuer_mismatch, bad_credential_proof, malformed_proof, count_mismatch, not_included.

What it deliberately does NOT do

Disclosure proves which receipts happened. It does not recompute reputation scores — score aggregation needs the whole receipt graph (collusion-ring severance requires every edge), so the signed aggregate from issuance stands. The two layers stay separate, matching the recomputing-gate philosophy of #63: membership is provable piecewise; standing is not.

Scope note / roadmap

The #63 credential embeds the full receipts list in the signed document (the recomputing admit() gate consumes it), so presentations are not receipts-private until a receipts-free credential variant exists. verify_presentation deliberately never reads the embedded list — it verifies from root + signed count only — so such a variant (a small build_credential option, left out here to avoid touching #63's signed format) will verify unchanged. Happy to send that as a follow-up if there's interest.

Tests

make ci-local green: 777 → 797 (+20: tree/proof round-trips incl. a Hypothesis property over random ledgers, tamper/forgery/wrong-ledger/count-lie adversarial cases, byte-determinism). Demo:

ledger receipts:        20
disclosed:              ['r03', 'r07', 'r11']
verify (genuine):       ok=True reasons=()
verify (tampered):      ok=False reasons=('not_included',)

Doc: docs/layers/trust.md §parc gains a "Selective disclosure" subsection.

…ssion gate

Reputation in NEST dies with the run: no plugin exports it, carries it, or
verifies it in another trust domain. This adds the parc trust plugin
(extending the merged agent_receipts receipt maths), the parc_migration
two-domain scenario, and six adversarial validators.

- Export: an agent's receipt ledger becomes a W3C-VC-shaped credential
  (behavioral_merkle_root + nanda-rep/0.2 scores as fixed 6-decimal strings),
  signed through the ed25519_rotating identity layer with key_id pinned in
  proof.verificationMethod (vendored did:key encoder, stdlib-only).
- Admission: the gate recomputes the root and scores from the carried
  receipts instead of trusting signed claims — an issuer-inflated,
  genuinely re-signed score is rejected (score_mismatch). A valid
  signature is not admission.
- Cross-layer: proofs are checked against the issuer's key-rotation
  window as-of the credential's validFrom tick (stale_key), using genuine
  rotation records replayed onto the gate.
- Ring severance at the border: an inline single-subject ledger is a star
  graph and cannot show an N-party ring, so the gate re-runs whole-graph
  collusion severance over the originating domain's published ledger
  (severed_below_threshold).
- Differential proof: with task.config.naive_gate (signed claims trusted)
  the inflation + ring validators FAIL; under trust: score_average no
  credentials exist and every validator fails.

Registered in _BUILTINS and as a real nest.plugins.trust entry point.
41 new tests (hostile paths per typed reason, Hypothesis properties,
5-seed e2e, byte-identical determinism). make ci-local green.
A holder can now reveal a chosen subset of receipts instead of the whole
ledger: inclusion_proof/verify_inclusion build and check authentication
paths over the exact tree merkle_root computes (lexicographically sorted
hex leaves, hex-concat parents, odd-node duplication), so proofs verify
against the behavioral_merkle_root a credential already signs.

ParcTrust.build_presentation packages the credential plus per-receipt
proofs; ParcTrust.verify_presentation checks the credential's Ed25519
proof through the same as-of key-window path as admit(), each disclosed
receipt's membership under the signed root, and each proof's leaf_count
against the signed receipt_count — the bound on the undisclosed
remainder. Typed reasons throughout (bad_credential_proof, not_included,
count_mismatch, malformed_proof, issuer_mismatch,
malformed_presentation). Scores are deliberately never recomputed from a
disclosed subset: the aggregate is a whole-graph property, so the signed
value stands and full-ledger recomputation remains admit()'s job.
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