diff --git a/ethereum/contracts/BeefyLightClient.sol b/ethereum/contracts/BeefyLightClient.sol index 56b4d2c99..85385a2e7 100644 --- a/ethereum/contracts/BeefyLightClient.sol +++ b/ethereum/contracts/BeefyLightClient.sol @@ -8,6 +8,7 @@ import "./utils/Bitfield.sol"; import "./ValidatorRegistry.sol"; import "./SimplifiedMMRVerification.sol"; import "./ScaleCodec.sol"; +import "./SparseMerkleMultiProof.sol"; /** * @title A entry contract for the Ethereum light client @@ -81,6 +82,22 @@ contract BeefyLightClient { bytes32[][] publicKeyMerkleProofs; } + /** + * The MultiProof is a collection of proofs used to verify the signatures from the signers signing + * each new justification. + * @param depth Depth of the Merkle tree. Equal to log2(number of leafs) + * @param signatures an array of signatures from the chosen signers + * @param positions an array of the positions of the chosen signers + * @param decommitments multi merkle proof from the chosen validators proving that their addresses + * are in the validator set + */ + struct MultiProof { + uint256 depth; + bytes[] signatures; + uint256[] positions; + bytes32[] decommitments; + } + /** * The ValidationData is the set of data used to link each pair of initial and complete verification transactions. * @param senderAddress the sender of the initial transaction @@ -280,7 +297,7 @@ contract BeefyLightClient { function completeSignatureCommitment( uint256 id, Commitment calldata commitment, - ValidatorProof calldata validatorProof, + MultiProof calldata validatorProof, BeefyMMRLeaf calldata latestMMRLeaf, SimplifiedMMRProof calldata proof ) public { @@ -412,7 +429,7 @@ contract BeefyLightClient { function verifyCommitment( uint256 id, Commitment calldata commitment, - ValidatorProof calldata proof + MultiProof calldata proof ) internal view { ValidationData storage data = validationData[id]; @@ -454,7 +471,7 @@ contract BeefyLightClient { function verifyValidatorProofLengths( uint256 requiredNumOfSignatures, - ValidatorProof calldata proof + MultiProof calldata proof ) internal pure { /** * @dev verify that required number of signatures, positions, public keys and merkle proofs are @@ -468,79 +485,75 @@ contract BeefyLightClient { proof.positions.length == requiredNumOfSignatures, "Error: Number of validator positions does not match required" ); - require( - proof.publicKeys.length == requiredNumOfSignatures, - "Error: Number of validator public keys does not match required" - ); - require( - proof.publicKeyMerkleProofs.length == requiredNumOfSignatures, - "Error: Number of validator public keys does not match required" - ); + } + + function roundUpToPow2(uint256 len) internal pure returns (uint256) { + if (len <= 1) return 1; + else return 2 * roundUpToPow2((len + 1) / 2); } function verifyValidatorProofSignatures( uint256[] memory randomBitfield, - ValidatorProof calldata proof, + MultiProof calldata proof, uint256 requiredNumOfSignatures, Commitment calldata commitment - ) internal view { - // Encode and hash the commitment + ) private view { bytes32 commitmentHash = createCommitmentHash(commitment); + verifyProofSignatures( + validatorRegistry.root(), + validatorRegistry.numOfValidators(), + randomBitfield, + proof, + requiredNumOfSignatures, + commitmentHash + ); + } + + function verifyProofSignatures( + bytes32 root, + uint256 len, + uint256[] memory bitfield, + MultiProof memory proof, + uint256 requiredNumOfSignatures, + bytes32 commitmentHash + ) private pure { + uint256 width = roundUpToPow2(len); /** * @dev For each randomSignature, do: */ + bytes32[] memory leaves = new bytes32[](requiredNumOfSignatures); for (uint256 i = 0; i < requiredNumOfSignatures; i++) { - verifyValidatorSignature( - randomBitfield, - proof.signatures[i], - proof.positions[i], - proof.publicKeys[i], - proof.publicKeyMerkleProofs[i], - commitmentHash + uint256 pos = proof.positions[i]; + + require(pos < len, "Error: invalid signer position"); + /** + * @dev Check if validator in bitfield + */ + require( + bitfield.isSet(pos), + "Error: signer must be once in bitfield" ); - } - } - function verifyValidatorSignature( - uint256[] memory randomBitfield, - bytes calldata signature, - uint256 position, - address publicKey, - bytes32[] calldata publicKeyMerkleProof, - bytes32 commitmentHash - ) internal view { - /** - * @dev Check if validator in randomBitfield - */ - require( - randomBitfield.isSet(position), - "Error: Validator must be once in bitfield" - ); + /** + * @dev Remove validator from bitfield such that no validator can appear twice in signatures + */ + bitfield.clear(pos); - /** - * @dev Remove validator from randomBitfield such that no validator can appear twice in signatures - */ - randomBitfield.clear(position); + address signer = ECDSA.recover(commitmentHash, proof.signatures[i]); + leaves[i] = keccak256(abi.encodePacked(signer)); + } - /** - * @dev Check if merkle proof is valid - */ + require(1 << proof.depth == width, "Error: invalid depth"); require( - validatorRegistry.checkValidatorInSet( - publicKey, - position, - publicKeyMerkleProof + SparseMerkleMultiProof.verify( + root, + proof.depth, + proof.positions, + leaves, + proof.decommitments ), - "Error: Validator must be in validator set at correct position" - ); - - /** - * @dev Check if signature is correct - */ - require( - ECDSA.recover(commitmentHash, signature) == publicKey, - "Error: Invalid Signature" + "Error: invalid multi proof" ); } diff --git a/ethereum/contracts/SparseMerkleMultiProof.sol b/ethereum/contracts/SparseMerkleMultiProof.sol new file mode 100644 index 000000000..ab9128768 --- /dev/null +++ b/ethereum/contracts/SparseMerkleMultiProof.sol @@ -0,0 +1,75 @@ +// "SPDX-License-Identifier: MIT" +pragma solidity ^0.8.5; + +library SparseMerkleMultiProof { + + function hash_node(bytes32 left, bytes32 right) + internal + pure + returns (bytes32 hash) + { + assembly { + mstore(0x00, left) + mstore(0x20, right) + hash := keccak256(0x00, 0x40) + } + return hash; + } + + // Indices are required to be sorted highest to lowest. + function verify( + bytes32 root, + uint256 depth, + uint256[] memory indices, + bytes32[] memory leaves, + bytes32[] memory decommitments + ) + internal + pure + returns (bool) + { + require(indices.length == leaves.length, "LENGTH_MISMATCH"); + uint256 n = indices.length; + + // Dynamically allocate index and hash queue + uint256[] memory tree_indices = new uint256[](n + 1); + bytes32[] memory hashes = new bytes32[](n + 1); + uint256 head = 0; + uint256 tail = 0; + uint256 di = 0; + + // Queue the leafs + for(; tail < n; ++tail) { + tree_indices[tail] = 2**depth + indices[tail]; + hashes[tail] = leaves[tail]; + } + + // Itterate the queue until we hit the root + while (true) { + uint256 index = tree_indices[head]; + bytes32 hash = hashes[head]; + head = (head + 1) % (n + 1); + + // Merkle root + if (index == 1) { + return hash == root; + // Even node, take sibbling from decommitments + } else if (index & 1 == 0) { + hash = hash_node(hash, decommitments[di++]); + // Odd node with sibbling in the queue + } else if (head != tail && tree_indices[head] == index - 1) { + hash = hash_node(hashes[head], hash); + head = (head + 1) % (n + 1); + // Odd node with sibbling from decommitments + } else { + hash = hash_node(decommitments[di++], hash); + } + tree_indices[tail] = index / 2; + hashes[tail] = hash; + tail = (tail + 1) % (n + 1); + } + + // Fix warning + return false; + } +} diff --git a/ethereum/contracts/ValidatorRegistry.sol b/ethereum/contracts/ValidatorRegistry.sol index 9f55727b8..ea4bd3387 100644 --- a/ethereum/contracts/ValidatorRegistry.sol +++ b/ethereum/contracts/ValidatorRegistry.sol @@ -60,8 +60,13 @@ contract ValidatorRegistry is Ownable { root, hashedLeaf, pos, - numOfValidators, + roundUpToPow2(numOfValidators), proof ); } + + function roundUpToPow2(uint256 len) internal pure returns (uint256) { + if (len <= 1) return 1; + else return 2 * roundUpToPow2((len + 1) / 2); + } }