From 0bd3e1316d7e0613447611cb8560b3a880ae6d3f Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Fri, 26 May 2023 12:25:13 +0200 Subject: [PATCH 01/15] Add previous Merkle proof verifier --- .../contracts/src/utils/MerkleProof.sol | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 core/packages/contracts/src/utils/MerkleProof.sol diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol new file mode 100644 index 0000000000..5f8c9621b2 --- /dev/null +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.19; + +library MerkleProof { + /** + * @notice Verify that a specific leaf element is part of the Merkle Tree at a specific position in the tree + * + * @param root the root of the merkle tree + * @param leaf the leaf which needs to be proven + * @param pos the position of the leaf, index starting with 0 + * @param width the width or number of leaves in the tree + * @param proof the array of proofs to help verify the leaf's membership, ordered from leaf to root + * @return a boolean value representing the success or failure of the operation + */ + function verifyMerkleLeafAtPosition( + bytes32 root, + bytes32 leaf, + uint256 pos, + uint256 width, + bytes32[] calldata proof + ) public pure returns (bool) { + bytes32 computedHash = computeRootFromProofAtPosition(leaf, pos, width, proof); + + return computedHash == root; + } + + /** + * @notice Compute the root of a MMR from a leaf and proof + * + * @param leaf the leaf we want to prove + * @param proof an array of nodes to be hashed in order that they should be hashed + * @param side an array of booleans signalling whether the corresponding proof hash should be hashed on the left side (true) or + * the right side (false) of the current node hash + */ + function computeRootFromProofAndSide( + bytes32 leaf, + bytes32[] calldata proof, + bool[] calldata side + ) public pure returns (bytes32) { + bytes32 node = leaf; + for (uint256 i = 0; i < proof.length; i++) { + if (side[i]) { + node = keccak256(abi.encodePacked(proof[i], node)); + } else { + node = keccak256(abi.encodePacked(node, proof[i])); + } + } + return node; + } + + function computeRootFromProofAtPosition( + bytes32 leaf, + uint256 pos, + uint256 width, + bytes32[] calldata proof + ) public pure returns (bytes32) { + bytes32 computedHash = leaf; + + require(pos < width, "Merkle position is too high"); + + unchecked { + uint256 i = 0; + for (uint256 height = 0; width > 1; height++) { + bool computedHashLeft = pos & 1 == 0; + + // check if at rightmost branch and whether the computedHash is left + if (pos + 1 == width && computedHashLeft) { + // there is no sibling and also no element in proofs, so we just go up one layer in the tree + pos = pos >> 1; + width = ((width - 1) >> 1) + 1; + continue; + } + + if (computedHashLeft) { + computedHash = _efficientHash(computedHash, proof[i]); + } else { + computedHash = _efficientHash(proof[i], computedHash); + } + + pos = pos >> 1; + width = ((width - 1) >> 1) + 1; + i++; + } + + return computedHash; + } + } + + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} From d03946f107493c319e110b804f6c69e9d80cc276 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Fri, 26 May 2023 12:31:39 +0200 Subject: [PATCH 02/15] Switch BeefyClient to previous MerkleProof verifier --- core/packages/contracts/src/BeefyClient.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/packages/contracts/src/BeefyClient.sol b/core/packages/contracts/src/BeefyClient.sol index 03a981a664..4ed6ed5715 100644 --- a/core/packages/contracts/src/BeefyClient.sol +++ b/core/packages/contracts/src/BeefyClient.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol"; import {Ownable} from "openzeppelin/access/Ownable.sol"; -import {MerkleProof} from "openzeppelin/utils/cryptography/MerkleProof.sol"; +import {MerkleProof} from "./utils/MerkleProof.sol"; import {Bitfield} from "./utils/Bitfield.sol"; import {MMRProof} from "./utils/MMRProof.sol"; import {ScaleCodec} from "./ScaleCodec.sol"; @@ -444,7 +444,7 @@ contract BeefyClient is Ownable { // // NOTE: This currently insecure due to a regression documented in SNO-427. // Basically `proof.index` (the merkle leaf index) is not being validated. - if (!isValidatorInSet(vset, proof.account, proof.proof)) { + if (!isValidatorInSet(vset, proof.account, proof.index, proof.proof)) { revert InvalidValidatorProof(); } @@ -498,13 +498,13 @@ contract BeefyClient is Ownable { * @param proof Merkle proof required for validation of the address * @return true if the validator is in the set */ - function isValidatorInSet(ValidatorSet memory vset, address addr, bytes32[] calldata proof) + function isValidatorInSet(ValidatorSet memory vset, address addr, uint256 index, bytes32[] calldata proof) internal pure returns (bool) { bytes32 hashedLeaf = keccak256(abi.encodePacked(addr)); - return MerkleProof.verify(proof, vset.root, hashedLeaf); + return MerkleProof.verifyMerkleLeafAtPosition(vset.root, hashedLeaf, index, vset.length, proof); } /** From 115d7c29a42f7fa300fcbb532b45d718b434d432 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Fri, 26 May 2023 12:40:50 +0200 Subject: [PATCH 03/15] Switch ParachainClient to previous MerkleProof verifier --- core/packages/contracts/src/ParachainClient.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/packages/contracts/src/ParachainClient.sol b/core/packages/contracts/src/ParachainClient.sol index 3c10775a34..af4e976901 100644 --- a/core/packages/contracts/src/ParachainClient.sol +++ b/core/packages/contracts/src/ParachainClient.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.19; -import "openzeppelin/utils/cryptography/MerkleProof.sol"; +import "./utils/MerkleProof.sol"; import "./BeefyClient.sol"; import "./IParachainClient.sol"; import "./ScaleCodec.sol"; @@ -74,7 +74,12 @@ contract ParachainClient is IParachainClient { bytes32 parachainHeadHash = createParachainHeaderMerkleLeaf(proof.header); // Compute the merkle root hash of all parachain heads - bytes32 parachainHeadsRoot = MerkleProof.processProof(proof.headProof.proof, parachainHeadHash); + bytes32 parachainHeadsRoot = MerkleProof.computeRootFromProofAtPosition( + parachainHeadHash, + proof.headProof.pos, + proof.headProof.width, + proof.headProof.proof + ); bytes32 leafHash = createMMRLeaf(proof.leafPartial, parachainHeadsRoot); return beefyClient.verifyMMRLeafProof(leafHash, proof.leafProof, proof.leafProofOrder); From ea920658bd289fa070588497de6a2546f0fdb326 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Fri, 26 May 2023 12:46:26 +0200 Subject: [PATCH 04/15] Remove unused function --- .../contracts/src/utils/MerkleProof.sol | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol index 5f8c9621b2..9f90fbc43e 100644 --- a/core/packages/contracts/src/utils/MerkleProof.sol +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -24,30 +24,6 @@ library MerkleProof { return computedHash == root; } - /** - * @notice Compute the root of a MMR from a leaf and proof - * - * @param leaf the leaf we want to prove - * @param proof an array of nodes to be hashed in order that they should be hashed - * @param side an array of booleans signalling whether the corresponding proof hash should be hashed on the left side (true) or - * the right side (false) of the current node hash - */ - function computeRootFromProofAndSide( - bytes32 leaf, - bytes32[] calldata proof, - bool[] calldata side - ) public pure returns (bytes32) { - bytes32 node = leaf; - for (uint256 i = 0; i < proof.length; i++) { - if (side[i]) { - node = keccak256(abi.encodePacked(proof[i], node)); - } else { - node = keccak256(abi.encodePacked(node, proof[i])); - } - } - return node; - } - function computeRootFromProofAtPosition( bytes32 leaf, uint256 pos, From 58fd3efdd9725d10ff9c9e293fe6b08a62a2a6d5 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Mon, 29 May 2023 12:50:35 +0200 Subject: [PATCH 05/15] Update polkadot to include BEEFY revert --- core/packages/test/scripts/set-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packages/test/scripts/set-env.sh b/core/packages/test/scripts/set-env.sh index 6788aacc81..c23a07ad7f 100755 --- a/core/packages/test/scripts/set-env.sh +++ b/core/packages/test/scripts/set-env.sh @@ -1,7 +1,7 @@ # exports enable use in core/packages/test/config/launch-config.toml root_dir="$(realpath ../../..)" bridge_hub_runtime="${PARACHAIN_RUNTIME:-bridge-hub-rococo-local}" -relaychain_version="${POLKADOT_VER:-v0.9.38}" +relaychain_version="${POLKADOT_VER:-v0.9.43}" relaychain_dir="$root_dir/parachain/.cargo/$relaychain_version" relaychain_bin="${POLKADOT_BIN:-$relaychain_dir/bin/polkadot}" cumulus_version="${CUMULUS_VER:-snowbridge}" From 8111e7f9b3308d2ac9ab30d9dbb5f23bdc816d81 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Wed, 14 Jun 2023 13:42:22 +0200 Subject: [PATCH 06/15] Remove note about not checking index --- core/packages/contracts/src/BeefyClient.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/packages/contracts/src/BeefyClient.sol b/core/packages/contracts/src/BeefyClient.sol index 4ed6ed5715..1f89415c8e 100644 --- a/core/packages/contracts/src/BeefyClient.sol +++ b/core/packages/contracts/src/BeefyClient.sol @@ -441,9 +441,6 @@ contract BeefyClient is Ownable { } // Check that validator is actually in a validator set - // - // NOTE: This currently insecure due to a regression documented in SNO-427. - // Basically `proof.index` (the merkle leaf index) is not being validated. if (!isValidatorInSet(vset, proof.account, proof.index, proof.proof)) { revert InvalidValidatorProof(); } From 5e95b32b691f9b7869f074a3f18e1c98faaeacf6 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:55:16 +0200 Subject: [PATCH 07/15] Remove leading underscore --- core/packages/contracts/src/utils/MerkleProof.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol index 9f90fbc43e..458538592e 100644 --- a/core/packages/contracts/src/utils/MerkleProof.sol +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -48,9 +48,9 @@ library MerkleProof { } if (computedHashLeft) { - computedHash = _efficientHash(computedHash, proof[i]); + computedHash = efficientHash(computedHash, proof[i]); } else { - computedHash = _efficientHash(proof[i], computedHash); + computedHash = efficientHash(proof[i], computedHash); } pos = pos >> 1; @@ -62,7 +62,7 @@ library MerkleProof { } } - function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + function efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { /// @solidity memory-safe-assembly assembly { mstore(0x00, a) From a174801ecc9bdd8b056f59bd02da08f195417649 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:32:21 +0200 Subject: [PATCH 08/15] Don't sort hash pairs when generating test data --- core/packages/contracts/scripts/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packages/contracts/scripts/helpers.ts b/core/packages/contracts/scripts/helpers.ts index b15c599f2a..69355595b5 100644 --- a/core/packages/contracts/scripts/helpers.ts +++ b/core/packages/contracts/scripts/helpers.ts @@ -67,7 +67,7 @@ class ValidatorSet { let leaves = wallets.map((w) => keccakFromHexString(w.address)) let tree = new MerkleTree(leaves, keccak, { sortLeaves: false, - sortPairs: true, + sortPairs: false, }) this.wallets = wallets From 46e7fba635e51301f766ed26dc30d566e73b139b Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:51:55 +0200 Subject: [PATCH 09/15] Print logs from forge test --- .github/workflows/ethereum.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ethereum.yml b/.github/workflows/ethereum.yml index 5ae1ca08b0..55d545081a 100644 --- a/.github/workflows/ethereum.yml +++ b/.github/workflows/ethereum.yml @@ -36,7 +36,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Test working-directory: core/packages/contracts - run: forge test + run: forge test -vv - name: Coverage working-directory: core/packages/contracts run: forge coverage --report=lcov From bd93622d5c47a74b864e5ae53f0f0955cdcf90bf Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:36:08 +0200 Subject: [PATCH 10/15] wat --- core/packages/contracts/src/BeefyClient.sol | 1 + core/packages/contracts/src/utils/MerkleProof.sol | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/packages/contracts/src/BeefyClient.sol b/core/packages/contracts/src/BeefyClient.sol index 1f89415c8e..5c50606b11 100644 --- a/core/packages/contracts/src/BeefyClient.sol +++ b/core/packages/contracts/src/BeefyClient.sol @@ -502,6 +502,7 @@ contract BeefyClient is Ownable { { bytes32 hashedLeaf = keccak256(abi.encodePacked(addr)); return MerkleProof.verifyMerkleLeafAtPosition(vset.root, hashedLeaf, index, vset.length, proof); + // return true; } /** diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol index 458538592e..c36d1978a3 100644 --- a/core/packages/contracts/src/utils/MerkleProof.sol +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -16,12 +16,13 @@ library MerkleProof { bytes32 root, bytes32 leaf, uint256 pos, - uint256 width, + uint128 width, bytes32[] calldata proof ) public pure returns (bool) { - bytes32 computedHash = computeRootFromProofAtPosition(leaf, pos, width, proof); + return true; + // bytes32 computedHash = computeRootFromProofAtPosition(leaf, pos, width, proof); - return computedHash == root; + // return computedHash == root; } function computeRootFromProofAtPosition( @@ -47,6 +48,8 @@ library MerkleProof { continue; } + // require(i < proof.length, "Merkle proof is too short"); + if (computedHashLeft) { computedHash = efficientHash(computedHash, proof[i]); } else { From 0e06843dc873f20a391d4b3fc424639e8f54b928 Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:19:51 +0200 Subject: [PATCH 11/15] Fix library function signatures --- core/packages/contracts/src/BeefyClient.sol | 1 - core/packages/contracts/src/utils/MerkleProof.sol | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/core/packages/contracts/src/BeefyClient.sol b/core/packages/contracts/src/BeefyClient.sol index 5c50606b11..1f89415c8e 100644 --- a/core/packages/contracts/src/BeefyClient.sol +++ b/core/packages/contracts/src/BeefyClient.sol @@ -502,7 +502,6 @@ contract BeefyClient is Ownable { { bytes32 hashedLeaf = keccak256(abi.encodePacked(addr)); return MerkleProof.verifyMerkleLeafAtPosition(vset.root, hashedLeaf, index, vset.length, proof); - // return true; } /** diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol index c36d1978a3..86e5e5ce14 100644 --- a/core/packages/contracts/src/utils/MerkleProof.sol +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -16,21 +16,20 @@ library MerkleProof { bytes32 root, bytes32 leaf, uint256 pos, - uint128 width, - bytes32[] calldata proof - ) public pure returns (bool) { - return true; - // bytes32 computedHash = computeRootFromProofAtPosition(leaf, pos, width, proof); + uint256 width, + bytes32[] memory proof + ) internal pure returns (bool) { + bytes32 computedHash = computeRootFromProofAtPosition(leaf, pos, width, proof); - // return computedHash == root; + return computedHash == root; } function computeRootFromProofAtPosition( bytes32 leaf, uint256 pos, uint256 width, - bytes32[] calldata proof - ) public pure returns (bytes32) { + bytes32[] memory proof + ) internal pure returns (bytes32) { bytes32 computedHash = leaf; require(pos < width, "Merkle position is too high"); From dc7b345d209e1b851a137b4efb4a47abaa2b620b Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:20:16 +0200 Subject: [PATCH 12/15] Remove test logs --- .github/workflows/ethereum.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ethereum.yml b/.github/workflows/ethereum.yml index 55d545081a..5ae1ca08b0 100644 --- a/.github/workflows/ethereum.yml +++ b/.github/workflows/ethereum.yml @@ -36,7 +36,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Test working-directory: core/packages/contracts - run: forge test -vv + run: forge test - name: Coverage working-directory: core/packages/contracts run: forge coverage --report=lcov From 2cb81003b81b9dd1011bbf6d610146c67405ec9a Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:42:24 +0200 Subject: [PATCH 13/15] Use internal modifier in Bitfield --- core/packages/contracts/src/utils/Bitfield.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/packages/contracts/src/utils/Bitfield.sol b/core/packages/contracts/src/utils/Bitfield.sol index 06e62d185a..20d89e7908 100644 --- a/core/packages/contracts/src/utils/Bitfield.sol +++ b/core/packages/contracts/src/utils/Bitfield.sol @@ -61,7 +61,7 @@ library Bitfield { * @dev Helper to create a bitfield. */ function createBitfield(uint256[] calldata bitsToSet, uint256 length) - public + internal pure returns (uint256[] memory bitfield) { @@ -82,7 +82,7 @@ library Bitfield { * The algorithm below is implemented after https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation. * Further improvements are possible, see the article above. */ - function countSetBits(uint256[] memory self) public pure returns (uint256) { + function countSetBits(uint256[] memory self) internal pure returns (uint256) { unchecked { uint256 count = 0; for (uint256 i = 0; i < self.length; i++) { From 37699bdc60c38ef06dcc620bce4a46488c3fb80a Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:52:42 +0200 Subject: [PATCH 14/15] Revert modifier so that BeefyClient test works --- core/packages/contracts/src/utils/Bitfield.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packages/contracts/src/utils/Bitfield.sol b/core/packages/contracts/src/utils/Bitfield.sol index 20d89e7908..221304b871 100644 --- a/core/packages/contracts/src/utils/Bitfield.sol +++ b/core/packages/contracts/src/utils/Bitfield.sol @@ -61,7 +61,7 @@ library Bitfield { * @dev Helper to create a bitfield. */ function createBitfield(uint256[] calldata bitsToSet, uint256 length) - internal + public pure returns (uint256[] memory bitfield) { From d5095b74bc96082dba454c35af6e0022e50a968f Mon Sep 17 00:00:00 2001 From: David Dunn <26876072+doubledup@users.noreply.github.com> Date: Thu, 15 Jun 2023 17:18:23 +0200 Subject: [PATCH 15/15] Guard against short proofs --- core/packages/contracts/src/utils/MerkleProof.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packages/contracts/src/utils/MerkleProof.sol b/core/packages/contracts/src/utils/MerkleProof.sol index 86e5e5ce14..2cfa8bba9b 100644 --- a/core/packages/contracts/src/utils/MerkleProof.sol +++ b/core/packages/contracts/src/utils/MerkleProof.sol @@ -47,7 +47,7 @@ library MerkleProof { continue; } - // require(i < proof.length, "Merkle proof is too short"); + require(i < proof.length, "Merkle proof is too short"); if (computedHashLeft) { computedHash = efficientHash(computedHash, proof[i]);