Skip to content

fix(docs): the merkle leaf is hashed twice, not once - #64

Merged
mfw78 merged 1 commit into
developfrom
fix/merkle-leaf-encoding
Aug 3, 2026
Merged

fix(docs): the merkle leaf is hashed twice, not once#64
mfw78 merged 1 commit into
developfrom
fix/merkle-leaf-encoding

Conversation

@mfw78

@mfw78 mfw78 commented Aug 3, 2026

Copy link
Copy Markdown

Based on develop.

The payload standard documents a merkle leaf encoding that _auth rejects. An integrator following the published leafEncoding: "v1" spec would build a tree whose every proof fails.

Why

  • _auth computes keccak256(bytes.concat(hash(params))), and hash(params) is itself keccak256(abi.encode(params)). The leaf is therefore hashed twice.
  • docs/discovery.md documented leaf = keccak256(abi.encode(ConditionalOrderParams)), a single hash. Not a wording slip: a tree built that way is rejected outright with ProofNotAuthed.
  • The contract is correct and unchanged in behaviour. The spec describing it, a note about that spec, and the test named after it were all wrong.

What

1. The spec now matches the contract

  • leaf = keccak256(keccak256(abi.encode(ConditionalOrderParams))), stated against the bytes.concat form in _auth so the two can be compared by eye.
  • Says why the second hash is there: it keeps a leaf preimage from ever being 64 bytes, which is what would otherwise let an internal node be presented as a leaf.

2. The StandardMerkleTree note was wrong on its stated ground

3. test_payloadTree_NormativeConstructionVerifiesLikeAuth could not catch it

  • Despite the name it never called _auth. It built single-hashed leaves and verified them against their own root with MerkleProofLib, which any self-consistent tree passes, so it was wrong in exactly the way the spec was and still green.
  • It now goes through getTradeableOrderWithSignature, so _auth is what accepts or rejects the construction.
  • test_payloadTree_SingleHashedLeafIsRejected pins the old encoding as rejected, so the spec cannot drift back without a failure.

4. EfficientHashLib at the leaf site

  • keccak256(bytes.concat(hash(params))) becomes EfficientHashLib.hash(hash(params)): identical value, without allocating a 32-byte bytes to hash once and discard.
  • 696 gas off each merkle-root end to end path, 352 off TWAP settlement, 1,833 across the suite. 23 of 138 snapshot entries move.
  • It is the only site in src/ where it applies. hash(params) at line 373 encodes a struct with a dynamic bytes staticInput, so the encoding has to be materialised anyway, and that value is the order id.

Testing

  • 173 tests pass; forge fmt --check clean; gas snapshot regenerated.
  • Mutation-checked, both directions:
Mutation Killed by
test leaf reverted to the single hash the spec described test_payloadTree_NormativeConstructionVerifiesLikeAuth
contract leaf single-hashed test_payloadTree_SingleHashedLeafIsRejected, and 2 others
  • The first mutation is the one that matters: it is the exact state the test was in before this PR, and it now fails.

AI Assistance

AI Assistance: Claude Code used for the investigation, the fix, the tests and this PR description.

`_auth` computes `keccak256(bytes.concat(hash(params)))`, so a leaf is the
params hash hashed again. The payload standard documented a single
`keccak256(abi.encode(params))`, which `_auth` rejects outright: an integrator
following the published `leafEncoding: "v1"` spec would have built a tree whose
every proof fails.

The second hash is the second-preimage defence. It keeps a leaf preimage from
ever being 64 bytes, which is what would let an internal node be presented as a
leaf.

The note added in #62 disqualified OpenZeppelin's `StandardMerkleTree` partly
because it double-hashes leaves. That was the wrong ground: this does too. Tree
shape remains the real incompatibility, for `StandardMerkleTree` and for
Solady's `MerkleTreeLib`.

`test_payloadTree_NormativeConstructionVerifiesLikeAuth` could not catch any of
this. Despite the name it never called `_auth`: it built single-hashed leaves
and verified them against their own root with `MerkleProofLib`, which any
self-consistent tree passes. It now goes through
`getTradeableOrderWithSignature`, and a companion test pins that the old
single-hash encoding is rejected.

Also replaces `keccak256(bytes.concat(...))` at that site with
`EfficientHashLib.hash`, which is the same value without allocating a 32-byte
`bytes`: 696 gas off each merkle-root end to end path, 1,833 across the suite.

AI Assistance: Claude Code used for the investigation, the fix and the tests.
@mfw78
mfw78 merged commit d1f3603 into develop Aug 3, 2026
1 check passed
@mfw78
mfw78 deleted the fix/merkle-leaf-encoding branch August 3, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant