Repository navigation
Evidence Verification and Merkle Proofs
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
This document explains the evidence verification system that uses Merkle proofs to make oracle-computed agent scores auditable without requiring trust in the oracle’s internal records. Each finalized score is backed by a Merkle root stored on-chain. The oracle serves the leaves, per-leaf hashes, and sibling paths for each payment behind an agent’s score. Verifiers independently rebuild leaf hashes from on-chain payment data and verify them against the stored root using verifyEvidence.
The design ensures:
- Leaves commit to the transaction hash, payer, amount and settlement time, all of which can be read off the chain, and to the outcome. The outcome is the word of the payer who signed it or of a credentialed service: nothing on chain can confirm it, and a payment found by the scan or attested with neither an oracle token nor the payer's signature carries none.
- Roots are reproducible from chain data; a dropped or invented payment changes the root. The root does not show that the oracle counted every payment it should have, so catching an omission takes someone watching the chain independently.
- Oracle computations can be checked without trusting the oracle’s JSON state file.
The evidence system spans three layers:
- On-chain commitment and verification in the reputation contract.
- Off-chain Merkle tree construction and proof generation in the oracle.
- Documentation and fixtures ensuring both implementations agree.
graph TB
subgraph "On-chain"
Rep["SigvaraReputation.sol<br/>Stores evidenceRoots and verifyEvidence"]
end
subgraph "Off-chain Oracle"
Index["index.js<br/>buildEvidenceBody and /evidence response"]
Merkle["merkle.js<br/>leafFor, buildTree, proofFor, verifyProof"]
end
subgraph "Verification and Fixtures"
Docs["payment-backed-attestations.md<br/>Leaf encoding and verifier procedure"]
Fixture["check-evidence-fixture.mjs<br/>Cross-check JS vs Solidity"]
Test["SigvaraReputation.t.sol<br/>On-chain proof tests"]
end
Index --> Merkle
Rep --> Test
Docs --> Rep
Docs --> Merkle
Fixture --> Merkle
Fixture --> Test
Diagram sources
- SigvaraReputation.sol:199-220
- SigvaraReputation.sol:761-769
- index.js:2267-2334
- merkle.js:1-242
- payment-backed-attestations.md:219-281
- check-evidence-fixture.mjs:1-81
- SigvaraReputation.t.sol:1221-1244
Section sources
- SigvaraReputation.sol:199-220
- index.js:2267-2334
- merkle.js:1-242
- payment-backed-attestations.md:219-281
- Merkle utilities: Build leaves, trees, sibling proofs, and local verification.
- Oracle evidence builder: Convert payment events into a JSON body containing the root, count, committed fields, and per-leaf evidence with proofs.
-
On-chain storage and verification: Store the Merkle root alongside each finalized score and expose
verifyEvidencefor public checks. - Documentation and fixtures: Define the exact leaf encoding and ensure the JavaScript and Solidity implementations stay in sync.
Key responsibilities:
-
merkle.js: Encodes leaves, builds Merkle trees, generates proofs, and validates them locally. -
index.js: Builds the/evidencepayload once per agent and payment set, including the root and per-leaf proofs. -
SigvaraReputation.sol: StoresevidenceRoots, proposes/finalizes scores with roots, and verifies individual leaves against those roots. -
payment-backed-attestations.md: Documents the leaf format, verifier workflow, and security properties. -
check-evidence-fixture.mjsandSigvaraReputation.t.sol: Cross-validate that the off-chain tree matches on-chain expectations.
Section sources
- merkle.js:20-242
- index.js:2267-2334
- SigvaraReputation.sol:199-220
- SigvaraReputation.sol:761-769
- payment-backed-attestations.md:219-281
- check-evidence-fixture.mjs:1-81
- SigvaraReputation.t.sol:1221-1244
The system ties on-chain payment attestations to off-chain scoring through a Merkle commitment.
sequenceDiagram
participant Verifier as "Verifier"
participant Chain as "Blockchain"
participant Oracle as "Oracle index.js"
participant Merkle as "merkle.js"
participant Contract as "SigvaraReputation.sol"
Verifier->>Chain : Read payment receipt and block timestamp
Verifier->>Verifier : Rebuild leaf from txHash, payer, amount, settledAt and the outcome the oracle reports
Verifier->>Oracle : GET /evidence/{didHash}
Oracle-->>Verifier : { evidenceRoot, count, evidence[i].leaf, proof }
Verifier->>Contract : verifyEvidence(didHash, leaf, proof)
Contract-->>Verifier : bool (root exists and proof verifies)
Diagram sources
- payment-backed-attestations.md:235-241
- index.js:2267-2334
- merkle.js:78-238
- SigvaraReputation.sol:761-769
The oracle constructs a Merkle tree over verified payment events, in one fixed order: by settlement time, then chain, transaction and payer, with a payment chain's after the scored chain's. Two operators credit the same payments in different orders and the order of the leaves can change the root, so the order they were credited in does not count (payment-chains.js:271-280, README.md:474-477). Each event becomes a double-hashed leaf. Internal nodes are computed by hashing sorted pairs, matching OpenZeppelin’s MerkleProof. Odd nodes are promoted rather than duplicated to prevent malleability.
flowchart TD
Start(["Start buildTree(events)"]) --> MapLeaves["Map events to leaves via leafFor"]
MapLeaves --> EmptyCheck{"Any leaves?"}
EmptyCheck --> |No| ZeroRoot["Return zero root, empty layers"]
EmptyCheck --> |Yes| InitLayers["Initialize layers with leaves"]
InitLayers --> LoopLevels{"More than one node at level?"}
LoopLevels --> |Yes| PairHash["Hash sorted pairs<br/>hashPair(a,b)"]
PairHash --> NextLevel["Push next level"]
NextLevel --> LoopLevels
LoopLevels --> |No| ReturnTree["Return root, leaves, layers"]
Diagram sources
Section sources
A leaf for a payment on the scored chain commits to exactly five fields:
-
txHash: bytes32 -
payer: address -
amount: uint256 -
settledAt: uint256 seconds -
outcome: uint8 (0 failed, 1 succeeded, 2 not reported)
The leaf is double-hashed to prevent a leaf from being mistaken for an internal node. Settlement time is converted from milliseconds to seconds before encoding so verifiers reading block timestamps off-chain compute identical leaves.
A payment that settled on a payment chain (ADR 0009) puts its chain first, as a uint256: abi.encode(chainId, txHash, payer, amount, settledAt, outcome). Six words rather than five, so it cannot be mistaken for a scored-chain leaf, and every scored-chain leaf keeps the bytes the published roots were made with (merkle.js:102-109). Payment chains are off by default, and the one running on 4 November 2026, Elysium testnet on the checker, was in shadow, which credits nothing, so no root on a live operator held such a leaf (payment-chains.js:37-39, payment-chains.js:498, 0009-payments-on-base.md:236-237).
leafFor writes the five 32-byte words into a buffer by hand and caches the result against each frozen event; leafByAbi is the ABI-coder definition it falls back to for anything it does not recognise, and both produce the same bytes. A payment chain's leaf always takes the ABI path (merkle.js:143-144). The store freezes every event and replaces the array instead of editing it, which is what makes the leaf cache, and the root cache in rootFor, safe.
flowchart TD
Event(["Payment event"]) --> CheckTx["Validate txHash present"]
CheckTx --> Encode["ABI encode bytes32,address,uint256,uint256,uint8"]
Encode --> Seconds["Convert ts to settledAt seconds"]
Seconds --> Outcome["Map success to uint8 outcome"]
Outcome --> DoubleHash["keccak256(keccak256(encoded))"]
DoubleHash --> Leaf(["Leaf hash"])
Diagram sources
Section sources
The oracle builds a single JSON payload per agent and payment set. It includes:
didHashevidenceRootcountcommittedFields-
unverifiedFields, namingcounterauditPacketId, and, wherePAYER_OUTCOMES_PUBLISH=on,payerOutcomeDomain, the EIP-712 domain payers sign in - With a payment chain configured,
paymentChainCommittedFields,paymentChains(each chain'schainIdanddelaySeconds) and, once a chain ison,evidenceCut(quantumSeconds,windowSeconds) (index.js:2292-2303) -
evidence[i]withchainIdon a payment that settled on a payment chain (index.js:2316),txHash,payer,amount,settledAt,success,outcomeSource(payer_signed,credentialedornull),leaf,proof, optional corroboration metadata such ascounterauditPacketId, and, where publishing is on, the payer's signedpayerOutcome
Corroboration identifiers, outcomeSource and the signed payload are served but deliberately excluded from the leaf and root.
classDiagram
class EvidenceBody {
+string didHash
+bytes32 evidenceRoot
+uint256 count
+string[] committedFields
+EvidenceItem[] evidence
}
class EvidenceItem {
+bytes32 txHash
+address payer
+uint256 amount
+uint256 settledAt
+bool|null success
+bytes32 leaf
+bytes32[] proof
+string? counterauditPacketId
}
EvidenceBody "1" o-- "*" EvidenceItem
Diagram sources
Section sources
Each finalized score stores its associated Merkle root in evidenceRoots. The contract exposes verifyEvidence(didHash, leaf, proof) which returns false if no root exists for the agent and otherwise delegates to OpenZeppelin’s MerkleProof.verify. The root held is the one from the proposal that was last finalized, so a check applies to the payments as they stood then; a payment credited, given an outcome or pruned afterwards changes what /evidence serves, not what was committed.
sequenceDiagram
participant Caller as "Caller"
participant Contract as "SigvaraReputation.sol"
participant OZ as "OpenZeppelin MerkleProof"
Caller->>Contract : verifyEvidence(didHash, leaf, proof)
Contract->>Contract : Load evidenceRoots[didHash]
alt No root stored
Contract-->>Caller : false
else Root exists
Contract->>OZ : verify(proof, root, leaf)
OZ-->>Contract : bool
Contract-->>Caller : bool
end
Diagram sources
Section sources
- Collect verified payment events for an agent.
- Compute leaves with
leafFor. - Build the tree with
buildTree. - For each payment index, call
proofFor(tree, i). - Serve
evidenceRoot,leaf, andproofin the/evidenceresponse.
flowchart TD
A["Verified events for agent"] --> B["Compute leaves"]
B --> C["Build Merkle tree"]
C --> D["Select leaf index i"]
D --> E["Generate proofFor(tree,i)"]
E --> F["Include leaf and proof in /evidence"]
Diagram sources
- Read the payment receipt and block timestamp from the chain it settled on: the chain an entry's
chainIdnames, or the scored chain when it has none (store.js:691-693). - Fetch the
/evidenceresponse for the agent. The outcome in each leaf (0 failed, 1 succeeded, 2 not reported) is not on the chain, so it comes from here. Where the oracle publishes a payer-signed outcome's payload, check its signature underpayerOutcomeDomainas well: that shows the committed outcome is one the payer signed. - Rebuild the leaf using the documented ABI encoding, with
chainIdfirst when the entry has one (merkle.js:105-109). - Call
verifyEvidence(didHash, leaf, proof)on-chain or validate locally.
The site's Agents page (sigvara.xyz/agents) does this for any agent on Arc testnet without a wallet: it rebuilds every leaf and the root from the payments /evidence lists, compares the root with the one the contract holds, and asks verifyEvidence about each payment the finalized root covers. It can also check each payment against its transaction, except that a payment chain's payment is rebuilt with its chain id and listed as not checked, since it is not on this chain (explorer.js:537-555, agents.js:248-252). The SDK rebuilds roots the same way from 1.0.0-alpha.15 (evidenceLeaf, evidenceRoot, matchEvidenceRoot), and sigvara-mcp's sigvara_verify_evidence runs the same root check for an agent's MCP host, confirming up to the last 100 covered payments with verifyEvidence. Both write the five-word leaf only, so neither can yet rebuild a root that holds a payment chain's payment (evidence.ts:61-67, mcp.ts:28).
flowchart TD
VStart(["Verifier start"]) --> ReadChain["Read txHash, payer, amount, settledAt"]
ReadChain --> FetchEvidence["GET /evidence/{didHash}, take the outcome code"]
FetchEvidence --> RebuildLeaf["Rebuild keccak256(keccak256(encode(...)))"]
RebuildLeaf --> VerifyCall["verifyEvidence(didHash, leaf, proof)"]
VerifyCall --> Result{"Verifies?"}
Result --> |Yes| Trust["Accept payment as part of scored evidence"]
Result --> |No| Reject["Reject or investigate mismatch"]
Diagram sources
Section sources
- merkle.js:78-231
- index.js:2274-2334
- payment-backed-attestations.md:235-241
- SigvaraReputation.sol:761-769
Security benefits:
- Auditable inputs: The root commits to the exact set of payments used in scoring. Dropping or inventing a payment changes the root.
-
Chain-verifiable leaves: The transaction hash, payer, amount and settlement time of every leaf can be rebuilt from on-chain data; no hidden oracle-only fields enter the commitment. The outcome is the one field that cannot be checked against the chain. It is committed, so it cannot change once the root is on chain, but it is the word of the payer who signed it or of a credentialed service.
outcomeSourceon/evidencesays which. - Settlement-time correctness: Using seconds prevents self-refuting commitments caused by unit mismatches.
- Outcome clarity: Three-state outcome avoids collisions between unreported and failed outcomes.
- Set-based commitment: Sorted-pair hashing commits to membership, not ordering within pairs. Leaves are paired by position, so their order can still change the root, which is why the root is built over one fixed order (merkle.js:192-206, README.md:475-477).
External trust integration:
- Corroboration metadata such as CounterAudit packet IDs can accompany evidence but is intentionally excluded from the leaf and root. Verifiers fetch and check these records independently. The response lists
counterauditPacketIdunderunverifiedFields. For a payer-signed outcome the packet's public proof names the EIP-712 hash of what the payer signed (outcome_claim.signed_outcome_hash), so the link can be checked exactly. - ERC-8004 external feedback contributes to other score factors but is separate from the payment-backed evidence root.
Section sources
The components depend on each other as follows:
graph LR
Merkle["oracle/merkle.js"] --> Index["oracle/index.js"]
Index --> Docs["docs/payment-backed-attestations.md"]
Docs --> Contract["src/SigvaraReputation.sol"]
Merkle --> Contract
Fixture["scripts/check-evidence-fixture.mjs"] --> Merkle
Fixture --> Contract
Test["test/SigvaraReputation.t.sol"] --> Contract
Diagram sources
- merkle.js:1-242
- index.js:2267-2334
- payment-backed-attestations.md:219-281
- SigvaraReputation.sol:199-220
- check-evidence-fixture.mjs:1-81
- SigvaraReputation.t.sol:1221-1244
Section sources
- merkle.js:1-242
- index.js:2267-2334
- payment-backed-attestations.md:219-281
- SigvaraReputation.sol:199-220
- check-evidence-fixture.mjs:1-81
- SigvaraReputation.t.sol:1221-1244
- Building the tree and generating all proofs is performed once per agent and payment set. The documentation notes this is efficient enough to cache cleanly because it is a pure function of the events.
- The oracle serves precomputed proofs alongside leaves, reducing client-side computation.
- Leaves and roots are cached against frozen evidence, so an epoch in which an agent’s payments have not changed reuses its root.
- On-chain verification uses OpenZeppelin’s
MerkleProof.verify, which is gas-efficient for typical evidence sizes.
Common issues and resolutions:
-
Leaf does not verify: Ensure the leaf was rebuilt using seconds for
settledAt, not milliseconds. The leaf must match the documented ABI encoding. An entry with achainIdhas six words, not five. -
Root changed unexpectedly: Any change in
txHash,payer,amount,settledAt, oroutcomechanges the root. A payment credited with no outcome (2) that a token holder or the payer's signature later gives one (0or1), or a token holder's outcome that the payer's signed one replaces, changes its leaf, so the root served by/evidencemoves while the one on chain stays at the last finalized proposal’s. Also confirm that extra fields likepacketIdorblockNumberwere not included in the leaf. -
Root order: A root is over the payments in the fixed order
/evidenceserves them, so rebuild it in that order. A root committed before this order was fixed, over payments stored in another order, cannot be rebuilt from what is served; the agent's next proposal can (README.md:474-480, payment-chains.js:271-280). - Odd-node behavior: The implementation promotes odd nodes rather than duplicating them. Do not assume duplication preserves the root.
-
Outcome mismatch: Unreported outcomes are encoded as
uint8value 2, distinct from failed (0) and succeeded (1). - Fixture drift: Use the fixture script to regenerate and compare constants between the JavaScript Merkle implementation and the Solidity test fixtures.
Section sources
- merkle.js:34-40
- merkle.js:72-111
- merkle.js:185-207
- payment-backed-attestations.md:243-281
- check-evidence-fixture.mjs:1-81
The evidence verification system anchors agent scores to on-chain payment attestations through a reproducible Merkle commitment. By committing to the payment fields a verifier can read from the chain, plus the outcome the oracle reports, and exposing verifyEvidence, the protocol allows third parties to audit oracle computations without trusting the oracle’s internal state. The combination of careful leaf encoding, sorted-pair hashing, odd-node promotion, and cross-implementation fixtures ensures that the root remains a reliable, tamper-evident summary of the payments counted toward each agent’s score.
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions