Skip to content

Commit

Permalink
chore: add tests, change challenge id formula
Browse files Browse the repository at this point in the history
  • Loading branch information
merklefruit committed Sep 27, 2024
1 parent 9f62934 commit d129e31
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 40 deletions.
83 changes: 44 additions & 39 deletions bolt-contracts/src/contracts/BoltChallenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ contract BoltChallenger is IBoltChallenger {
revert IncorrectChallengeBond();
}

// Compute the unique challenge ID, based on the signatures of the provided commitments
bytes32 challengeID = _computeChallengeID(commitments);

// Check that a challenge for this commitment bundle does not already exist
if (challengeIDs.contains(challengeID)) {
revert ChallengeAlreadyExists();
}

uint256 targetSlot = commitments[0].slot;
if (targetSlot > BeaconChainUtils._getCurrentSlot() - BeaconChainUtils.JUSTIFICATION_DELAY_SLOTS) {
// We cannot open challenges for slots that are not finalized by Ethereum consensus yet.
Expand Down Expand Up @@ -155,14 +163,6 @@ contract BoltChallenger is IBoltChallenger {
}
}

// Build the challenge ID: `keccak( keccak(signed tx 1) || keccak(signed tx 2) || ... || le_bytes(slot) )`
bytes32 challengeID = _computeChallengeID(commitments);

// Check that a challenge for this commitment bundle does not already exist
if (challengeIDs.contains(challengeID)) {
revert ChallengeAlreadyExists();
}

// Add the challenge to the set of challenges
challengeIDs.add(challengeID);
challenges[challengeID] = Challenge({
Expand All @@ -182,10 +182,11 @@ contract BoltChallenger is IBoltChallenger {

/// @notice Resolve a challenge by providing proofs of the inclusion of the committed transactions.
/// @dev Challenges are DEFENDED if the resolver successfully defends the inclusion of the transactions.
/// In the event of no valid defense in the challenge time window, the challenge is considered BREACHED.
/// In the event of no valid defense in the challenge time window, the challenge is considered BREACHED
/// and anyone can call `resolveExpiredChallenge()` to settle the challenge.
/// @param challengeID The ID of the challenge to resolve.
/// @param proof The proof data to resolve the challenge.
function resolveChallenge(bytes32 challengeID, Proof calldata proof) public {
function resolveOpenChallenge(bytes32 challengeID, Proof calldata proof) public {
// Check that the challenge exists
if (!challengeIDs.contains(challengeID)) {
revert ChallengeDoesNotExist();
Expand Down Expand Up @@ -233,9 +234,7 @@ contract BoltChallenger is IBoltChallenger {
}

// If the challenge has expired without being resolved, it is considered breached.
challenge.status = ChallengeStatus.Breached;
_transferFullBond(challenge.challenger);
emit ChallengeBreached(challengeID);
_settleChallengeResolution(ChallengeStatus.Breached, challenge);
}

/// @notice Resolve a challenge by providing proofs of the inclusion of the committed transactions.
Expand All @@ -257,12 +256,8 @@ contract BoltChallenger is IBoltChallenger {

if (challenge.openedAt + MAX_CHALLENGE_DURATION < Time.timestamp()) {
// If the challenge has expired without being resolved, it is considered breached.
// It's cheaper to call `resolveExpiredChallenge()` directly in this case, but this
// case should still be handled here for consistency.
challenge.status = ChallengeStatus.Breached;
_transferFullBond(challenge.challenger);
emit ChallengeBreached(challengeID);
return;
// This should be handled by calling the `resolveExpiredChallenge()` function instead.
revert ChallengeExpired();
}

// Check the integrity of the proof data
Expand Down Expand Up @@ -316,23 +311,15 @@ contract BoltChallenger is IBoltChallenger {
if (account.nonce > committedTx.nonce) {
// The tx sender (aka "challenge.commitmentReceiver") has sent a transaction with a higher nonce
// than the committed transaction, before the proposer could include it. Consider the challenge
// defended, as the proposer is not at fault. The bond will be shared between the resolver and
// commitment signer.
challenge.status = ChallengeStatus.Defended;
_transferHalfBond(msg.sender);
_transferHalfBond(challenge.commitmentSigner);
emit ChallengeDefended(challengeID);
// defended, as the proposer is not at fault.
_settleChallengeResolution(ChallengeStatus.Defended, challenge);
return;
}

if (account.balance < inclusionBlockHeader.baseFee * committedTx.gasLimit) {
// The tx sender account doesn't have enough balance to pay for the worst-case baseFee of the committed
// transaction. Consider the challenge defended, as the proposer is not at fault. The bond will be
// shared between the resolver and commitment signer.
challenge.status = ChallengeStatus.Defended;
_transferHalfBond(msg.sender);
_transferHalfBond(challenge.commitmentSigner);
emit ChallengeDefended(challengeID);
// transaction. Consider the challenge defended, as the proposer is not at fault.
_settleChallengeResolution(ChallengeStatus.Defended, challenge);
return;
}

Expand Down Expand Up @@ -364,15 +351,32 @@ contract BoltChallenger is IBoltChallenger {
}

// If all checks pass, the challenge is considered DEFENDED as the proposer provided valid proofs.
// The bond will be shared between the resolver and commitment signer.
challenge.status = ChallengeStatus.Defended;
_transferHalfBond(msg.sender);
_transferHalfBond(challenge.commitmentSigner);
emit ChallengeDefended(challengeID);
_settleChallengeResolution(ChallengeStatus.Defended, challenge);
}

// ========= HELPERS =========

/// @notice Settle the resolution of a challenge based on the outcome.
/// @dev The outcome must be either DEFENDED or BREACHED.
/// @param outcome The outcome of the challenge resolution.
/// @param challenge The challenge to settle the resolution for.
function _settleChallengeResolution(ChallengeStatus outcome, Challenge storage challenge) internal {
if (outcome == ChallengeStatus.Defended) {
// If the challenge is considered DEFENDED, the proposer has provided valid proofs.
// The bond will be shared between the resolver and commitment signer.
challenge.status = ChallengeStatus.Defended;
_transferHalfBond(msg.sender);
_transferHalfBond(challenge.commitmentSigner);
emit ChallengeDefended(challenge.id);
} else if (outcome == ChallengeStatus.Breached) {
// If the challenge is considered BREACHED, the proposer has failed to provide valid proofs.
// The bond will be transferred back to the challenger in full.
challenge.status = ChallengeStatus.Breached;
_transferFullBond(challenge.challenger);
emit ChallengeBreached(challenge.id);
}
}

/// @notice Recover the commitment data from a signed commitment.
/// @param commitment The signed commitment to recover the data from.
/// @return txSender The sender of the committed transaction.
Expand All @@ -392,17 +396,18 @@ contract BoltChallenger is IBoltChallenger {
}

/// @notice Compute the challenge ID for a given set of signed commitments.
/// @dev Formula: `keccak( keccak(signature_1) || keccak(signature_2) || ... )`
/// @param commitments The signed commitments to compute the ID for.
/// @return challengeID The computed challenge ID.
function _computeChallengeID(
SignedCommitment[] calldata commitments
) internal pure returns (bytes32) {
bytes32[] memory txHashes = new bytes32[](commitments.length);
bytes32[] memory signatures = new bytes32[](commitments.length);
for (uint256 i = 0; i < commitments.length; i++) {
txHashes[i] = keccak256(commitments[i].signedTx);
signatures[i] = keccak256(commitments[i].signature);
}

return keccak256(abi.encodePacked(txHashes, abi.encodePacked(commitments[0].slot)));
return keccak256(abi.encodePacked(signatures));
}

/// @notice Compute the commitment ID for a given signed commitment.
Expand Down
3 changes: 2 additions & 1 deletion bolt-contracts/src/interfaces/IBoltChallenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface IBoltChallenger {
error InvalidBlockNumber();
error BondTransferFailed();
error ChallengeNotExpired();
error ChallengeExpired();
error EmptyCommitments();
error UnexpectedMixedSenders();
error UnexpectedMixedSlots();
Expand Down Expand Up @@ -107,5 +108,5 @@ interface IBoltChallenger {
bytes32 challengeID
) external;

function resolveChallenge(bytes32 challengeID, Proof calldata proof) external;
function resolveOpenChallenge(bytes32 challengeID, Proof calldata proof) external;
}
49 changes: 49 additions & 0 deletions bolt-contracts/test/BoltChallenger.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,55 @@ contract BoltChallengerTest is Test {
assertEq(uint256(challenge.status), uint256(IBoltChallenger.ChallengeStatus.Defended));
}

function testResolveExpiredChallenge() public {
IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1);
commitments[0] = _parseTestCommitment();

// Open a challenge with the commitment
vm.resumeGasMetering();
vm.prank(challenger);
boltChallenger.openChallenge{value: 1 ether}(commitments);
vm.pauseGasMetering();

// Check the challenge was opened
IBoltChallenger.Challenge[] memory challenges = boltChallenger.getAllChallenges();
assertEq(challenges.length, 1);

// Warp time to make the challenge expire
vm.warp(block.timestamp + 2 weeks);

// Try to resolve the challenge
vm.prank(resolver);
boltChallenger.resolveExpiredChallenge(challenges[0].id);

// Check the challenge was resolved
IBoltChallenger.Challenge memory challenge = boltChallenger.getAllChallenges()[0];
assertEq(uint256(challenge.status), uint256(IBoltChallenger.ChallengeStatus.Breached));
}

function testCannotResolveChallengeBeforeExpiration() public {
IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1);
commitments[0] = _parseTestCommitment();

// Open a challenge with the commitment
vm.resumeGasMetering();
vm.prank(challenger);
boltChallenger.openChallenge{value: 1 ether}(commitments);
vm.pauseGasMetering();

// Check the challenge was opened
IBoltChallenger.Challenge[] memory challenges = boltChallenger.getAllChallenges();
assertEq(challenges.length, 1);
bytes32 id = challenges[0].id;

// Try to resolve the challenge before it expires
vm.resumeGasMetering();
vm.prank(resolver);
vm.expectRevert(IBoltChallenger.ChallengeNotExpired.selector);
boltChallenger.resolveExpiredChallenge(id);
vm.pauseGasMetering();
}

// =========== Helper functions ===========

// Helper to create a test commitment with a recent slot, valid for a recent challenge
Expand Down

0 comments on commit d129e31

Please sign in to comment.