Skip to content
Merged
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
44 changes: 22 additions & 22 deletions contracts/src/BeefyClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {ScaleCodec} from "./utils/ScaleCodec.sol";
/**
* @title BeefyClient
*
* High-level documentation at https://docs.snowbridge.network/architecture/verification/polkadot
* The BEEFY protocol is defined in https://eprint.iacr.org/2025/057.pdf. Higher level documentation
* is available at https://docs.snowbridge.network/architecture/verification/polkadot.
*
* To submit new commitments, relayers must call the following methods sequentially:
* 1. submitInitial: Setup the session for the interactive submission
Expand Down Expand Up @@ -189,9 +190,8 @@ contract BeefyClient {
uint256 public immutable randaoCommitExpiration;

/**
* @dev Minimum number of signatures required to validate a new commitment. This parameter
* is calculated based on `randaoCommitExpiration`. See ~/scripts/beefy_signature_sampling.py
* for the calculation.
* @dev The lower bound on the number of signatures required to validate a new commitment. Note
* that the final number of signatures is calculated dynamically.
*/
uint256 public immutable minNumRequiredSignatures;

Expand All @@ -207,7 +207,6 @@ contract BeefyClient {
error InvalidValidatorProof();
error InvalidValidatorProofLength();
error CommitmentNotRelevant();
error NotEnoughClaims();
error PrevRandaoAlreadyCaptured();
error PrevRandaoNotCaptured();
error StaleCommitment();
Expand Down Expand Up @@ -256,14 +255,13 @@ contract BeefyClient {
revert StaleCommitment();
}

ValidatorSetState storage vset;
ValidatorSetState storage vset = currentValidatorSet;
uint16 signatureUsageCount;
if (commitment.validatorSetID == currentValidatorSet.id) {
signatureUsageCount = currentValidatorSet.usageCounters.get(proof.index);
currentValidatorSet.usageCounters.set(
proof.index, signatureUsageCount.saturatingAdd(1)
);
vset = currentValidatorSet;
} else if (commitment.validatorSetID == nextValidatorSet.id) {
signatureUsageCount = nextValidatorSet.usageCounters.get(proof.index);
nextValidatorSet.usageCounters.set(proof.index, signatureUsageCount.saturatingAdd(1));
Expand All @@ -275,7 +273,7 @@ contract BeefyClient {
// Check if merkle proof is valid based on the validatorSetRoot and if proof is included in bitfield
if (
!isValidatorInSet(vset, proof.account, proof.index, proof.proof)
|| !Bitfield.isSet(bitfield, proof.index)
|| !Bitfield.isSet(bitfield, proof.index)
) {
revert InvalidValidatorProof();
}
Expand All @@ -289,8 +287,9 @@ contract BeefyClient {

// For the initial submission, the supplied bitfield should claim that more than
// two thirds of the validator set have sign the commitment
if (Bitfield.countSetBits(bitfield) < computeQuorum(vset.length)) {
revert NotEnoughClaims();
if (bitfield.length != Bitfield.containerLength(vset.length)
|| Bitfield.countSetBits(bitfield, vset.length) < computeQuorum(vset.length)) {
revert InvalidBitfield();
}

tickets[createTicketID(msg.sender, commitmentHash)] = Ticket({
Expand Down Expand Up @@ -361,13 +360,11 @@ contract BeefyClient {
validateTicket(ticketID, commitment, bitfield);

bool is_next_session = false;
ValidatorSetState storage vset;
ValidatorSetState storage vset = currentValidatorSet;
if (commitment.validatorSetID == nextValidatorSet.id) {
is_next_session = true;
vset = nextValidatorSet;
} else if (commitment.validatorSetID == currentValidatorSet.id) {
vset = currentValidatorSet;
} else {
} else if (commitment.validatorSetID != currentValidatorSet.id) {
revert InvalidCommitment();
}

Expand Down Expand Up @@ -444,7 +441,7 @@ contract BeefyClient {
revert InvalidBitfield();
}
return Bitfield.subsample(
ticket.prevRandao, bitfield, ticket.numRequiredSignatures, ticket.validatorSetLen
ticket.prevRandao, bitfield, ticket.validatorSetLen, ticket.numRequiredSignatures
);
}

Expand Down Expand Up @@ -488,11 +485,14 @@ contract BeefyClient {
}

/**
* @dev Calculates 2/3 majority required for quorum for a given number of validators.
* @dev Calculates majority required for quorum for a given number of validators.
* @param numValidators The number of validators in the validator set.
*/
function computeQuorum(uint256 numValidators) internal pure returns (uint256) {
return numValidators - (numValidators - 1) / 3;
if (numValidators > 3) {
return numValidators - (numValidators - 1) / 3;
}
return numValidators;
}

/**
Expand All @@ -514,18 +514,18 @@ contract BeefyClient {

// Generate final bitfield indicating which validators need to be included in the proofs.
uint256[] memory finalbitfield =
Bitfield.subsample(ticket.prevRandao, bitfield, numRequiredSignatures, vset.length);
Bitfield.subsample(ticket.prevRandao, bitfield, vset.length, numRequiredSignatures);

for (uint256 i = 0; i < proofs.length; i++) {
ValidatorProof calldata proof = proofs[i];

// Check that validator is in bitfield
if (!Bitfield.isSet(finalbitfield, proof.index)) {
// Check that validator is actually in a validator set
if (!isValidatorInSet(vset, proof.account, proof.index, proof.proof)) {
revert InvalidValidatorProof();
}

// Check that validator is actually in a validator set
if (!isValidatorInSet(vset, proof.account, proof.index, proof.proof)) {
// Check that validator is in bitfield
if (!Bitfield.isSet(finalbitfield, proof.index)) {
revert InvalidValidatorProof();
}

Expand Down
114 changes: 92 additions & 22 deletions contracts/src/utils/Bitfield.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {Bits} from "./Bits.sol";
library Bitfield {
using Bits for uint256;

error InvalidSamplingParams();

/**
* @dev Constants used to efficiently calculate the hamming weight of a bitfield. See
* https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation for an explanation of those constants.
Expand All @@ -31,41 +33,46 @@ library Bitfield {
uint256 internal constant ONE = uint256(1);

/**
* @notice Core subsampling algorithm. Draws a random number, derives an index in the bitfield, and sets the bit if it is in the `prior` and not
* yet set. Repeats that `n` times.
* @dev Core subsampling algorithm. Draws a random number, derives an index in the bitfield,
* and sets the bit if it is in the `priorBitfield` and not yet set. Repeats that `n` times.
* @param seed Source of randomness for selecting validator signatures.
* @param prior Bitfield indicating which validators claim to have signed the commitment.
* @param n Number of unique bits in prior that must be set in the result. Must be <= number of set bits in `prior`.
* @param length Length of the bitfield prior to draw bits from. Must be <= prior.length * 256.
* @param priorBitfield Bitfield indicating which validators claim to have signed the commitment.
* @param priorBitfieldSize Number of bits in priorBitfield Must be <= priorBitfield.length * 256.
* @param n Number of unique bits in priorBitfield that must be set in the output.
* Must be <= number of set bits in priorBitfield.
*/
function subsample(uint256 seed, uint256[] memory prior, uint256 n, uint256 length)
internal
pure
returns (uint256[] memory bitfield)
{
bitfield = new uint256[](prior.length);
function subsample(
uint256 seed,
uint256[] memory priorBitfield,
uint256 priorBitfieldSize,
uint256 n
) internal pure returns (uint256[] memory outputBitfield) {
if (priorBitfield.length != Bitfield.containerLength(priorBitfieldSize)
|| n > countSetBits(priorBitfield, priorBitfieldSize)) {
revert InvalidSamplingParams();
}

outputBitfield = new uint256[](priorBitfield.length);
uint256 found = 0;

for (uint256 i = 0; found < n;) {
uint256 index = makeIndex(seed, i, length);
uint256 index = makeIndex(seed, i, priorBitfieldSize);

// require randomly selected bit to be set in prior and not yet set in bitfield
if (!isSet(prior, index) || isSet(bitfield, index)) {
// require randomly selected bit to be set in priorBitfield and not yet set in bitfield
if (!isSet(priorBitfield, index) || isSet(outputBitfield, index)) {
unchecked {
i++;
}
continue;
}

set(bitfield, index);
set(outputBitfield, index);

unchecked {
found++;
i++;
}
}

return bitfield;
}

/**
Expand All @@ -76,10 +83,7 @@ library Bitfield {
pure
returns (uint256[] memory bitfield)
{
// Calculate length of uint256 array based on rounding up to number of uint256 needed
uint256 arrayLength = (length + 255) / 256;

bitfield = new uint256[](arrayLength);
bitfield = new uint256[](containerLength(length));

for (uint256 i = 0; i < bitsToSet.length; i++) {
set(bitfield, bitsToSet[i]);
Expand Down Expand Up @@ -112,6 +116,67 @@ library Bitfield {
}
}

/**
* @notice Calculates the number of set bits in the first `maxBits` bits of the bitfield.
* This is a bounded variant of `countSetBits` that only counts bits within the specified range.
*
* @dev Example usage:
* If a bitfield has bits set at positions [0, 5, 10, 256, 300]:
* - countSetBits(bitfield, 11) returns 3 (bits 0, 5, 10)
* - countSetBits(bitfield, 257) returns 4 (bits 0, 5, 10, 256)
* - countSetBits(bitfield, 1000) returns 5 (all bits)
*
* @param self The bitfield to count bits in
* @param maxBits The maximum number of bits to count (counting from bit 0)
* @return count The number of set bits in the first `maxBits` positions
*/
function countSetBits(uint256[] memory self, uint256 maxBits)
internal
pure
returns (uint256)
{
if (maxBits == 0 || self.length == 0) {
return 0;
}

unchecked {
uint256 count = 0;
uint256 fullElements = maxBits / 256;
uint256 remainingBits = maxBits % 256;

// Count bits in full 256-bit elements
for (uint256 i = 0; i < fullElements && i < self.length; i++) {
uint256 x = self[i];
x = (x & M1) + ((x >> 1) & M1); //put count of each 2 bits into those 2 bits
x = (x & M2) + ((x >> 2) & M2); //put count of each 4 bits into those 4 bits
x = (x & M4) + ((x >> 4) & M4); //put count of each 8 bits into those 8 bits
x = (x & M8) + ((x >> 8) & M8); //put count of each 16 bits into those 16 bits
x = (x & M16) + ((x >> 16) & M16); //put count of each 32 bits into those 32 bits
x = (x & M32) + ((x >> 32) & M32); //put count of each 64 bits into those 64 bits
x = (x & M64) + ((x >> 64) & M64); //put count of each 128 bits into those 128 bits
x = (x & M128) + ((x >> 128) & M128); //put count of each 256 bits into those 256 bits
count += x;
}

// Count bits in the partial element (if any)
if (remainingBits > 0 && fullElements < self.length) {
uint256 mask = (ONE << remainingBits) - 1;
uint256 x = self[fullElements] & mask;
x = (x & M1) + ((x >> 1) & M1);
x = (x & M2) + ((x >> 2) & M2);
x = (x & M4) + ((x >> 4) & M4);
x = (x & M8) + ((x >> 8) & M8);
x = (x & M16) + ((x >> 16) & M16);
x = (x & M32) + ((x >> 32) & M32);
x = (x & M64) + ((x >> 64) & M64);
x = (x & M128) + ((x >> 128) & M128);
count += x;
}

return count;
}
}

function isSet(uint256[] memory self, uint256 index) internal pure returns (bool) {
uint256 element = index >> 8;
return self[element].bit(uint8(index)) == 1;
Expand All @@ -136,11 +201,16 @@ library Bitfield {
if (length == 0) {
return 0;
}

assembly {
mstore(0x00, seed)
mstore(0x20, iteration)
index := mod(keccak256(0x00, 0x40), length)
}
}

// Calculate length of uint256 bitfield array based on rounding up to number of uint256 needed
function containerLength(uint256 bitfieldSize) internal pure returns (uint256) {
return (bitfieldSize + 255) / 256;
}
}
4 changes: 2 additions & 2 deletions contracts/test/BeefyClient.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ contract BeefyClientTest is Test {
console.log("print initialBitField");
printBitArray(bitfield);
prevRandao = uint32(vm.envOr("PREV_RANDAO", prevRandao));
finalBitfield = Bitfield.subsample(prevRandao, bitfield, numRequiredSignatures, setSize);
finalBitfield = Bitfield.subsample(prevRandao, bitfield, setSize, numRequiredSignatures);
console.log("print finalBitField");
printBitArray(finalBitfield);

Expand Down Expand Up @@ -775,7 +775,7 @@ contract BeefyClientTest is Test {
uint256[] memory initialBits = absentBitfield;
Bitfield.set(initialBits, finalValidatorProofs[0].index);
printBitArray(initialBits);
vm.expectRevert(BeefyClient.NotEnoughClaims.selector);
vm.expectRevert(BeefyClient.InvalidBitfield.selector);
beefyClient.submitInitial(commitment, initialBits, finalValidatorProofs[0]);
}

Expand Down
Loading