The rules for recomputing manifestHash, resultHash and merkleRoot from published data. The reference implementations are sdk/src/canonical.ts and sdk/src/merkle.ts.
The campaign manifest and the result dataset are JSON documents. Each is hashed over a canonical serialization, RFC 8785 (JSON Canonicalization Scheme), restricted to JSON without numbers:
-
Allowed values: objects, arrays, strings,
true,falseandnull. JSON numbers are not allowed. -
Integers (token amounts, metrics, timestamps in unix seconds, IDs) are decimal strings: digits only, no sign, no leading zeros, and
"0"for zero. -
Object members are sorted by key, comparing keys as sequences of UTF-16 code units. Keys are unique.
-
There is no whitespace between tokens.
-
Strings are written literally in UTF-8, except for these escapes:
"becomes\", and\becomes\\- U+0008, U+0009, U+000A, U+000C and U+000D become
\b,\t,\n,\fand\r - every other character below U+0020 becomes
\u00xx, with lowercase hexadecimal digits.
Strings must be valid Unicode, with no unpaired surrogates.
-
Arrays keep their order. Each document type defines the order of its arrays.
hash = keccak256(UTF-8 bytes of the canonical text)
Example:
value { "b": "2", "a": ["x", true, null] }
canonical {"a":["x",true,null],"b":"2"}
Rule 3 differs from sorting by Unicode code point only for keys that mix characters above U+FFFF with characters between U+E000 and U+FFFF. The vector utf16-key-order covers this case.
Each non-zero creator payout is one leaf:
leaf = keccak256(keccak256(abi.encode(uint256 campaignId, address wallet, uint256 amount)))
The tree is an OpenZeppelin StandardMerkleTree with default options:
- Leave out zero payouts. With no leaf at all, the root is 32 zero bytes.
- Compute every leaf hash and sort the hashes in ascending byte order:
leaf[0] … leaf[n − 1]. - Create an array
nodeof2n − 1hashes and setnode[2n − 2 − i] = leaf[i]. - For
ifromn − 2down to0, setnode[i] = hashPair(node[2i + 1], node[2i + 2]), wherehashPair(a, b) = keccak256(min(a, b) ‖ max(a, b)). - The root is
node[0].
The proof of the leaf at index j lists the sibling of j (j + 1 if j is odd, j − 1 if even), then the sibling of its parent (j − 1) / 2, and so on up to the root. A proof is checked by folding, which is what MerkleProof.verify does in the escrow:
h = leaf
for p in proof: h = hashPair(h, p)
valid if h == root
| File | Content | Checked by |
|---|---|---|
vectors/canonical.json |
Canonical texts and hashes, produced by an independent serializer and hashed with Foundry's cast keccak |
SDK tests. Solidity keccak256 |
vectors/merkle.json |
Payout trees with roots, leaves and proofs, produced by the SDK | SDK tests. Solidity tests prove every leaf and claim a full tree through the escrow |