Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 71 additions & 58 deletions ethereum/contracts/BeefyLightClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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
Expand All @@ -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"
);
}

Expand Down
75 changes: 75 additions & 0 deletions ethereum/contracts/SparseMerkleMultiProof.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 6 additions & 1 deletion ethereum/contracts/ValidatorRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}