mergedhumanPR #66Trust
trust: parc — selective disclosure via Merkle inclusion proofs (stacked on #63)
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…
Author
@Sharathvc23
github profile →- Status
- Merged
- Merged on
- Jul 8
- Branch
- hackathon/stellarminds-ai-parc-selective-disclosure
Description
The pitch.
> **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.
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/66/head:pr-66
git checkout pr-66