Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`: 白皮书中文版
Expand Down Expand Up @@ -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 发布问题定义和最小接口讨论帖。
Expand Down
228 changes: 228 additions & 0 deletions contracts/test/integration/PrivateWitnessEvidence.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions evidence/private-witness/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@
"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",
"security:scope": "node scripts/security-rc-gate.mjs --scope-only",
"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",
Expand Down
Loading
Loading