Verification & Proofs
Merkle Specification
The exact specification for independently verifying proof hashes, including tree structure and reference implementations.
This document defines the exact algorithm PacSpace uses to compute proof hashes. Follow this specification to independently verify any proof hash you receive.
Tree Construction Rules
Rule 1: Leaf Hashes
Each item in a batch is hashed using keccak256 with a domain-separated prefix:
leafHash = keccak256("pacspace:item:v1:" + canonicalJSON)
The JSON is canonicalized — keys are sorted alphabetically at every nesting level, and no extra whitespace is included.
Rule 2: Internal Nodes
Each internal node is computed by concatenating two child hashes with a domain prefix:
nodeHash = keccak256("pacspace:node:v1:" + leftHash + rightHash)
Rule 3: Pair Order
Leaves are paired left-to-right in insertion order. The first two leaves form the first pair, the next two form the second pair, and so on up the tree.
Rule 4: Odd Number of Leaves
If a level has an odd number of nodes, the last node is duplicated to form a complete pair.
Rule 5: Empty Tree
An empty batch (zero items) produces a zero hash:
0x0000000000000000000000000000000000000000000000000000000000000000
Rule 6: Single Item
A batch with one item uses the item's leaf hash directly as the root — no tree is constructed.
root = leafHash(item)
Tree Structure
Here's how a four-item batch produces a proof hash:
┌──────────────┐
│ Proof Hash │
│ (Root) │
└──────┬───────┘
│
┌────────┴────────┐
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Node A │ │ Node B │
└─────┬─────┘ └─────┬─────┘
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ L₀ │ │ L₁ │ │ L₂ │ │ L₃ │
└─────┘ └─────┘ └─────┘ └─────┘
Item 0 Item 1 Item 2 Item 3
Where:
- L₀–L₃ =
keccak256("pacspace:item:v1:" + canonicalJSON(item)) - Node A =
keccak256("pacspace:node:v1:" + L₀ + L₁) - Node B =
keccak256("pacspace:node:v1:" + L₂ + L₃) - Root =
keccak256("pacspace:node:v1:" + Node A + Node B)
For an odd number of items (e.g., three items), the last leaf is duplicated:
┌──────────────┐
│ Proof Hash │
└──────┬───────┘
│
┌────────┴────────┐
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Node A │ │ Node B │
└─────┬─────┘ └─────┬─────┘
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
L₀ L₁ L₂ L₂ (dup)
Reference Implementations
JavaScript
computeItemHash
Computes the leaf hash for a single item.
const { keccak256 } = require('@ethersproject/keccak256');
const { toUtf8Bytes } = require('@ethersproject/strings');
function canonicalize(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return '[' + obj.map(canonicalize).join(',') + ']';
const sortedKeys = Object.keys(obj).sort();
const pairs = sortedKeys.map(k => JSON.stringify(k) + ':' + canonicalize(obj[k]));
return '{' + pairs.join(',') + '}';
}
function computeItemHash(item) {
const canonical = canonicalize(item);
const prefixed = 'pacspace:item:v1:' + canonical;
return keccak256(toUtf8Bytes(prefixed));
}
computeMerkleRoot
Builds the tree and returns the root hash.
const ZERO_HASH = '0x' + '0'.repeat(64);
function computeMerkleRoot(itemHashes) {
if (itemHashes.length === 0) return ZERO_HASH;
if (itemHashes.length === 1) return itemHashes[0];
let level = [...itemHashes];
while (level.length > 1) {
const nextLevel = [];
// If odd number of nodes, duplicate the last one
if (level.length % 2 !== 0) {
level.push(level[level.length - 1]);
}
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = level[i + 1];
const prefixed = 'pacspace:node:v1:' + left + right;
nextLevel.push(keccak256(toUtf8Bytes(prefixed)));
}
level = nextLevel;
}
return level[0];
}
verifyReceipt
End-to-end verification of a webhook receipt.
function verifyReceipt(receipt) {
const { contentHash, proofHash, itemHashes, previousProofHash } = receipt.proof;
// 1. Verify content hash is in the batch
if (!itemHashes.includes(contentHash)) {
return { verified: false, reason: 'Content hash not found in itemHashes' };
}
// 2. Recompute the Merkle root from itemHashes
const recomputedRoot = computeMerkleRoot(itemHashes);
// 3. Compare against the proof hash
if (recomputedRoot !== proofHash) {
return { verified: false, reason: 'Recomputed root does not match proofHash' };
}
// 4. (Optional) Validate chain link if previous hash is available
if (previousProofHash && receipt._storedPreviousHash) {
if (previousProofHash !== receipt._storedPreviousHash) {
return { verified: false, reason: 'Chain link broken — previousProofHash mismatch' };
}
}
return { verified: true };
}
Usage Example
// From a delta.verified webhook payload
const receipt = {
proof: {
proofHash: '0x8f3a9b...',
contentHash: '0x2d4e7f...',
itemHashes: ['0x2d4e7f...', '0x9f1a3c...'],
previousProofHash: '0x1c7b2e...'
}
};
const result = verifyReceipt(receipt);
console.log(result); // { verified: true }
Hashing Notes
- Algorithm: keccak256 (the same hash function used widely in cryptographic applications)
- Encoding: All string inputs are UTF-8 encoded before hashing
- Output: 32-byte hex string prefixed with
0x - Domain prefixes: Prevent hash collisions between leaf and internal nodes (
pacspace:item:v1:vspacspace:node:v1:)
Last updated February 11, 2026