Skip to content

feat: zero-knowledge run attestation + static no-recursion gate#474

Open
Solizardking wants to merge 1 commit into
Gitlawb:mainfrom
Solizardking:feat/zk-attestation
Open

feat: zero-knowledge run attestation + static no-recursion gate#474
Solizardking wants to merge 1 commit into
Gitlawb:mainfrom
Solizardking:feat/zk-attestation

Conversation

@Solizardking

@Solizardking Solizardking commented Jul 4, 2026

Copy link
Copy Markdown

Add internal/attest — a stdlib-only package that makes "Zero" literal:

  • Hash-chained transcripts: every run event folds into a SHA-256 chain whose head is a 32-byte payload commitment; JSONL export re-verifies offline with VerifyJSONL, and any tampering is detected.
  • Nullifiers bit-compatible with clawd-zk (@clawd/zk-client computeNullifier): SHA-256(secret ‖ context ‖ nonce_u64le), giving one-shot replay protection when published on Solana via publish_attestation (~$0.005/run via compressed PDAs).
  • Attestation artifact carrying exactly the four public inputs the clawd-zk program verifies: attester, modelHash, payloadCommitment, nullifier. The chain learns that a run happened, which model set produced it, and that it happened exactly once — never prompts, tool calls, or outputs.
  • Static no-recursion gate: norecursion_test.go builds the package's intra-package call graph on every go test and fails on any direct or mutual recursion.

Announce the capability at the top of the README and link docs/ATTESTATION.md from the documentation index.

Summary

Linked issue

Fixes #

Checklist

  • The linked issue already has the issue-approved label.
  • go build ./..., go vet ./..., and go test ./... pass locally.
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant).
  • UI changes include screenshots or a short recording where possible.

Summary by CodeRabbit

  • New Features

    • Added zero-knowledge run attestation support, including transcript tracking, verification, and attestation generation.
    • Added replay protection with one-time nullifiers and support for publishing attestation commitments.
  • Documentation

    • Added a new attestation guide and updated the main README with a prominent overview and reference link.
  • Tests

    • Added coverage for transcript consistency, verification, nullifier behavior, model-set normalization, and attestation output.
    • Added a safeguard to prevent recursive call patterns in the attestation logic.

Add internal/attest — a stdlib-only package that makes "Zero" literal:

- Hash-chained transcripts: every run event folds into a SHA-256 chain
  whose head is a 32-byte payload commitment; JSONL export re-verifies
  offline with VerifyJSONL, and any tampering is detected.
- Nullifiers bit-compatible with clawd-zk (@clawd/zk-client
  computeNullifier): SHA-256(secret ‖ context ‖ nonce_u64le), giving
  one-shot replay protection when published on Solana via
  publish_attestation (~$0.005/run via compressed PDAs).
- Attestation artifact carrying exactly the four public inputs the
  clawd-zk program verifies: attester, modelHash, payloadCommitment,
  nullifier. The chain learns that a run happened, which model set
  produced it, and that it happened exactly once — never prompts,
  tool calls, or outputs.
- Static no-recursion gate: norecursion_test.go builds the package's
  intra-package call graph on every go test and fails on any direct
  or mutual recursion.

Announce the capability at the top of the README and link
docs/ATTESTATION.md from the documentation index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a new internal/attest Go package implementing zero-knowledge run attestation: a SHA-256 hash-chained transcript, nullifiers for replay protection, and attestation artifact generation. Includes unit tests, a static no-recursion call-graph gate test, and README/docs updates.

Changes

ZK Run Attestation

Layer / File(s) Summary
Transcript, nullifier, and attestation core
internal/attest/attest.go
Adds Record/Transcript types with SHA-256 hash-chain append, commitment, JSONL write/save/verify; Nullifier/NullifierWithNonce with secret-length validation; Attestation struct, ModelSetID canonicalization, and (*Transcript).Attest to build attestations.
Tests and recursion gate
internal/attest/attest_test.go, internal/attest/norecursion_test.go
Adds tests for transcript round-trip/tamper detection, nullifier correctness, attestation fields, and ModelSetID; adds TestZeroRecursion that parses the package AST and detects call-graph cycles via iterative DFS.
README and attestation docs
README.md, docs/ATTESTATION.md
Adds an announcement section and documentation link in README, and a new docs/ATTESTATION.md describing the attestation system, privacy model, and flow.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Transcript
  participant Nullifier
  participant Attestation

  Caller->>Transcript: Append(kind, taskID, payload)
  Transcript-->>Transcript: update SHA-256 hash chain
  Caller->>Transcript: Attest(secret, context, modelID)
  Transcript->>Nullifier: Nullifier(secret, context)
  Nullifier-->>Transcript: nullifier hash
  Transcript->>Attestation: build Attestation struct
  Attestation-->>Caller: Attestation (commitment, nullifier, modelHash)
Loading

Suggested reviewers: Vasanthdev2004

A little chain of hashes, link by link,
Nullifiers born so no run repeats, I think.
No recursion allowed — the AST-cop checks,
Tests all pass, docs updated, no more hex.
Hop along, reviewer — this rabbit approves the flow! 🐇🔗

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main additions: run attestation and a static no-recursion gate.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments, description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/attest/norecursion_test.go (1)

28-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Call-graph nodes are keyed by bare function name, ignoring receivers.

Methods and funcs are merged into the same namespace by fd.Name.Name alone (acknowledged in the comment as "conservative"). If the package ever grows two distinct types with same-named methods (e.g., two String() methods, one calling the other type's helper of the same name), this could produce false-positive cycles unrelated to actual recursion. Not an issue with the current single-type package, but worth keeping in mind as the package grows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/attest/norecursion_test.go` around lines 28 - 86, The call graph in
norecursion_test.go is collapsing methods and functions into the same bucket by
using only fd.Name.Name, which can create false recursion matches when different
receivers share a method name. Update the graph-building logic around the fn
struct and the fd.Name.Name/declared lookup to use a receiver-qualified symbol
name for methods while keeping plain names for funcs, and apply the same symbol
scheme when collecting callees so graph keys remain unique.
internal/attest/attest.go (1)

60-76: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

No concurrency guard on Transcript.

Append mutates t.head/t.records without synchronization. If multiple goroutines ever append concurrently (e.g., tool calls run in parallel and each appends its own event), this races. Given "run" semantics probably imply single-goroutine sequential appends today, this is speculative, but worth a doc comment noting the type is not safe for concurrent use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/attest/attest.go` around lines 60 - 76, Transcript.Append mutates
Transcript.head and Transcript.records without any synchronization, so the
Transcript type should be documented as not safe for concurrent use unless you
plan to add locking. Add a clear doc comment on Transcript and/or Append stating
it must only be used sequentially by one goroutine, and if concurrent appends
are intended, introduce a mutex around the Append mutation path in
Transcript.Append.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/attest/attest.go`:
- Around line 187-231: The Attestation schema and Transcript.Attest()
implementation are missing the attester field that docs/ATTESTATION.md describes
as a public input. Update Attestation and Attest() to either include and
populate attester explicitly, or make the serialization/docs consistently state
that attester is provided elsewhere. Use the existing Attestation struct,
ModelSetID, and Transcript.Attest symbols to locate the change, and keep the
JSON/public-input shape aligned with publish_attestation.

---

Nitpick comments:
In `@internal/attest/attest.go`:
- Around line 60-76: Transcript.Append mutates Transcript.head and
Transcript.records without any synchronization, so the Transcript type should be
documented as not safe for concurrent use unless you plan to add locking. Add a
clear doc comment on Transcript and/or Append stating it must only be used
sequentially by one goroutine, and if concurrent appends are intended, introduce
a mutex around the Append mutation path in Transcript.Append.

In `@internal/attest/norecursion_test.go`:
- Around line 28-86: The call graph in norecursion_test.go is collapsing methods
and functions into the same bucket by using only fd.Name.Name, which can create
false recursion matches when different receivers share a method name. Update the
graph-building logic around the fn struct and the fd.Name.Name/declared lookup
to use a receiver-qualified symbol name for methods while keeping plain names
for funcs, and apply the same symbol scheme when collecting callees so graph
keys remain unique.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9fa7981b-8834-4083-b7e5-4cc0ee41f220

📥 Commits

Reviewing files that changed from the base of the PR and between 949ee43 and 3eca8b6.

📒 Files selected for processing (5)
  • README.md
  • docs/ATTESTATION.md
  • internal/attest/attest.go
  • internal/attest/attest_test.go
  • internal/attest/norecursion_test.go

Comment thread internal/attest/attest.go
Comment on lines +187 to +231
type Attestation struct {
Schema string `json:"schema"`
Context string `json:"context"`
ModelHash string `json:"modelHash"`
PayloadCommitment string `json:"payloadCommitment"`
Nullifier string `json:"nullifier"`
Events int `json:"events"`
CreatedAt string `json:"createdAt"`
}

// ModelSetID canonicalizes a set of model IDs (dedupe, sort, join) so
// the same model set always hashes to the same modelHash regardless of
// which model won which turn.
func ModelSetID(models []string) string {
seen := make(map[string]bool, len(models))
uniq := make([]string, 0, len(models))
for _, m := range models {
if m != "" && !seen[m] {
seen[m] = true
uniq = append(uniq, m)
}
}
sort.Strings(uniq)
return strings.Join(uniq, ",")
}

// Attest builds the attestation for a finished transcript.
// modelID is hashed (SHA-256) so the chain learns *which* model class ran
// without the transcript; secret must be >=16 bytes of private material.
func (t *Transcript) Attest(secret []byte, context, modelID string) (*Attestation, error) {
null, err := Nullifier(secret, context)
if err != nil {
return nil, err
}
modelHash := sha256.Sum256([]byte(modelID))
return &Attestation{
Schema: attestationSchema,
Context: context,
ModelHash: "0x" + hex.EncodeToString(modelHash[:]),
PayloadCommitment: t.CommitmentHex(),
Nullifier: "0x" + hex.EncodeToString(null[:]),
Events: len(t.records),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== internal/attest/attest.go ==\n'
wc -l internal/attest/attest.go
sed -n '1,280p' internal/attest/attest.go

printf '\n== docs/ATTESTATION.md ==\n'
wc -l docs/ATTESTATION.md
sed -n '1,260p' docs/ATTESTATION.md

printf '\n== search attester / publish_attestation ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'attester|publish_attestation|Attestation struct|Attest\(' .

Repository: Gitlawb/zero

Length of output: 10429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== attestation usages ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'ModelHash|PayloadCommitment|Nullifier|CreatedAt|Schema|Context|Attestation\{' internal docs README.md

printf '\n== internal/daemon/remote/auth.go ==\n'
wc -l internal/daemon/remote/auth.go
sed -n '1,220p' internal/daemon/remote/auth.go

printf '\n== internal/attest/attest_test.go ==\n'
wc -l internal/attest/attest_test.go
sed -n '1,220p' internal/attest/attest_test.go

Repository: Gitlawb/zero

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== attester references ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' '\battester\b|\bAttester\b' internal docs README.md

printf '\n== attestation tests ==\n'
sed -n '1,220p' internal/attest/attest_test.go

printf '\n== any publish_attestation binding code ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'publish_attestation|payloadCommitment|nullifier|modelHash|attestation' internal docs README.md | head -n 200

Repository: Gitlawb/zero

Length of output: 6786


Align the attestation schema with the documented public inputs Attestation here omits attester, but docs/ATTESTATION.md says publish_attestation consumes it as a public input. Either add it to Attestation/Attest() or update the docs and downstream serialization to make it explicit that attester is supplied elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/attest/attest.go` around lines 187 - 231, The Attestation schema and
Transcript.Attest() implementation are missing the attester field that
docs/ATTESTATION.md describes as a public input. Update Attestation and Attest()
to either include and populate attester explicitly, or make the
serialization/docs consistently state that attester is provided elsewhere. Use
the existing Attestation struct, ModelSetID, and Transcript.Attest symbols to
locate the change, and keep the JSON/public-input shape aligned with
publish_attestation.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Link an approved parent issue before continuing this PR
    PR description
    The PR still has the template placeholder Fixes # and the approved-issue checklist item is unchecked, but CONTRIBUTING.md says community PRs must be tied to an existing issue with the issue-approved label and that PRs opened before that approval may be closed without review. Please link the approved parent issue, or move this proposal back to an issue for maintainer approval before continuing implementation review.

  • [P2] Do not announce live attestation support before it is wired into Zero
    README.md:17
    The README now says "Zero can now prove an agent run happened" and describes per-run Solana replay protection, but the only references to internal/attest outside the new package are the README and the new doc. No agent runtime, transcript/session, CLI, or publish path actually records events into this transcript or produces/publishes an attestation during a Zero run. This turns an unused internal helper into a top-of-README product claim, so users will expect a capability the application does not provide. Please either wire the attestation flow into the actual run path, or keep the docs scoped to an experimental/internal package instead of announcing it as supported Zero behavior.

  • [P2] Complete CodeRabbit's request to include the attester public input
    internal/attest/attest.go:187
    CodeRabbit's schema request is still valid: docs/ATTESTATION.md says publish_attestation has four public inputs, including attester, but the exported Attestation struct and Transcript.Attest return only modelHash, payloadCommitment, and nullifier plus metadata. A caller following the documented artifact cannot construct the documented public-input set from this API. Please either include and populate attester in the artifact/API, or change the docs and serialization contract to make it explicit that attester is supplied somewhere else.

  • [P2] Make Attest generate a per-run nullifier
    internal/attest/attest.go:216
    Transcript.Attest always calls Nullifier(secret, context) without a nonce or any run-specific value, while the docs example passes the constant context "zero/run/v1". That means two different runs using the same secret and documented context produce the same nullifier, so the first published attestation would make every later run look like a replay instead of providing one-shot replay protection per run. Please thread a nonce/run identifier into Attest or derive the nullifier from a unique run-specific value, and update the docs/tests so the replay-protection claim matches the API.

@Solizardking

Copy link
Copy Markdown
Author

Perfect!!

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.

2 participants