Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f364999
Benchmark beefy by signatures
yrong Apr 18, 2025
6d17159
More samples
yrong Apr 18, 2025
aeef3a2
Cleanup
yrong Apr 23, 2025
e3734b3
SubmitFiatShamir
yrong Apr 25, 2025
2d55aa0
Add CreateFiatShamirFinalBitfield
yrong Apr 25, 2025
5bea664
Make requiredSignatures bounded
yrong Apr 28, 2025
f2ed8d2
Beefy relayer
yrong Apr 28, 2025
4f978c0
Cleanup
yrong Apr 28, 2025
bb5c9d2
Merge branch 'ron/benchmark-beefy-by-signatures' into ron/submitFiatS…
yrong Apr 30, 2025
a61d4ca
Generate test fixture
yrong Apr 30, 2025
a34b83d
Merge branch 'main' into ron/benchmark-beefy-by-signatures
yrong Apr 30, 2025
1458b8c
Merge branch 'ron/benchmark-beefy-by-signatures' into ron/submitFiatS…
yrong Apr 30, 2025
e48f5c4
testSubmitFiatShamirWithHandOver
yrong May 2, 2025
afc04c1
Fix compile error
yrong May 2, 2025
ec230b8
Use sha256 as hash function
yrong May 6, 2025
9da095f
Update test fixture
yrong May 6, 2025
e277eb7
Update quorum function
yrong May 6, 2025
b94851e
Switch to sha256d
yrong May 11, 2025
dcc7588
Initialize fiatShamirRequiredSignatures via the constructor
yrong May 11, 2025
b844164
Add an environment variable for local setup
yrong May 12, 2025
98b57d1
Merge branch 'main' into ron/submitFiatShamir
yrong Sep 8, 2025
042e546
Update contract binding
yrong Sep 8, 2025
2aeb930
Submit FiatShamir on demand
yrong Sep 8, 2025
b7ffc9e
Check for outdated commitments in the two-phase commit
yrong Sep 9, 2025
adf074a
Add an option to accelerate the transfer
yrong Sep 10, 2025
ee36155
Merge branch 'main' into ron/submitFiatShamir
yrong Sep 10, 2025
3e62355
Merge branch 'ron/submitFiatShamir' into ron/submitFiatShamir-fe
yrong Sep 10, 2025
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
4 changes: 4 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ ignored_error_codes = [
2394,
]

# To clean up, we disabled 3 signature tests. You can regenerate the test fixtures by setting the environment variable
# SIGNATURE_USAGE_COUNT=3 and enabling these tests again.
no_match_test = "testSubmitWith3SignatureCount*"

evm_version = 'Cancun'

[profile.production]
Expand Down
5 changes: 4 additions & 1 deletion contracts/scripts/DeployBeefyClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ contract DeployBeefyClient is Script {
uint256 randaoCommitDelay;
uint256 randaoCommitExpiration;
uint256 minimumSignatures;
uint256 fiatShamirRequiredSignatures;
}

function readConfig() internal pure returns (Config memory config) {
Expand All @@ -31,7 +32,8 @@ contract DeployBeefyClient is Script {
}),
randaoCommitDelay: 128,
randaoCommitExpiration: 24,
minimumSignatures: 17
minimumSignatures: 17,
fiatShamirRequiredSignatures: 101
});
}

Expand All @@ -43,6 +45,7 @@ contract DeployBeefyClient is Script {
config.randaoCommitDelay,
config.randaoCommitExpiration,
config.minimumSignatures,
config.fiatShamirRequiredSignatures,
config.startBlock,
config.current,
config.next
Expand Down
9 changes: 8 additions & 1 deletion contracts/scripts/DeployLocal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ contract DeployLocal is Script {
uint256 randaoCommitDelay = vm.envUint("RANDAO_COMMIT_DELAY");
uint256 randaoCommitExpiration = vm.envUint("RANDAO_COMMIT_EXP");
uint256 minimumSignatures = vm.envUint("MINIMUM_REQUIRED_SIGNATURES");
uint256 fiatShamirRequiredSignatures = vm.envUint("FIAT_SHAMIR_REQUIRED_SIGNATURES");
BeefyClient beefyClient = new BeefyClient(
randaoCommitDelay, randaoCommitExpiration, minimumSignatures, startBlock, current, next
randaoCommitDelay,
randaoCommitExpiration,
minimumSignatures,
fiatShamirRequiredSignatures,
startBlock,
current,
next
);

uint8 foreignTokenDecimals = uint8(vm.envUint("FOREIGN_TOKEN_DECIMALS"));
Expand Down
142 changes: 140 additions & 2 deletions contracts/src/BeefyClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ contract BeefyClient {
*/
uint256 public immutable minNumRequiredSignatures;

uint256 public immutable fiatShamirRequiredSignatures;

/* Errors */
error InvalidBitfield();
error InvalidBitfieldLength();
Expand All @@ -218,6 +220,7 @@ contract BeefyClient {
uint256 _randaoCommitDelay,
uint256 _randaoCommitExpiration,
uint256 _minNumRequiredSignatures,
uint256 _fiatShamirRequiredSignatures,
uint64 _initialBeefyBlock,
ValidatorSet memory _initialValidatorSet,
ValidatorSet memory _nextValidatorSet
Expand All @@ -228,6 +231,7 @@ contract BeefyClient {
randaoCommitDelay = _randaoCommitDelay;
randaoCommitExpiration = _randaoCommitExpiration;
minNumRequiredSignatures = _minNumRequiredSignatures;
fiatShamirRequiredSignatures = _fiatShamirRequiredSignatures;
latestBeefyBlock = _initialBeefyBlock;
currentValidatorSet.id = _initialValidatorSet.id;
currentValidatorSet.length = _initialValidatorSet.length;
Expand Down Expand Up @@ -448,6 +452,96 @@ contract BeefyClient {
);
}

/**
* @dev Helper to create a final bitfield with subsampled validator selections using the Fiat-Shamir approach
* @param commitment contains the full commitment that was used for the commitmentHash
* @param bitfield claiming which validators have signed the commitment
*/
function createFiatShamirFinalBitfield(
Commitment calldata commitment,
uint256[] calldata bitfield
) external view returns (uint256[] memory) {
ValidatorSetState storage vset;
if (commitment.validatorSetID == nextValidatorSet.id) {
vset = nextValidatorSet;
} else if (commitment.validatorSetID == currentValidatorSet.id) {
vset = currentValidatorSet;
} else {
revert InvalidCommitment();
}

bytes32 bitFieldHash = keccak256(abi.encodePacked(bitfield));
bytes32 commitmentHash = keccak256(encodeCommitment(commitment));
bytes32 fiatShamirHash =
sha256(bytes.concat(sha256(bytes.concat(commitmentHash, bitFieldHash, vset.root))));
uint256 requiredSignatures =
Math.min(fiatShamirRequiredSignatures, computeQuorum(vset.length));
return
Bitfield.subsample(uint256(fiatShamirHash), bitfield, requiredSignatures, vset.length);
}

/**
* @dev Submit a commitment and leaf using the Fiat-Shamir approach
* @param commitment contains the full commitment that was used for the commitmentHash
* @param bitfield claiming which validators have signed the commitment
* @param proofs a struct containing the data needed to verify all validator signatures
* @param leaf an MMR leaf provable using the MMR root in the commitment payload
* @param leafProof an MMR leaf proof
* @param leafProofOrder a bitfield describing the order of each item (left vs right)
*/
function submitFiatShamir(
Commitment calldata commitment,
uint256[] calldata bitfield,
ValidatorProof[] calldata proofs,
MMRLeaf calldata leaf,
bytes32[] calldata leafProof,
uint256 leafProofOrder
) external {
if (commitment.blockNumber <= latestBeefyBlock) {
// ticket is obsolete
revert StaleCommitment();
}

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

bytes32 commitmentHash = keccak256(encodeCommitment(commitment));

verifyFiatShamirCommitment(commitmentHash, bitfield, vset, proofs);

bytes32 newMMRRoot = ensureProvidesMMRRoot(commitment);

if (is_next_session) {
if (leaf.nextAuthoritySetID != nextValidatorSet.id + 1) {
revert InvalidMMRLeaf();
}
bool leafIsValid = MMRProof.verifyLeafProof(
newMMRRoot, keccak256(encodeMMRLeaf(leaf)), leafProof, leafProofOrder
);
if (!leafIsValid) {
revert InvalidMMRLeafProof();
}
currentValidatorSet = nextValidatorSet;
nextValidatorSet.id = leaf.nextAuthoritySetID;
nextValidatorSet.length = leaf.nextAuthoritySetLen;
nextValidatorSet.root = leaf.nextAuthoritySetRoot;
nextValidatorSet.usageCounters = createUint16Array(leaf.nextAuthoritySetLen);
}

latestMMRRoot = newMMRRoot;
latestBeefyBlock = commitment.blockNumber;

emit NewMMRRoot(newMMRRoot, commitment.blockNumber);
}

/* Internal Functions */

// Creates a unique ticket ID for a new interactive prover-verifier session
Expand Down Expand Up @@ -488,11 +582,11 @@ contract BeefyClient {
}

/**
* @dev Calculates 2/3 majority required for quorum for a given number of validators.
* @dev We have 2/3rd +1 honesty assumption on polkadot validators. Hence it is sufficient to check 1/3rd +1 validator signatures to ensure at least 1 honest validator signed the payload.
* @param numValidators The number of validators in the validator set.
*/
function computeQuorum(uint256 numValidators) internal pure returns (uint256) {
return numValidators - (numValidators - 1) / 3;
return numValidators / 3 + 1;
}

/**
Expand Down Expand Up @@ -539,6 +633,50 @@ contract BeefyClient {
}
}

/**
* @dev Verify commitment with the sampled signatures using the Fiat-Shamir hash
*/
function verifyFiatShamirCommitment(
bytes32 commitmentHash,
uint256[] calldata bitfield,
ValidatorSetState storage vset,
ValidatorProof[] calldata proofs
) internal view {
bytes32 bitFieldHash = keccak256(abi.encodePacked(bitfield));
bytes32 fiatShamirHash =
sha256(bytes.concat(sha256(bytes.concat(commitmentHash, bitFieldHash, vset.root))));
uint256 requiredSignatures =
Math.min(fiatShamirRequiredSignatures, computeQuorum(vset.length));
if (proofs.length != requiredSignatures) {
revert InvalidValidatorProofLength();
}

uint256[] memory finalbitfield =
Bitfield.subsample(uint256(fiatShamirHash), bitfield, requiredSignatures, vset.length);

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)) {
revert InvalidValidatorProof();
}

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

// Check that validator signed the commitment
if (ECDSA.recover(commitmentHash, proof.v, proof.r, proof.s) != proof.account) {
revert InvalidSignature();
}

// Ensure no validator can appear more than once in bitfield
Bitfield.unset(finalbitfield, proof.index);
}
}

// Ensure that the commitment provides a new MMR root
function ensureProvidesMMRRoot(Commitment calldata commitment)
internal
Expand Down
Loading
Loading