diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a15b0e..0080884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable implementation and protocol changes are recorded here. ### Added +- Reproducible private-witness evidence for a two-transition `TOOL_TRACE` + scenario: deterministic public commitments, a sequence-2-only auditor bundle, + privacy-boundary assertions, and a Foundry integration test covering relayed + EIP-712 authorization, selective `AuditGrant`, tamper rejection, and auditor + acknowledgement. - Provisional external-review intake and reviewer follow-up for G4, plus focused regression tests proving ERC-1271 `STATICCALL` blocks state-changing callbacks and invalid ECDSA `v` values are rejected. diff --git a/README.md b/README.md index e1c8a0d..a70c924 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ prevStateRoot + ExperienceDelta v1 - `docs/interop/fixture-space.md`: Sepolia 公开 fixture Space(4 条自验证转换 + 全公开见证,供外部 conformance checker 使用) - `docs/interop/attestation-refs.md`: 与 WYRIWE(ERC-8299)联合的 attestation_refs 互操作说明(informative,双向零依赖) - `docs/interop/external-reproduction.md`: R-002 外部独立复现证据、固定来源与可执行验证方法 +- `evidence/private-witness/README.md`: TOOL_TRACE 私有输入 → 承诺 → 授权 transition → `[2,2]` selective audit 的端到端证据 - `docs/roadmap/README.md`: 路线图(Deployed / Specified / Direction 三级诚实标注,含 ZK profile 方向) - `docs/whitepaper.md`: Agent Memory State 白皮书(英文,ERC 配套叙事文档,含 Open Questions,参考以太坊白皮书格式) - `docs/whitepaper.zh.md`: 白皮书中文版 @@ -99,6 +100,12 @@ cd contracts && forge fmt --check && forge test node scripts/external-reproduction-gate.mjs ``` +运行只披露一个 transition 的私有 witness 端到端演示: + +```bash +pnpm demo:private-witness +``` + ## 社区提交前还需完成 1. 在 Ethereum Magicians 发布问题定义和最小接口讨论帖。 diff --git a/contracts/test/integration/PrivateWitnessEvidence.t.sol b/contracts/test/integration/PrivateWitnessEvidence.t.sol new file mode 100644 index 0000000..c34bf8e --- /dev/null +++ b/contracts/test/integration/PrivateWitnessEvidence.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {AuditGrant} from "../../src/extensions/AuditGrant.sol"; +import {IAgentMemoryState} from "../../src/interfaces/IAgentMemoryState.sol"; +import {AgentMemoryStateRegistry} from "../../src/reference/AgentMemoryStateRegistry.sol"; +import {PrivateCommitment} from "../../src/reference/PrivateCommitment.sol"; + +contract PrivateWitnessEvidenceTest is Test { + uint256 internal constant CONTROLLER_PK = 0xA11CE; + uint256 internal constant ATTACKER_PK = 0xB0B; + address internal constant RELAYER = address(0xBEEF); + + AgentMemoryStateRegistry internal registry; + AuditGrant internal auditGrant; + string internal publicEvidence; + string internal auditorDisclosure; + address internal controller; + address internal auditor; + bytes32 internal spaceId; + + function setUp() public { + registry = new AgentMemoryStateRegistry(); + auditGrant = new AuditGrant(IAgentMemoryState(address(registry))); + publicEvidence = vm.readFile( + string.concat( + vm.projectRoot(), "/../test-vectors/private-witness/tool-trace-v1.public.json" + ) + ); + auditorDisclosure = vm.readFile( + string.concat( + vm.projectRoot(), "/../test-vectors/private-witness/tool-trace-v1.auditor.json" + ) + ); + controller = vm.parseJsonAddress(publicEvidence, ".onChain.space.controller"); + auditor = vm.parseJsonAddress(publicEvidence, ".onChain.auditGrant.auditor"); + spaceId = _publicBytes32(".onChain.space.spaceId"); + assertEq(controller, vm.addr(CONTROLLER_PK), "fixture controller/key mismatch"); + } + + function test_PrivateInputToAuthorizedTransitionToSelectiveAudit() public { + _registerSpaceThroughRelayer(); + _assertWrongAuthorizerRejected(); + _commitPublicTransition(0); + _commitPublicTransition(1); + _assertPublicHead(); + + bytes32 witnessSetRoot = _verifySequenceTwoDisclosure(); + _grantAndAcknowledge(witnessSetRoot); + } + + function _registerSpaceThroughRelayer() private { + bytes32 spaceSalt = _publicBytes32(".onChain.space.spaceSalt"); + address authorizer = vm.parseJsonAddress(publicEvidence, ".onChain.space.authorizer"); + assertEq(registry.deriveSpaceId(controller, spaceSalt), spaceId); + + bytes32 registrationId = registry.hashSpaceRegistration(spaceId, controller, authorizer); + bytes memory signature = _sign(CONTROLLER_PK, registrationId); + vm.prank(RELAYER); + registry.registerSpace(spaceId, controller, authorizer, spaceSalt, signature); + + (address storedController, address storedAuthorizer, uint64 nonce) = + registry.spaceAuthorization(spaceId); + assertEq(storedController, controller); + assertEq(storedAuthorizer, authorizer); + assertEq(nonce, 0); + } + + function _assertWrongAuthorizerRejected() private { + IAgentMemoryState.ExperienceDelta memory delta = _publicDelta(0); + bytes32 transitionId = registry.hashExperienceDelta(delta); + bytes memory signature = _sign(ATTACKER_PK, transitionId); + + vm.prank(RELAYER); + vm.expectRevert(AgentMemoryStateRegistry.InvalidAuthorization.selector); + registry.commitTransition(delta, signature); + } + + function _commitPublicTransition(uint256 index) private { + IAgentMemoryState.ExperienceDelta memory delta = _publicDelta(index); + string memory base = _transitionBase(index); + bytes32 expectedId = _publicBytes32(string.concat(base, ".transitionId")); + bytes32 expectedRoot = _publicBytes32(string.concat(base, ".nextStateRoot")); + assertEq(registry.hashExperienceDelta(delta), expectedId); + + vm.prank(RELAYER); + (bytes32 transitionId, bytes32 nextStateRoot) = + registry.commitTransition(delta, _sign(CONTROLLER_PK, expectedId)); + assertEq(transitionId, expectedId); + assertEq(nextStateRoot, expectedRoot); + } + + function _assertPublicHead() private view { + (bytes32 transitionId, bytes32 stateRoot, uint64 sequence) = registry.head(spaceId); + assertEq(transitionId, _publicBytes32(".onChain.head.transitionId")); + assertEq(stateRoot, _publicBytes32(".onChain.head.stateRoot")); + assertEq(sequence, uint64(vm.parseUint(_publicString(".onChain.head.sequence")))); + } + + function _verifySequenceTwoDisclosure() private view returns (bytes32 witnessSetRoot) { + IAgentMemoryState.ExperienceDelta memory delta = _publicDelta(1); + assertEq(_disclosureString(".items[0].witness.sequence"), "2"); + assertEq( + _disclosureBytes32(".items[0].witness.transitionId"), + _publicBytes32(".onChain.transitions[1].transitionId") + ); + + _assertDisclosedCommitments(delta); + + string memory witnessUtf8 = _disclosureString(".items[0].encoded.witnessUtf8"); + bytes32 witnessHash = keccak256(bytes(witnessUtf8)); + witnessSetRoot = auditGrant.foldWitnessRoot( + bytes32(0), _publicBytes32(".onChain.transitions[1].transitionId"), witnessHash + ); + assertEq(witnessSetRoot, _publicBytes32(".onChain.auditGrant.witnessSetRoot")); + + bytes32 tamperedHash = keccak256(bytes(string.concat(witnessUtf8, " "))); + bytes32 tamperedRoot = auditGrant.foldWitnessRoot( + bytes32(0), _publicBytes32(".onChain.transitions[1].transitionId"), tamperedHash + ); + assertTrue(tamperedRoot != witnessSetRoot); + } + + function _assertDisclosedCommitments(IAgentMemoryState.ExperienceDelta memory delta) + private + view + { + string memory payloadUtf8 = _disclosureString(".items[0].encoded.payloadUtf8"); + bytes32 deltaSalt = _disclosureBytes32(".items[0].witness.deltaSalt"); + assertEq( + PrivateCommitment.computeDelta(bytes(payloadUtf8), deltaSalt, delta.profileId), + delta.deltaCommitment + ); + assertTrue( + PrivateCommitment.computeDelta( + bytes(string.concat(payloadUtf8, " ")), deltaSalt, delta.profileId + ) != delta.deltaCommitment + ); + + assertEq( + PrivateCommitment.computeProvenance( + bytes(_disclosureString(".items[0].encoded.provenanceUtf8")), + _disclosureBytes32(".items[0].witness.provenanceSalt") + ), + delta.provenanceCommitment + ); + assertEq( + PrivateCommitment.computeLocator( + bytes(_disclosureString(".items[0].witness.locator")), + _disclosureBytes32(".items[0].witness.locatorSalt") + ), + delta.locatorCommitment + ); + } + + function _grantAndAcknowledge(bytes32 witnessSetRoot) private { + uint64 fromSequence = + uint64(vm.parseUint(_publicString(".onChain.auditGrant.fromSequence"))); + uint64 toSequence = uint64(vm.parseUint(_publicString(".onChain.auditGrant.toSequence"))); + bytes32 expectedGrantId = _publicBytes32(".onChain.auditGrant.grantId"); + assertEq( + auditGrant.deriveGrantId(spaceId, auditor, fromSequence, toSequence), expectedGrantId + ); + + vm.warp(1_800_000_000); + vm.prank(controller); + bytes32 grantId = + auditGrant.grant(spaceId, auditor, fromSequence, toSequence, witnessSetRoot); + assertEq(grantId, expectedGrantId); + + string memory witnessUtf8 = _disclosureString(".items[0].encoded.witnessUtf8"); + bytes32 tamperedRoot = auditGrant.foldWitnessRoot( + bytes32(0), + _publicBytes32(".onChain.transitions[1].transitionId"), + keccak256(bytes(string.concat(witnessUtf8, " "))) + ); + vm.prank(auditor); + vm.expectRevert(AuditGrant.RootMismatch.selector); + auditGrant.acknowledge(spaceId, grantId, tamperedRoot); + + vm.prank(auditor); + auditGrant.acknowledge(spaceId, grantId, witnessSetRoot); + assertGt(auditGrant.grantOf(grantId).acknowledgedAt, 0); + } + + function _publicDelta(uint256 index) + private + view + returns (IAgentMemoryState.ExperienceDelta memory) + { + string memory base = _transitionBase(index); + return IAgentMemoryState.ExperienceDelta({ + spaceId: _publicBytes32(string.concat(base, ".spaceId")), + sequence: uint64(vm.parseUint(_publicString(string.concat(base, ".sequence")))), + prevStateRoot: _publicBytes32(string.concat(base, ".prevStateRoot")), + deltaCommitment: _publicBytes32(string.concat(base, ".deltaCommitment")), + provenanceCommitment: _publicBytes32(string.concat(base, ".provenanceCommitment")), + profileId: _publicBytes32(string.concat(base, ".profileId")), + locatorCommitment: _publicBytes32(string.concat(base, ".locatorCommitment")) + }); + } + + function _sign(uint256 privateKey, bytes32 structHash) private view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, registry.signingDigest(structHash)); + return abi.encodePacked(r, s, v); + } + + function _transitionBase(uint256 index) private pure returns (string memory) { + return string.concat(".onChain.transitions[", vm.toString(index), "]"); + } + + function _publicBytes32(string memory path) private view returns (bytes32) { + return vm.parseJsonBytes32(publicEvidence, path); + } + + function _publicString(string memory path) private view returns (string memory) { + return vm.parseJsonString(publicEvidence, path); + } + + function _disclosureBytes32(string memory path) private view returns (bytes32) { + return vm.parseJsonBytes32(auditorDisclosure, path); + } + + function _disclosureString(string memory path) private view returns (string memory) { + return vm.parseJsonString(auditorDisclosure, path); + } +} diff --git a/docs/architecture.md b/docs/architecture.md index 4275c96..eac2acb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -40,6 +40,10 @@ flowchart LR The private witness travels only between authorized off-chain systems. It is not needed by the registry to enforce ordering or authorization. +The executable [`private-witness evidence demo`](../evidence/private-witness/README.md) +shows this boundary with two `TOOL_TRACE` transitions and an audit disclosure +restricted to sequence range `[2,2]`. + ## Layer boundaries ### Core diff --git a/evidence/private-witness/README.md b/evidence/private-witness/README.md new file mode 100644 index 0000000..984638d --- /dev/null +++ b/evidence/private-witness/README.md @@ -0,0 +1,98 @@ +# Private-Witness End-to-End Evidence + +Status: **reproducible local evidence; synthetic data; not a production deployment** + +中文摘要:这个演示选择一个窄范围的 `TOOL_TRACE` 客服场景。控制者将两次 +私有工具调用转换为带盐承诺,由固定测试账户签署并通过 relayer 提交到真实的 +`AgentMemoryStateRegistry` 合约。随后,控制者只向指定 auditor 披露 sequence 2, +并通过 `AuditGrant` 的 `[2,2]` 范围完成校验和确认。sequence 1 的明文、盐和 +locator 不进入 auditor disclosure。 + +The demo commits two private tool calls, submits both transitions through the +Registry's real EIP-712 authorization path, and selectively discloses only +sequence `2` to one auditor. "Selective" means a selected transition range. It +does **not** mean field-level zero-knowledge disclosure: opening a transition's +baseline commitment reveals that transition's complete disclosed witness. + +## Reproduce + +Install the pinned workspace dependencies, then run: + +```bash +pnpm install --frozen-lockfile +pnpm demo:private-witness +``` + +A passing run prints these stable public identifiers: + +```text +Private-witness TOOL_TRACE demo: PASS +Space: 0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa +Authorized transitions: 2 +Auditor disclosure: sequence 2 only (range [2,2]) +Audit witnessSetRoot: 0x8ef3a2f2247a6049dc26fe48472e7373607e85b3d76aa4d50e156b80c3062f9d +``` + +The command performs two independent checks: + +1. `scripts/private-witness-demo.mjs` derives every commitment and public + identifier from the synthetic private fixture, compares the generated files + byte-for-byte, verifies the auditor opening, and rejects private-marker leaks. +2. `PrivateWitnessEvidence.t.sol` deploys the real Registry and `AuditGrant`, + rejects a wrong signer, accepts two relayed EIP-712 authorizations, recomputes + the disclosed witness, rejects a tampered audit root, and records the auditor + acknowledgement. + +## Evidence files + +| File | Role | Production visibility | +|---|---|---| +| `test-vectors/private-witness/tool-trace-v1.private.json` | Controller-side synthetic source: both tool payloads, commitment salts, provenance, and locators | Private; committed here only because all values are fake fixtures | +| `test-vectors/private-witness/tool-trace-v1.public.json` | Exact deterministic values modeled as Registry and `AuditGrant` public state/events | Public | +| `test-vectors/private-witness/tool-trace-v1.auditor.json` | Out-of-band disclosure containing sequence 2 only, plus exact canonical UTF-8 encodings | Auditor only | +| `scripts/private-witness-demo.mjs` | Generator, byte-for-byte verifier, privacy-boundary assertions, and tamper checks | Public implementation | +| `contracts/test/integration/PrivateWitnessEvidence.t.sol` | Independent Solidity execution of authorization, state transition, grant, and acknowledgement | Public test | + +## Boundary + +| Surface | Data present | +|---|---| +| Registry calldata/events/state | Space salt and ID; controller/authorizer; EIP-712 signatures in transaction calldata; sequence; prior/next roots; `deltaCommitment`; `provenanceCommitment`; public `profileId`; `locatorCommitment`; transition ID | +| `AuditGrant` calldata/events/state | Auditor address; selected range `[2,2]`; `witnessSetRoot`; grant ID; grant/acknowledgement timing | +| Controller private witness | Both raw tool arguments/results; payload, provenance, and locator salts; raw provenance; raw locators; exact witness encodings | +| Auditor disclosure | Only sequence 2 payload, provenance, locator, salts, and canonical witness bytes | +| Not disclosed to auditor | Sequence 1 email, customer record, locator, delta salt, and locator salt | + +The Space salt is public registration calldata and is not a content-commitment +salt. Content, provenance, and locator salts remain secret in production. + +The Node verifier scans `public.json` for fixture-private markers including the +email, customer/order/ticket identifiers, both locators, and every content salt. +It also verifies that `auditor.json` contains none of the sequence 1 markers. + +## Auditor-verifiable claims + +Given the public chain record and the private sequence 2 disclosure, the auditor +can verify that: + +- the disclosed canonical payload opens `deltaCommitment` under the public + `TOOL_TRACE` profile ID; +- disclosed provenance and locator open their respective commitments; +- the seven public Delta fields reproduce the transition ID; +- `prevStateRoot -> transitionId` reproduces `nextStateRoot`; +- the disclosed witness hash folds to the granted `witnessSetRoot` for `[2,2]`; +- the grant targets the stated Space, auditor, and range; and +- the Registry accepted the transition only after configured-authorizer approval. + +The auditor cannot verify the undisclosed sequence 1 witness from this bundle. +Nor does the demo prove that the tool really executed, its result was truthful, +the witness remains available, the auditor was competent, or any copy was +deleted. Those remain explicit protocol non-claims. + +## Fixture warning + +All payloads are fictional. The fixed controller private key appears only in the +Solidity test and has no authority or value. The salts are deliberately published +to make the result reproducible, which is the opposite of production practice. +Never adapt this demo by replacing fixture values with real memory and committing +the file to source control. diff --git a/package.json b/package.json index 2a4fad6..eba552c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "check:erc-assets": "node scripts/check-erc-assets.mjs", "check:vectors": "node scripts/check-vectors.mjs", "build:vectors": "node scripts/build-test-vectors-v2.mjs", + "check:private-witness": "node scripts/private-witness-demo.mjs", + "demo:private-witness": "pnpm --filter @erc-awar/core build && pnpm --filter @erc-awar/reference-engine build && pnpm check:private-witness && cd contracts && forge test --match-path test/integration/PrivateWitnessEvidence.t.sol -vv", "evidence:validate": "node scripts/external-reproduction-gate.mjs --validate-only", "evidence:test": "node --test scripts/external-reproduction-gate.test.mjs", "evidence:external": "node scripts/external-reproduction-gate.mjs", @@ -29,7 +31,7 @@ "security:test": "node --test scripts/security-rc-gate.test.mjs", "security:rc": "node scripts/security-rc-gate.mjs", "test": "pnpm test:ts && pnpm check:vectors && pnpm test:contracts", - "check": "pnpm clean && pnpm build && pnpm -r --if-present typecheck && pnpm -r --if-present test && pnpm check:erc-assets && pnpm check:vectors && pnpm evidence:validate && pnpm evidence:test && pnpm security:scope && pnpm security:test && pnpm format:check && pnpm test:contracts", + "check": "pnpm clean && pnpm build && pnpm -r --if-present typecheck && pnpm -r --if-present test && pnpm check:erc-assets && pnpm check:vectors && pnpm check:private-witness && pnpm evidence:validate && pnpm evidence:test && pnpm security:scope && pnpm security:test && pnpm format:check && pnpm test:contracts", "conformance:gate": "node scripts/conformance-gate.mjs", "conformance:clean": "node scripts/clean-checkout-conformance.mjs", "audit": "pnpm audit --audit-level=moderate", diff --git a/scripts/private-witness-demo.mjs b/scripts/private-witness-demo.mjs new file mode 100644 index 0000000..7b0014f --- /dev/null +++ b/scripts/private-witness-demo.mjs @@ -0,0 +1,372 @@ +#!/usr/bin/env node + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import { utf8ToBytes } from "@noble/hashes/utils"; +import { + ZERO32, + addressWord, + bytes32Word, + computeLocatorCommitment, + computeNextStateRoot, + computePrivateCommitment, + computeProvenanceCommitment, + computeTransitionId, + concatBytes, + deriveSpaceId, + keccak256, + keccak256Utf8, + uintWord, +} from "../packages/core/dist/index.js"; +import { stableStringify } from "../packages/reference-engine/dist/record.js"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const vectorDirectory = path.join(repositoryRoot, "test-vectors", "private-witness"); +const privatePath = path.join(vectorDirectory, "tool-trace-v1.private.json"); +const disclosurePath = path.join(vectorDirectory, "tool-trace-v1.auditor.json"); +const publicPath = path.join(vectorDirectory, "tool-trace-v1.public.json"); + +const FIXTURE_SCHEMA = "erc-8350/private-witness-fixture/v1"; +const DISCLOSURE_SCHEMA = "erc-8350/auditor-disclosure/v1"; +const WITNESS_SCHEMA = "erc-8350/private-witness-item/v1"; +const PUBLIC_SCHEMA = "erc-8350/private-witness-public-evidence/v1"; +const AUDIT_WITNESS_TYPE = + "AuditWitness(bytes32 prevRoot,bytes32 transitionId,bytes32 witnessHash)"; +const AUDIT_WITNESS_TYPEHASH = keccak256Utf8(AUDIT_WITNESS_TYPE); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, "utf8")); +} + +function jsonBytes(value) { + return JSON.stringify(value, null, 2) + "\n"; +} + +function assertExactFile(filePath, expected) { + const actual = readFileSync(filePath, "utf8"); + assert( + actual === jsonBytes(expected), + `${path.relative(repositoryRoot, filePath)} drifted; run this script with --write`, + ); +} + +function foldWitnessRoot(previousRoot, transitionId, witnessHash) { + return keccak256( + concatBytes( + bytes32Word(AUDIT_WITNESS_TYPEHASH), + bytes32Word(previousRoot), + bytes32Word(transitionId), + bytes32Word(witnessHash), + ), + ); +} + +function deriveGrantId(spaceId, auditor, fromSequence, toSequence) { + return keccak256( + concatBytes( + bytes32Word(spaceId), + addressWord(auditor), + uintWord(BigInt(fromSequence)), + uintWord(BigInt(toSequence)), + ), + ); +} + +function buildTransition(entry, profileId, spaceId, previousRoot) { + const payloadUtf8 = stableStringify(entry.payload); + const provenanceUtf8 = entry.provenance === null + ? null + : stableStringify(entry.provenance); + const delta = { + spaceId, + sequence: BigInt(entry.sequence), + prevStateRoot: previousRoot, + deltaCommitment: computePrivateCommitment( + utf8ToBytes(payloadUtf8), + entry.deltaSalt, + profileId, + ), + provenanceCommitment: provenanceUtf8 === null + ? ZERO32 + : computeProvenanceCommitment( + utf8ToBytes(provenanceUtf8), + entry.provenanceSalt, + ), + profileId, + locatorCommitment: computeLocatorCommitment(entry.locator, entry.locatorSalt), + }; + const transitionId = computeTransitionId(delta); + const nextStateRoot = computeNextStateRoot(previousRoot, transitionId); + return { + entry, + payloadUtf8, + provenanceUtf8, + delta, + transitionId, + nextStateRoot, + }; +} + +function publicTransition(record, authorizer) { + return { + spaceId: record.delta.spaceId, + sequence: record.delta.sequence.toString(10), + prevStateRoot: record.delta.prevStateRoot, + deltaCommitment: record.delta.deltaCommitment, + provenanceCommitment: record.delta.provenanceCommitment, + profileId: record.delta.profileId, + locatorCommitment: record.delta.locatorCommitment, + transitionId: record.transitionId, + nextStateRoot: record.nextStateRoot, + authorizer, + }; +} + +function disclosureItem(record, profileId) { + const witness = { + schema: WITNESS_SCHEMA, + spaceId: record.delta.spaceId, + sequence: record.delta.sequence.toString(10), + transitionId: record.transitionId, + profileId, + payload: record.entry.payload, + deltaSalt: record.entry.deltaSalt, + provenance: record.entry.provenance, + provenanceSalt: record.entry.provenanceSalt, + locator: record.entry.locator, + locatorSalt: record.entry.locatorSalt, + }; + return { + witness, + encoded: { + payloadUtf8: record.payloadUtf8, + provenanceUtf8: record.provenanceUtf8, + witnessUtf8: stableStringify(witness), + }, + }; +} + +function buildEvidence(fixture) { + assert(fixture.schema === FIXTURE_SCHEMA, "unsupported private fixture schema"); + assert(fixture.transitions.length === 2, "demo must contain exactly two transitions"); + assert( + fixture.transitions.every((entry, index) => entry.sequence === String(index + 1)), + "private transitions must be contiguous from sequence 1", + ); + + const profileId = keccak256Utf8(fixture.profileUri); + const spaceId = deriveSpaceId(fixture.space.controller, fixture.space.spaceSalt); + const records = []; + let previousRoot = ZERO32; + for (const entry of fixture.transitions) { + const record = buildTransition(entry, profileId, spaceId, previousRoot); + records.push(record); + previousRoot = record.nextStateRoot; + } + + const selected = new Set(fixture.disclosure.sequences); + const selectedRecords = records.filter((record) => + selected.has(record.delta.sequence.toString(10)) + ); + assert(selectedRecords.length === selected.size, "disclosure sequence is missing"); + const items = selectedRecords.map((record) => disclosureItem(record, profileId)); + const disclosure = { + _warning: + "SYNTHETIC OUT-OF-BAND DISCLOSURE. Production auditors receive this privately; it is committed here only so anyone can reproduce the demo.", + schema: DISCLOSURE_SCHEMA, + scenarioId: fixture.scenarioId, + range: { + fromSequence: fixture.disclosure.fromSequence, + toSequence: fixture.disclosure.toSequence, + }, + items, + }; + + let witnessSetRoot = ZERO32; + for (const item of items) { + const witnessHash = keccak256(utf8ToBytes(item.encoded.witnessUtf8)); + witnessSetRoot = foldWitnessRoot( + witnessSetRoot, + item.witness.transitionId, + witnessHash, + ); + } + const grantId = deriveGrantId( + spaceId, + fixture.auditor, + fixture.disclosure.fromSequence, + fixture.disclosure.toSequence, + ); + const publicEvidence = { + _notice: + "PUBLIC EVIDENCE ONLY. No raw payload, provenance, locator, salt, or witness hash belongs in this file or in calldata.", + schema: PUBLIC_SCHEMA, + scenarioId: fixture.scenarioId, + onChain: { + space: { + spaceId, + controller: fixture.space.controller, + authorizer: fixture.space.authorizer, + spaceSalt: fixture.space.spaceSalt, + }, + transitions: records.map((record) => + publicTransition(record, fixture.space.authorizer) + ), + head: { + transitionId: records.at(-1).transitionId, + stateRoot: records.at(-1).nextStateRoot, + sequence: records.at(-1).delta.sequence.toString(10), + }, + auditGrant: { + grantId, + auditor: fixture.auditor, + fromSequence: fixture.disclosure.fromSequence, + toSequence: fixture.disclosure.toSequence, + witnessSetRoot, + }, + }, + disclosurePolicy: { + disclosedSequences: fixture.disclosure.sequences, + undisclosedSequences: records + .map((record) => record.delta.sequence.toString(10)) + .filter((sequence) => !selected.has(sequence)), + }, + }; + return { disclosure, publicEvidence }; +} + +function verifyAuditorDisclosure(disclosure, publicEvidence) { + assert(disclosure.schema === DISCLOSURE_SCHEMA, "unsupported disclosure schema"); + const publicBySequence = new Map( + publicEvidence.onChain.transitions.map((transition) => [transition.sequence, transition]), + ); + let witnessSetRoot = ZERO32; + for (const item of disclosure.items) { + const transition = publicBySequence.get(item.witness.sequence); + assert(transition !== undefined, "disclosed transition is not public"); + assert( + stableStringify(item.witness.payload) === item.encoded.payloadUtf8, + "payload encoding mismatch", + ); + assert( + stableStringify(item.witness.provenance) === item.encoded.provenanceUtf8, + "provenance encoding mismatch", + ); + assert( + stableStringify(item.witness) === item.encoded.witnessUtf8, + "witness encoding mismatch", + ); + assert( + computePrivateCommitment( + utf8ToBytes(item.encoded.payloadUtf8), + item.witness.deltaSalt, + item.witness.profileId, + ) === transition.deltaCommitment, + "disclosed payload does not open deltaCommitment", + ); + assert( + computeProvenanceCommitment( + utf8ToBytes(item.encoded.provenanceUtf8), + item.witness.provenanceSalt, + ) === transition.provenanceCommitment, + "disclosed provenance does not open provenanceCommitment", + ); + assert( + computeLocatorCommitment(item.witness.locator, item.witness.locatorSalt) === + transition.locatorCommitment, + "disclosed locator does not open locatorCommitment", + ); + const transitionId = computeTransitionId({ + ...transition, + sequence: BigInt(transition.sequence), + }); + assert(transitionId === transition.transitionId, "transitionId mismatch"); + assert( + computeNextStateRoot(transition.prevStateRoot, transitionId) === + transition.nextStateRoot, + "nextStateRoot mismatch", + ); + const witnessHash = keccak256(utf8ToBytes(item.encoded.witnessUtf8)); + witnessSetRoot = foldWitnessRoot(witnessSetRoot, transitionId, witnessHash); + + const tamperedPayload = item.encoded.payloadUtf8 + " "; + assert( + computePrivateCommitment( + utf8ToBytes(tamperedPayload), + item.witness.deltaSalt, + item.witness.profileId, + ) !== transition.deltaCommitment, + "tampered payload unexpectedly opened the commitment", + ); + } + assert( + witnessSetRoot === publicEvidence.onChain.auditGrant.witnessSetRoot, + "disclosure does not match witnessSetRoot", + ); + assert( + deriveGrantId( + publicEvidence.onChain.space.spaceId, + publicEvidence.onChain.auditGrant.auditor, + disclosure.range.fromSequence, + disclosure.range.toSequence, + ) === publicEvidence.onChain.auditGrant.grantId, + "grantId mismatch", + ); +} + +function verifyBoundaries(fixture, disclosure, publicEvidence) { + const publicText = JSON.stringify(publicEvidence); + for (const marker of fixture.boundaryAssertions.neverPublic) { + assert(!publicText.includes(marker), `private marker leaked into public evidence: ${marker}`); + } + for (const field of [ + "deltaSalt", + "provenanceSalt", + "locatorSalt", + "payloadUtf8", + "provenanceUtf8", + "witnessUtf8", + "witnessHash", + ]) { + assert(!publicText.includes(field), `private field leaked into public evidence: ${field}`); + } + + const disclosureText = JSON.stringify(disclosure); + for (const marker of fixture.boundaryAssertions.notDisclosedToAuditor) { + assert( + !disclosureText.includes(marker), + `sequence 1 marker leaked into selective disclosure: ${marker}`, + ); + } + assert(disclosure.items.length === 1, "auditor must receive exactly one witness"); + assert(disclosure.items[0].witness.sequence === "2", "auditor received wrong sequence"); +} + +const fixture = readJson(privatePath); +const generated = buildEvidence(fixture); +if (process.argv.includes("--write")) { + mkdirSync(vectorDirectory, { recursive: true }); + writeFileSync(disclosurePath, jsonBytes(generated.disclosure)); + writeFileSync(publicPath, jsonBytes(generated.publicEvidence)); +} + +assertExactFile(disclosurePath, generated.disclosure); +assertExactFile(publicPath, generated.publicEvidence); +const disclosure = readJson(disclosurePath); +const publicEvidence = readJson(publicPath); +verifyAuditorDisclosure(disclosure, publicEvidence); +verifyBoundaries(fixture, disclosure, publicEvidence); + +console.log("Private-witness TOOL_TRACE demo: PASS"); +console.log(`Space: ${publicEvidence.onChain.space.spaceId}`); +console.log(`Authorized transitions: ${publicEvidence.onChain.head.sequence}`); +console.log("Auditor disclosure: sequence 2 only (range [2,2])"); +console.log(`Audit witnessSetRoot: ${publicEvidence.onChain.auditGrant.witnessSetRoot}`); +console.log("Verified: payload/provenance/locator openings, transition, state root, grant root"); +console.log("Not proved: tool execution truth, undisclosed sequence 1, availability, or deletion"); diff --git a/test-vectors/private-witness/tool-trace-v1.auditor.json b/test-vectors/private-witness/tool-trace-v1.auditor.json new file mode 100644 index 0000000..5eb13af --- /dev/null +++ b/test-vectors/private-witness/tool-trace-v1.auditor.json @@ -0,0 +1,56 @@ +{ + "_warning": "SYNTHETIC OUT-OF-BAND DISCLOSURE. Production auditors receive this privately; it is committed here only so anyone can reproduce the demo.", + "schema": "erc-8350/auditor-disclosure/v1", + "scenarioId": "support-tool-trace-v1", + "range": { + "fromSequence": "2", + "toSequence": "2" + }, + "items": [ + { + "witness": { + "schema": "erc-8350/private-witness-item/v1", + "spaceId": "0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa", + "sequence": "2", + "transitionId": "0x422804f4a453efe49c0e7f52b629883bc4527b17e0526ad9f1d6b5872f8ffae8", + "profileId": "0xf847003f2b63074f71c873268d340107107939eb218235cf88ed46caa0905f45", + "payload": { + "op": "append", + "resourceId": "session/support-2026-08-02/tool-trace", + "observedAt": 1785632460, + "content": { + "calls": [ + { + "tool": "shipping.get_status", + "args": { + "orderId": "ORDER-1042" + }, + "result": { + "eta": "2026-08-05", + "status": "delayed", + "warehouse": "SHA-03" + }, + "status": "ok", + "ts": 1785632460 + } + ] + } + }, + "deltaSalt": "0x5d871b621e666953d439ec415e523791540f9b3dacc6d5b47db776692e9d5360", + "provenance": { + "channel": "support-chat", + "receivedAt": 1785632460, + "ticketId": "TCK-1042" + }, + "provenanceSalt": "0x867241643a1d0ecb483e06b327480e5efa7406380b202d32ddfa15b75f2e1fef", + "locator": "vault://tool-traces/support-2026-08-02/2", + "locatorSalt": "0x7b4df867da8c1049f76f1a0421f4bc7d1abc9d5c8105fa35d3e1fcb9267435af" + }, + "encoded": { + "payloadUtf8": "{\"content\":{\"calls\":[{\"args\":{\"orderId\":\"ORDER-1042\"},\"result\":{\"eta\":\"2026-08-05\",\"status\":\"delayed\",\"warehouse\":\"SHA-03\"},\"status\":\"ok\",\"tool\":\"shipping.get_status\",\"ts\":1785632460}]},\"observedAt\":1785632460,\"op\":\"append\",\"resourceId\":\"session/support-2026-08-02/tool-trace\"}", + "provenanceUtf8": "{\"channel\":\"support-chat\",\"receivedAt\":1785632460,\"ticketId\":\"TCK-1042\"}", + "witnessUtf8": "{\"deltaSalt\":\"0x5d871b621e666953d439ec415e523791540f9b3dacc6d5b47db776692e9d5360\",\"locator\":\"vault://tool-traces/support-2026-08-02/2\",\"locatorSalt\":\"0x7b4df867da8c1049f76f1a0421f4bc7d1abc9d5c8105fa35d3e1fcb9267435af\",\"payload\":{\"content\":{\"calls\":[{\"args\":{\"orderId\":\"ORDER-1042\"},\"result\":{\"eta\":\"2026-08-05\",\"status\":\"delayed\",\"warehouse\":\"SHA-03\"},\"status\":\"ok\",\"tool\":\"shipping.get_status\",\"ts\":1785632460}]},\"observedAt\":1785632460,\"op\":\"append\",\"resourceId\":\"session/support-2026-08-02/tool-trace\"},\"profileId\":\"0xf847003f2b63074f71c873268d340107107939eb218235cf88ed46caa0905f45\",\"provenance\":{\"channel\":\"support-chat\",\"receivedAt\":1785632460,\"ticketId\":\"TCK-1042\"},\"provenanceSalt\":\"0x867241643a1d0ecb483e06b327480e5efa7406380b202d32ddfa15b75f2e1fef\",\"schema\":\"erc-8350/private-witness-item/v1\",\"sequence\":\"2\",\"spaceId\":\"0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa\",\"transitionId\":\"0x422804f4a453efe49c0e7f52b629883bc4527b17e0526ad9f1d6b5872f8ffae8\"}" + } + } + ] +} diff --git a/test-vectors/private-witness/tool-trace-v1.private.json b/test-vectors/private-witness/tool-trace-v1.private.json new file mode 100644 index 0000000..b1578e6 --- /dev/null +++ b/test-vectors/private-witness/tool-trace-v1.private.json @@ -0,0 +1,107 @@ +{ + "_warning": "SYNTHETIC FIXTURE ONLY. This file deliberately publishes fake private inputs and salts so the demo is reproducible. Never commit a production witness, salt, locator, credential, customer record, or tool result.", + "schema": "erc-8350/private-witness-fixture/v1", + "scenarioId": "support-tool-trace-v1", + "space": { + "controller": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7", + "authorizer": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7", + "spaceSalt": "0x404bc10ac782bfbdfc1879db7bdd7dfbc8e4d10b34e4b85db7910164228a893d" + }, + "auditor": "0x00000000000000000000000000000000000ad170", + "profileUri": "https://awareness.market/profiles/memory/tool-trace/v1", + "transitions": [ + { + "sequence": "1", + "payload": { + "op": "append", + "resourceId": "session/support-2026-08-02/tool-trace", + "observedAt": 1785632400, + "content": { + "calls": [ + { + "tool": "crm.lookup_customer", + "args": { + "email": "ada@example.test" + }, + "result": { + "customerId": "CUS-1042", + "supportTier": "priority" + }, + "status": "ok", + "ts": 1785632400 + } + ] + } + }, + "deltaSalt": "0xc29830105e2bdfca99d5e6543367f6171311ae63c6a3cd0f8bcb7a68d99dbf81", + "provenance": null, + "provenanceSalt": null, + "locator": "vault://tool-traces/support-2026-08-02/1", + "locatorSalt": "0xccc29b857fa7d3ee2da599779e42b7a4bec1bb9c36f1c359ae2237da02ac3fbd" + }, + { + "sequence": "2", + "payload": { + "op": "append", + "resourceId": "session/support-2026-08-02/tool-trace", + "observedAt": 1785632460, + "content": { + "calls": [ + { + "tool": "shipping.get_status", + "args": { + "orderId": "ORDER-1042" + }, + "result": { + "eta": "2026-08-05", + "status": "delayed", + "warehouse": "SHA-03" + }, + "status": "ok", + "ts": 1785632460 + } + ] + } + }, + "deltaSalt": "0x5d871b621e666953d439ec415e523791540f9b3dacc6d5b47db776692e9d5360", + "provenance": { + "channel": "support-chat", + "receivedAt": 1785632460, + "ticketId": "TCK-1042" + }, + "provenanceSalt": "0x867241643a1d0ecb483e06b327480e5efa7406380b202d32ddfa15b75f2e1fef", + "locator": "vault://tool-traces/support-2026-08-02/2", + "locatorSalt": "0x7b4df867da8c1049f76f1a0421f4bc7d1abc9d5c8105fa35d3e1fcb9267435af" + } + ], + "disclosure": { + "fromSequence": "2", + "toSequence": "2", + "sequences": [ + "2" + ] + }, + "boundaryAssertions": { + "neverPublic": [ + "ada@example.test", + "CUS-1042", + "ORDER-1042", + "TCK-1042", + "SHA-03", + "vault://tool-traces/support-2026-08-02/1", + "vault://tool-traces/support-2026-08-02/2", + "0xc29830105e2bdfca99d5e6543367f6171311ae63c6a3cd0f8bcb7a68d99dbf81", + "0xccc29b857fa7d3ee2da599779e42b7a4bec1bb9c36f1c359ae2237da02ac3fbd", + "0x5d871b621e666953d439ec415e523791540f9b3dacc6d5b47db776692e9d5360", + "0x867241643a1d0ecb483e06b327480e5efa7406380b202d32ddfa15b75f2e1fef", + "0x7b4df867da8c1049f76f1a0421f4bc7d1abc9d5c8105fa35d3e1fcb9267435af" + ], + "notDisclosedToAuditor": [ + "ada@example.test", + "CUS-1042", + "vault://tool-traces/support-2026-08-02/1", + "0xc29830105e2bdfca99d5e6543367f6171311ae63c6a3cd0f8bcb7a68d99dbf81", + "0xccc29b857fa7d3ee2da599779e42b7a4bec1bb9c36f1c359ae2237da02ac3fbd" + ] + } +} diff --git a/test-vectors/private-witness/tool-trace-v1.public.json b/test-vectors/private-witness/tool-trace-v1.public.json new file mode 100644 index 0000000..ac1c75f --- /dev/null +++ b/test-vectors/private-witness/tool-trace-v1.public.json @@ -0,0 +1,59 @@ +{ + "_notice": "PUBLIC EVIDENCE ONLY. No raw payload, provenance, locator, salt, or witness hash belongs in this file or in calldata.", + "schema": "erc-8350/private-witness-public-evidence/v1", + "scenarioId": "support-tool-trace-v1", + "onChain": { + "space": { + "spaceId": "0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa", + "controller": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7", + "authorizer": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7", + "spaceSalt": "0x404bc10ac782bfbdfc1879db7bdd7dfbc8e4d10b34e4b85db7910164228a893d" + }, + "transitions": [ + { + "spaceId": "0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa", + "sequence": "1", + "prevStateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "deltaCommitment": "0xbedbaa7e47d2d7cde26835b63b32527c4eb40bc17c1be3481e796eea51554ac2", + "provenanceCommitment": "0x0000000000000000000000000000000000000000000000000000000000000000", + "profileId": "0xf847003f2b63074f71c873268d340107107939eb218235cf88ed46caa0905f45", + "locatorCommitment": "0xc0082bd9fb40da719341e116b2de3427f32b0060b66b47bc5d18084298d1dabd", + "transitionId": "0x397e0a3efbc6bf0f0697e993f7a7b1c0f05e05fda80fd9838b03ffdd6020e9bc", + "nextStateRoot": "0xa6000d03b3d390bb626d5136fb25bcaeb56679e20d89f19b8f381fd86b1df51c", + "authorizer": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7" + }, + { + "spaceId": "0x9dcc578df03c4fe1a65e5eea4142d4677f1115a64ee491fc85de67c52a2913aa", + "sequence": "2", + "prevStateRoot": "0xa6000d03b3d390bb626d5136fb25bcaeb56679e20d89f19b8f381fd86b1df51c", + "deltaCommitment": "0x25753573a84403606ccb7e4d2f05b898ad95a8744af6f041a478b3a66da04540", + "provenanceCommitment": "0x3d180a119d29f7b1b5c347d57e7d9bb79860d53399cf1b766f6f7bdd2149ed44", + "profileId": "0xf847003f2b63074f71c873268d340107107939eb218235cf88ed46caa0905f45", + "locatorCommitment": "0x6a3d72b91545ade7be993e1b736d9b9a5be09fb86cb8544e887cdde6f9b7e624", + "transitionId": "0x422804f4a453efe49c0e7f52b629883bc4527b17e0526ad9f1d6b5872f8ffae8", + "nextStateRoot": "0xc4357c83a22fdb119a0a2a34677fcf71ee013129f18d0fdffe921a652fcb87fd", + "authorizer": "0xe05fcc23807536bee418f142d19fa0d21bb0cff7" + } + ], + "head": { + "transitionId": "0x422804f4a453efe49c0e7f52b629883bc4527b17e0526ad9f1d6b5872f8ffae8", + "stateRoot": "0xc4357c83a22fdb119a0a2a34677fcf71ee013129f18d0fdffe921a652fcb87fd", + "sequence": "2" + }, + "auditGrant": { + "grantId": "0x828f699c70bedb8516a2e2b581cd048929d988f9d4e6c329301c6d466def675b", + "auditor": "0x00000000000000000000000000000000000ad170", + "fromSequence": "2", + "toSequence": "2", + "witnessSetRoot": "0x8ef3a2f2247a6049dc26fe48472e7373607e85b3d76aa4d50e156b80c3062f9d" + } + }, + "disclosurePolicy": { + "disclosedSequences": [ + "2" + ], + "undisclosedSequences": [ + "1" + ] + } +}