fix(docs): the merkle leaf is hashed twice, not once - #64
Merged
Conversation
`_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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based on
develop.The payload standard documents a merkle leaf encoding that
_authrejects. An integrator following the publishedleafEncoding: "v1"spec would build a tree whose every proof fails.Why
_authcomputeskeccak256(bytes.concat(hash(params))), andhash(params)is itselfkeccak256(abi.encode(params)). The leaf is therefore hashed twice.docs/discovery.mddocumentedleaf = keccak256(abi.encode(ConditionalOrderParams)), a single hash. Not a wording slip: a tree built that way is rejected outright withProofNotAuthed.What
1. The spec now matches the contract
leaf = keccak256(keccak256(abi.encode(ConditionalOrderParams))), stated against thebytes.concatform in_authso the two can be compared by eye.2. The
StandardMerkleTreenote was wrong on its stated groundStandardMerkleTreepartly because it double-hashes leaves. This double-hashes too, so that was never the difference.StandardMerkleTreeand for Solady'sMerkleTreeLib, which is unchanged from build!: upstream Safe v1.5.0, forge-std v1.11.0, and via_ir by default聽#62 and still measured at 5, 7 and 9 leaves.rootfromleaves, whereas a wrong leaf encoding fails immediately on every proof.3.
test_payloadTree_NormativeConstructionVerifiesLikeAuthcould not catch it_auth. It built single-hashed leaves and verified them against their own root withMerkleProofLib, which any self-consistent tree passes, so it was wrong in exactly the way the spec was and still green.getTradeableOrderWithSignature, so_authis what accepts or rejects the construction.test_payloadTree_SingleHashedLeafIsRejectedpins the old encoding as rejected, so the spec cannot drift back without a failure.4.
EfficientHashLibat the leaf sitekeccak256(bytes.concat(hash(params)))becomesEfficientHashLib.hash(hash(params)): identical value, without allocating a 32-bytebytesto hash once and discard.src/where it applies.hash(params)at line 373 encodes a struct with a dynamicbytes staticInput, so the encoding has to be materialised anyway, and that value is the order id.Testing
forge fmt --checkclean; gas snapshot regenerated.test_payloadTree_NormativeConstructionVerifiesLikeAuthtest_payloadTree_SingleHashedLeafIsRejected, and 2 othersAI Assistance
AI Assistance: Claude Code used for the investigation, the fix, the tests and this PR description.