From cf9f5bb74148128955ea87a57772fd808d16544b Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:05:27 +0200 Subject: [PATCH 1/9] feat: added stacked preconfs in the same slot by the same sender logic --- bolt-contracts/.gas-snapshot | 16 +- .../src/contracts/BoltChallenger.sol | 302 ++++++++++++------ .../src/interfaces/IBoltChallenger.sol | 48 ++- bolt-contracts/test/BoltChallenger.t.sol | 88 ++--- .../test/testdata/eth_proof_20785011.json | 22 ++ ...eth_proof.json => eth_proof_20785012.json} | 0 .../test/testdata/header_20785011.json | 5 + .../{raw_header.json => header_20785012.json} | 0 ..._proof.json => tx_mpt_proof_20785012.json} | 0 9 files changed, 330 insertions(+), 151 deletions(-) create mode 100644 bolt-contracts/test/testdata/eth_proof_20785011.json rename bolt-contracts/test/testdata/{eth_proof.json => eth_proof_20785012.json} (100%) create mode 100644 bolt-contracts/test/testdata/header_20785011.json rename bolt-contracts/test/testdata/{raw_header.json => header_20785012.json} (100%) rename bolt-contracts/test/testdata/{tx_mpt_proof.json => tx_mpt_proof_20785012.json} (100%) diff --git a/bolt-contracts/.gas-snapshot b/bolt-contracts/.gas-snapshot index 353740bd2..5dffb08e7 100644 --- a/bolt-contracts/.gas-snapshot +++ b/bolt-contracts/.gas-snapshot @@ -1,15 +1,15 @@ BoltChallengerTest:testCommitmentDigestAndSignature() (gas: 4626) BoltChallengerTest:testCommitmentSignature() (gas: 6754) -BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 16032) -BoltChallengerTest:testOpenChallenge() (gas: 379747) -BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 22416) -BoltChallengerTest:testOpenChallengeWithInsufficientBond() (gas: 16738) -BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 388679) -BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 17029) +BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 111137) +BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 23952) +BoltChallengerTest:testOpenChallengeSingleTx() (gas: 406261) +BoltChallengerTest:testOpenChallengeWithInsufficientBond() (gas: 17220) +BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 415209) +BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 17622) BoltChallengerTest:testProveAccountData() (gas: 355542) -BoltChallengerTest:testProveHeaderData() (gas: 45617) +BoltChallengerTest:testProveHeaderData() (gas: 46228) BoltChallengerTest:testProveTransactionInclusion() (gas: 176543) -BoltChallengerTest:testResolveChallengeFullDefense() (gas: 575605) +BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 698603) BoltManagerEigenLayerTest:testGetNonExistentProposerStatus() (gas: 921620) BoltManagerEigenLayerTest:testGetWhitelistedCollaterals() (gas: 99988) BoltManagerEigenLayerTest:testNonWhitelistedCollateral() (gas: 103013) diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol index 459f135a6..7bf1eb90e 100644 --- a/bolt-contracts/src/contracts/BoltChallenger.sol +++ b/bolt-contracts/src/contracts/BoltChallenger.sol @@ -31,7 +31,7 @@ contract BoltChallenger is IBoltChallenger { // ========= CONSTANTS ========= /// @notice The challenge bond required to open a challenge. - uint256 public constant CHALLENGE_BOND = 1 ether; + uint256 public constant COMMITMENT_BOND = 1 ether; /// @notice The maximum duration of a challenge to be considered valid. uint256 public constant MAX_CHALLENGE_DURATION = 7 days; @@ -98,52 +98,93 @@ contract BoltChallenger is IBoltChallenger { // ========= CHALLENGE CREATION ========= - // Q: should we add a commit-reveal scheme to prevent frontrunning to steal slashing rewards? + /// @notice Open a challenge against a bundle of committed transactions. + /// @dev The challenge bond must be paid in order to open a challenge. + /// @param commitments The signed commitments to open a challenge for. function openChallenge( - SignedCommitment calldata commitment + SignedCommitment[] calldata commitments ) public payable { + if (commitments.length == 0) { + revert EmptyCommitments(); + } + // Check that the challenge bond is sufficient - if (msg.value < CHALLENGE_BOND) { + uint256 expectedBond = COMMITMENT_BOND * commitments.length; + if (msg.value < expectedBond) { revert InsufficientChallengeBond(); - } else if (msg.value > CHALLENGE_BOND) { + } else if (msg.value > expectedBond) { // Refund the excess value, if any - payable(msg.sender).transfer(msg.value - CHALLENGE_BOND); + payable(msg.sender).transfer(msg.value - expectedBond); } - if (commitment.slot > BeaconChainUtils._getCurrentSlot() - BeaconChainUtils.JUSTIFICATION_DELAY_SLOTS) { + 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. // This is admittedly a bit strict, since 32-slot deep reorgs are very unlikely. revert BlockIsNotFinalized(); } - // Reconstruct the commitment digest: `keccak( keccak(signed tx) || le_bytes(slot) )` - bytes32 commitmentID = _computeCommitmentID(commitment); + // Check that all commitments are for the same slot and signed by the same sender + // and store the parsed transaction data for each commitment + TransactionData[] memory transactionsData = new TransactionData[](commitments.length); + (address txSender, address commitmentSigner, TransactionData memory firstTransactionData) = + _recoverCommitmentData(commitments[0]); + + transactionsData[0] = firstTransactionData; + + for (uint256 i = 1; i < commitments.length; i++) { + (address otherTxSender, address otherCommitmentSigner, TransactionData memory otherTransactionData) = + _recoverCommitmentData(commitments[i]); + + transactionsData[i] = otherTransactionData; + + // check that all commitments are for the same slot + if (commitments[i].slot != targetSlot) { + revert UnexpectedMixedSlots(); + } + + // check that all commitments are signed by the same sender + if (otherTxSender != txSender) { + revert UnexpectedMixedSenders(); + } + + // check that all commitments are signed by the same signer (aka "operator") + if (otherCommitmentSigner != commitmentSigner) { + revert UnexpectedMixedSigners(); + } + + // check that the nonces are strictly sequentially increasing in the bundle + if (otherTransactionData.nonce != transactionsData[i - 1].nonce + 1) { + revert UnexpectedNonceOrder(); + } + } - // Verify the commitment signature against the digest - address commitmentSigner = ECDSA.recover(commitmentID, commitment.signature); + // 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 does not already exist - if (challengeIDs.contains(commitmentID)) { + // 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(commitmentID); - challenges[commitmentID] = Challenge({ - id: commitmentID, + challengeIDs.add(challengeID); + challenges[challengeID] = Challenge({ + id: challengeID, openedAt: Time.timestamp(), status: ChallengeStatus.Open, + targetSlot: targetSlot, challenger: msg.sender, commitmentSigner: commitmentSigner, - commitment: commitment + commitmentReceiver: txSender, + committedTxs: transactionsData }); - - emit ChallengeOpened(commitmentID, msg.sender, commitmentSigner); + emit ChallengeOpened(challengeID, msg.sender, commitmentSigner); } // ========= CHALLENGE RESOLUTION ========= - function resolveRecentChallenge(bytes32 challengeID, Proof calldata proof) public { + function resolveChallenge(bytes32 challengeID, Proof calldata proof) public { // Check that the challenge exists if (!challengeIDs.contains(challengeID)) { revert ChallengeDoesNotExist(); @@ -151,31 +192,29 @@ contract BoltChallenger is IBoltChallenger { // The visibility of the BLOCKHASH opcode is limited to the 256 most recent blocks. // For simplicity we restrict this to 256 slots even though 256 blocks would be more accurate. - if (challenges[challengeID].commitment.slot < BeaconChainUtils._getCurrentSlot() - BLOCKHASH_EVM_LOOKBACK) { + if (challenges[challengeID].targetSlot < BeaconChainUtils._getCurrentSlot() - BLOCKHASH_EVM_LOOKBACK) { revert BlockIsTooOld(); } - // Check that the block number is within the EVM lookback window for block hashes - if (proof.blockNumber > block.number || proof.blockNumber < block.number - BLOCKHASH_EVM_LOOKBACK) { + // Check that the previous block is within the EVM lookback window for block hashes. + if ( + (proof.inclusionBlockNumber - 1) > block.number + || (proof.inclusionBlockNumber - 1) < block.number - BLOCKHASH_EVM_LOOKBACK + ) { revert InvalidBlockNumber(); } - // Get the trusted block hash for the block number in which the transaction was included. - bytes32 trustedBlockHash = blockhash(proof.blockNumber); + // Get the trusted block hash for the block number in which the transactions were included. + bytes32 trustedPreviousBlockHash = blockhash(proof.inclusionBlockNumber); // Finally resolve the challenge with the trusted block hash and the provided proofs - _resolve(challengeID, trustedBlockHash, proof); - } - - // Resolving a historical challenge requires acquiring a block hash from an alternative source - // from the EVM. This is because the BLOCKHASH opcode is limited to the 256 most recent blocks. - function resolveChallenge(bytes32 challengeID, Proof calldata proof) public { - // unimplemented!(); + _resolve(challengeID, trustedPreviousBlockHash, proof); } /// @notice Resolve a challenge that has expired without being resolved. - /// @dev This will result in the challenge being considered lost, without need to provide + /// @dev This will result in the challenge being considered breached, without need to provide /// additional proofs of inclusion, as the time window has elapsed. + /// @param challengeID The ID of the challenge to resolve. function resolveExpiredChallenge( bytes32 challengeID ) public { @@ -183,7 +222,6 @@ contract BoltChallenger is IBoltChallenger { revert ChallengeDoesNotExist(); } - // The challenge is assumed to exist at this point, so we can safely access it. Challenge storage challenge = challenges[challengeID]; if (challenge.status != ChallengeStatus.Open) { @@ -194,20 +232,23 @@ contract BoltChallenger is IBoltChallenger { revert ChallengeNotExpired(); } - // If the challenge has expired without being resolved, it is considered lost. - challenge.status = ChallengeStatus.Lost; + // If the challenge has expired without being resolved, it is considered breached. + challenge.status = ChallengeStatus.Breached; _transferFullBond(challenge.challenger); - emit ChallengeLost(challengeID); + emit ChallengeBreached(challengeID); } - /// @notice Resolve a challenge by providing proofs of the inclusion of the committed transaction. - /// @dev Challenges are DEFENDED if the resolver successfully defends the inclusion of the transaction, - /// and LOST if the challenger successfully demonstrates that the inclusion commitment was breached or - /// enough time has passed without proper resolution. - /// - /// q: should we also have a commit-reveal scheme for resolutions to avoid frontrunning to steal bonds? - function _resolve(bytes32 challengeID, bytes32 trustedBlockHash, Proof calldata proof) internal { - // The challenge is assumed to exist at this point, so we can safely access it. + /// @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. + /// @param challengeID The ID of the challenge to resolve. + /// @param trustedPreviousBlockHash The block hash of the block before the inclusion block of the committed txs. + /// @param proof The proof data to resolve the challenge. See `IBoltChallenger.Proof` struct for more details. + function _resolve(bytes32 challengeID, bytes32 trustedPreviousBlockHash, Proof calldata proof) internal { + if (!challengeIDs.contains(challengeID)) { + revert ChallengeDoesNotExist(); + } + Challenge storage challenge = challenges[challengeID]; if (challenge.status != ChallengeStatus.Open) { @@ -215,80 +256,114 @@ contract BoltChallenger is IBoltChallenger { } if (challenge.openedAt + MAX_CHALLENGE_DURATION < Time.timestamp()) { - // If the challenge has expired without being resolved, it is considered lost. - challenge.status = ChallengeStatus.Lost; + // 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 ChallengeLost(challengeID); + emit ChallengeBreached(challengeID); return; } - // Verify the validity of the header against the trusted block hash. - if (keccak256(proof.blockHeaderRLP) != trustedBlockHash) { - revert InvalidBlockHash(); + // Check theintegrity of the proofs + if ( + proof.txMerkleProofs.length != challenge.committedTxs.length + || proof.txIndexesInBlock.length != challenge.committedTxs.length + ) { + revert InvalidProofsLength(); } - // Decode the RLP-encoded block header fields - BlockHeaderData memory blockHeader = _decodeBlockHeaderRLP(proof.blockHeaderRLP); + // Check the integrity of the trusted block hash + if (keccak256(proof.previousBlockHeaderRLP) != trustedPreviousBlockHash) { + revert InvalidBlockHash(); + } - // Decode the committed raw signed transaction. Its sender will be the account to prove existence of. - TransactionDecoder.Transaction memory decodedTx = challenge.commitment.signedTx.decodeEnveloped(); + // Decode the RLP-encoded block header of the previous block to the inclusion block. + // + // The previous block's state root is necessary to verify the account had the correct balance and + // nonce at the top of the inclusion block (before any transactions were applied). + BlockHeaderData memory previousBlockHeader = _decodeBlockHeaderRLP(proof.previousBlockHeaderRLP); + + // Decode the RLP-encoded block header of the inclusion block. + // + // The inclusion block is necessary to extract the transaction root and verify the inclusion of the + // committed transactions. By checking against the previous block's parent hash we can ensure this + // is the correct block trusting a single block hash. + BlockHeaderData memory inclusionBlockHeader = _decodeBlockHeaderRLP(proof.inclusionBlockHeaderRLP); + + // Check that the inclusion block is a child of the previous block + if (inclusionBlockHeader.parentHash != keccak256(proof.previousBlockHeaderRLP)) { + revert InvalidParentBlockHash(); + } - // Decode the account fields by checking the account proof against the state root of the block header. - // The key in the account trie is the account pubkey (address) which we can recover from the signed tx. + // Decode the account fields by checking the account proof against the state root of the previous block header. + // The key in the account trie is the account pubkey (address) that sent the committed transactions. (bool accountExists, bytes memory accountRLP) = SecureMerkleTrie.get( - abi.encodePacked(decodedTx.recoverSender()), proof.accountMerkleProof, blockHeader.stateRoot + abi.encodePacked(challenge.commitmentReceiver), proof.accountMerkleProof, previousBlockHeader.stateRoot ); if (!accountExists) { revert AccountDoesNotExist(); } + // Extract the nonce and balance of the account from the RLP-encoded data AccountData memory account = _decodeAccountRLP(accountRLP); - if (account.nonce > decodedTx.nonce) { - // The transaction recovered sender 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); - return; - } else if (account.nonce < decodedTx.nonce) { - // Q: is this a valid case? technically the proposer would be at fault for accepting a commitment of an - // already included transaction. TBD. - } + // Loop through each committed transaction and verify its inclusion in the block + // along with the sender's balance and nonce (starting from the account state at the top of the block). + for (uint256 i = 0; i < challenge.committedTxs.length; i++) { + TransactionData memory committedTx = challenge.committedTxs[i]; + + 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); + return; + } - if (account.balance < blockHeader.baseFee * decodedTx.gasLimit) { - // The account does not have enough balance to pay for the worst-case base fee 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); - 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); + return; + } - // The key in the transaction trie is the RLP-encoded index of the transaction in the block - bytes memory txLeaf = RLPWriter.writeUint(proof.txIndexInBlock); + // Over/Underflow is checked in the previous if statements. + // This is the same logic applied by the Bolt Sidecar's off-chain checks + // before deciding to sign a new commitment. + account.nonce++; + account.balance -= inclusionBlockHeader.baseFee * committedTx.gasLimit; - // Verify transaction inclusion proof - // Note: the transactions trie is built with raw leaves, without hashing them first. - // This denotes why we use `MerkleTrie.get()` as opposed to `SecureMerkleTrie.get()` here. - (bool txExists, bytes memory txRLP) = MerkleTrie.get(txLeaf, proof.txMerkleProof, blockHeader.txRoot); + // The key in the transaction trie is the RLP-encoded index of the transaction in the block + bytes memory txLeaf = RLPWriter.writeUint(proof.txIndexesInBlock[i]); - if (!txExists) { - revert TransactionNotIncluded(); - } + // Verify transaction inclusion proof + // + // The transactions trie is built with raw leaves, without hashing them first + // (This denotes why we use `MerkleTrie.get()` as opposed to `SecureMerkleTrie.get()`). + (bool txExists, bytes memory txRLP) = + MerkleTrie.get(txLeaf, proof.txMerkleProofs[i], inclusionBlockHeader.txRoot); - // Decode the txRLP and check if it matches the committed transaction - // TODO: q: is txRLP also envelope encoded? if not, this check will fail. - if (keccak256(challenge.commitment.signedTx) != keccak256(txRLP)) { - revert WrongTransactionHashProof(); + if (!txExists) { + revert TransactionNotIncluded(); + } + + // Check if the committed transaction hash matches the hash of the included transaction + if (committedTx.txHash != keccak256(txRLP)) { + revert WrongTransactionHashProof(); + } } - // If all checks pass, the challenge is considered defended as the proposer defended with valid proofs. + // 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); @@ -298,6 +373,38 @@ contract BoltChallenger is IBoltChallenger { // ========= HELPERS ========= + /// @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. + /// @return commitmentSigner The signer of the commitment. + /// @return transactionData The decoded transaction data of the committed transaction. + function _recoverCommitmentData( + SignedCommitment calldata commitment + ) internal pure returns (address txSender, address commitmentSigner, TransactionData memory transactionData) { + commitmentSigner = ECDSA.recover(_computeCommitmentID(commitment), commitment.signature); + TransactionDecoder.Transaction memory decodedTx = commitment.signedTx.decodeEnveloped(); + txSender = decodedTx.recoverSender(); + transactionData = TransactionData({ + txHash: keccak256(commitment.signedTx), + nonce: decodedTx.nonce, + gasLimit: decodedTx.gasLimit + }); + } + + /// @notice Compute the challenge ID for a given set of signed commitments. + /// @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); + for (uint256 i = 0; i < commitments.length; i++) { + txHashes[i] = keccak256(commitments[i].signedTx); + } + + return keccak256(abi.encodePacked(txHashes, abi.encodePacked(commitments[0].slot))); + } + /// @notice Compute the commitment ID for a given signed commitment. /// @param commitment The signed commitment to compute the ID for. /// @return commitmentID The computed commitment ID. @@ -314,6 +421,7 @@ contract BoltChallenger is IBoltChallenger { ) internal pure returns (BlockHeaderData memory blockHeader) { RLPReader.RLPItem[] memory headerFields = headerRLP.toRLPItem().readList(); + blockHeader.parentHash = headerFields[0].readBytes32(); blockHeader.stateRoot = headerFields[3].readBytes32(); blockHeader.txRoot = headerFields[4].readBytes32(); blockHeader.blockNumber = headerFields[8].readUint256(); @@ -338,7 +446,7 @@ contract BoltChallenger is IBoltChallenger { function _transferFullBond( address recipient ) internal { - (bool success,) = payable(recipient).call{value: CHALLENGE_BOND}(""); + (bool success,) = payable(recipient).call{value: COMMITMENT_BOND}(""); if (!success) { revert BondTransferFailed(); } @@ -349,7 +457,7 @@ contract BoltChallenger is IBoltChallenger { function _transferHalfBond( address recipient ) internal { - (bool success,) = payable(recipient).call{value: CHALLENGE_BOND / 2}(""); + (bool success,) = payable(recipient).call{value: COMMITMENT_BOND / 2}(""); if (!success) { revert BondTransferFailed(); } diff --git a/bolt-contracts/src/interfaces/IBoltChallenger.sol b/bolt-contracts/src/interfaces/IBoltChallenger.sol index de67078bd..fb603de89 100644 --- a/bolt-contracts/src/interfaces/IBoltChallenger.sol +++ b/bolt-contracts/src/interfaces/IBoltChallenger.sol @@ -5,16 +5,18 @@ interface IBoltChallenger { enum ChallengeStatus { Open, Defended, - Lost + Breached } struct Challenge { bytes32 id; uint48 openedAt; ChallengeStatus status; + uint256 targetSlot; address challenger; address commitmentSigner; - SignedCommitment commitment; + address commitmentReceiver; + TransactionData[] committedTxs; } struct SignedCommitment { @@ -23,7 +25,14 @@ interface IBoltChallenger { bytes signedTx; } + struct TransactionData { + bytes32 txHash; + uint256 nonce; + uint256 gasLimit; + } + struct BlockHeaderData { + bytes32 parentHash; bytes32 stateRoot; bytes32 txRoot; uint256 blockNumber; @@ -37,11 +46,23 @@ interface IBoltChallenger { } struct Proof { - bytes blockHeaderRLP; + // block number where the transactions are included + uint256 inclusionBlockNumber; + // RLP-encoded block header of the previous block of the inclusion block + // (for clarity: `previousBlockHeader.number == inclusionBlockNumber - 1`) + bytes previousBlockHeaderRLP; + // RLP-encoded block header where the committed transactions are included + bytes inclusionBlockHeaderRLP; + // merkle inclusion proof of the account in the state trie of the previous block + // (checked against the previousBlockHeader.stateRoot) bytes accountMerkleProof; - bytes txMerkleProof; - uint256 blockNumber; - uint256 txIndexInBlock; + // merkle inclusion proof of the transactions in the transaction trie of the inclusion block + // (checked against the inclusionBlockHeader.txRoot). The order of the proofs should match + // the order of the committed transactions in the challenge: `Challenge.committedTxs`. + bytes[] txMerkleProofs; + // indexes of the committed transactions in the block. The order of the indexes should match + // the order of the committed transactions in the challenge: `Challenge.committedTxs`. + uint256[] txIndexesInBlock; } error SlotInTheFuture(); @@ -52,16 +73,23 @@ interface IBoltChallenger { error ChallengeDoesNotExist(); error BlockIsTooOld(); error InvalidBlockHash(); + error InvalidParentBlockHash(); error AccountDoesNotExist(); error TransactionNotIncluded(); error WrongTransactionHashProof(); error InvalidBlockNumber(); error BondTransferFailed(); error ChallengeNotExpired(); + error EmptyCommitments(); + error UnexpectedMixedSenders(); + error UnexpectedMixedSlots(); + error UnexpectedMixedSigners(); + error UnexpectedNonceOrder(); + error InvalidProofsLength(); event ChallengeOpened(bytes32 indexed challengeId, address indexed challenger, address indexed commitmentSigner); event ChallengeDefended(bytes32 indexed challengeId); - event ChallengeLost(bytes32 indexed challengeId); + event ChallengeBreached(bytes32 indexed challengeId); function getAllChallenges() external view returns (Challenge[] memory); @@ -72,10 +100,12 @@ interface IBoltChallenger { ) external view returns (Challenge memory); function openChallenge( - SignedCommitment calldata commitment + SignedCommitment[] calldata commitments ) external payable; - function resolveRecentChallenge(bytes32 challengeID, Proof calldata proof) external; + function resolveExpiredChallenge( + bytes32 challengeID + ) external; function resolveChallenge(bytes32 challengeID, Proof calldata proof) external; } diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index e5a7c6da0..c3f2f0fae 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -67,7 +67,7 @@ contract BoltChallengerTest is Test { bytes32 trustedBlockHash = 0x0fc7c840f5b4b451e99dc8adb0d475eab2ac7d36278d9601d7f4b2dd05e8022f; // Read the RLP-encoded block header from a file (obtained via `debug_getRawHeader` RPC call) - string memory file = vm.readFile("./test/testdata/raw_header.json"); + string memory file = vm.readFile("./test/testdata/header_20785012.json"); bytes memory headerRLP = vm.parseJsonBytes(file, ".result"); assertEq(keccak256(headerRLP), trustedBlockHash); @@ -94,7 +94,7 @@ contract BoltChallengerTest is Test { // Read the RLP-encoded account proof from a file. This is obtained from the `eth_getProof` // RPC call + ABI-encoding of the resulting accountProof array. - string memory file = vm.readFile("./test/testdata/eth_proof.json"); + string memory file = vm.readFile("./test/testdata/eth_proof_20785012.json"); bytes[] memory accountProofJson = vm.parseJsonBytesArray(file, ".result.accountProof"); bytes memory accountProof = _RLPEncodeList(accountProofJson); @@ -127,7 +127,7 @@ contract BoltChallengerTest is Test { bytes32 txHash = 0x9ec2c56ca36e445a46bc77ca77510f0ef21795d00834269f3752cbd29d63ba1f; // MPT proof, obtained with the `eth-trie-proof` CLI tool - string memory file = vm.readFile("./test/testdata/tx_mpt_proof.json"); + string memory file = vm.readFile("./test/testdata/tx_mpt_proof_20785012.json"); bytes[] memory txProofJson = vm.parseJsonBytesArray(file, ".proof"); bytes memory txProof = _RLPEncodeList(txProofJson); @@ -198,15 +198,16 @@ contract BoltChallengerTest is Test { // =========== Opening a challenge =========== - function testOpenChallenge() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); + function testOpenChallengeSingleTx() public { + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); assertEq(challenger.balance, 100 ether); // Open a challenge with the commitment vm.resumeGasMetering(); vm.prank(challenger); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); vm.pauseGasMetering(); assertEq(challenger.balance, 99 ether); @@ -220,114 +221,127 @@ contract BoltChallengerTest is Test { assertEq(uint256(challenge.status), 0); assertEq(challenge.challenger, challenger); assertEq(challenge.commitmentSigner, 0x71f7D1B81E297816cf6691B2396060Ede49eFA5e); - assertEq(challenge.commitment.slot, commitment.slot); - assertEq(challenge.commitment.signature, commitment.signature); - assertEq(challenge.commitment.signedTx, commitment.signedTx); + assertEq(challenge.targetSlot, commitments[0].slot); } function testOpenChallengeWithInsufficientBond() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); // Open a challenge with insufficient bond vm.resumeGasMetering(); vm.prank(challenger); vm.expectRevert(IBoltChallenger.InsufficientChallengeBond.selector); - boltChallenger.openChallenge{value: 0.1 ether}(commitment); + boltChallenger.openChallenge{value: 0.1 ether}(commitments); vm.pauseGasMetering(); } function testOpenChallengeWithLargebond() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); // Open a challenge with a large bond, making sure that the rest is refunded vm.resumeGasMetering(); vm.prank(challenger); - boltChallenger.openChallenge{value: 50 ether}(commitment); + boltChallenger.openChallenge{value: 50 ether}(commitments); vm.pauseGasMetering(); assertEq(challenger.balance, 99 ether); } function testOpenAlreadyExistingChallenge() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); // Open a challenge vm.prank(challenger); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); // Try to open the same challenge again vm.resumeGasMetering(); vm.prank(challenger); vm.expectRevert(IBoltChallenger.ChallengeAlreadyExists.selector); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); vm.pauseGasMetering(); } function testOpenChallengeWithSlotInTheFuture() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); - commitment.slot = uint64(BeaconChainUtils._getCurrentSlot()) + 10; + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); + + commitments[0].slot = uint64(BeaconChainUtils._getCurrentSlot()) + 10; // Open a challenge with a slot in the future vm.resumeGasMetering(); vm.prank(challenger); vm.expectRevert(IBoltChallenger.BlockIsNotFinalized.selector); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); vm.pauseGasMetering(); } function testOpenChallengeInvalidSignature() public { - IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _parseTestCommitment(); // Modify the signature to make it invalid - commitment.signature[0] = bytes1(uint8(commitment.signature[0]) + 5); + commitments[0].signature[0] = bytes1(uint8(commitments[0].signature[0]) + 5); // Open a challenge with an invalid signature vm.resumeGasMetering(); vm.prank(challenger); vm.expectRevert(ECDSA.ECDSAInvalidSignature.selector); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); vm.pauseGasMetering(); } // =========== Resolving a challenge =========== - function testResolveChallengeFullDefense() public { + function testResolveChallengeFullDefenseSingleTx() public { // Prove the full defense of a challenge: the block header, account proof, and tx proof // are all valid and the proposer has included the transaction in their slot. - IBoltChallenger.SignedCommitment memory commitment = _createRecentBoltCommitment(); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); + commitments[0] = _createRecentBoltCommitment(); // Open a challenge vm.prank(challenger); - boltChallenger.openChallenge{value: 1 ether}(commitment); + boltChallenger.openChallenge{value: 1 ether}(commitments); // Get the challenge ID IBoltChallenger.Challenge[] memory challenges = boltChallenger.getAllChallenges(); assertEq(challenges.length, 1); bytes32 challengeID = challenges[0].id; - string memory rawHeader = vm.readFile("./test/testdata/raw_header.json"); - string memory ethProof = vm.readFile("./test/testdata/eth_proof.json"); - string memory txProof = vm.readFile("./test/testdata/tx_mpt_proof.json"); + string memory rawPreviousHeader = vm.readFile("./test/testdata/header_20785011.json"); + string memory rawInclusionHeader = vm.readFile("./test/testdata/header_20785012.json"); + string memory ethProof = vm.readFile("./test/testdata/eth_proof_20785011.json"); + string memory txProof = vm.readFile("./test/testdata/tx_mpt_proof_20785012.json"); + + bytes[] memory txProofs = new bytes[](1); + txProofs[0] = _RLPEncodeList(vm.parseJsonBytesArray(txProof, ".proof")); + + uint256[] memory txIndexesInBlock = new uint256[](1); + txIndexesInBlock[0] = vm.parseJsonUint(txProof, ".index"); IBoltChallenger.Proof memory proof = IBoltChallenger.Proof({ - blockHeaderRLP: vm.parseJsonBytes(rawHeader, ".result"), + inclusionBlockNumber: 20_785_012, + previousBlockHeaderRLP: vm.parseJsonBytes(rawPreviousHeader, ".result"), + inclusionBlockHeaderRLP: vm.parseJsonBytes(rawInclusionHeader, ".result"), accountMerkleProof: _RLPEncodeList(vm.parseJsonBytesArray(ethProof, ".result.accountProof")), - txMerkleProof: _RLPEncodeList(vm.parseJsonBytesArray(txProof, ".proof")), - txIndexInBlock: vm.parseJsonUint(txProof, ".index"), - blockNumber: 20_785_012 + txMerkleProofs: txProofs, + txIndexesInBlock: txIndexesInBlock }); - // check that the block header transactions root matches the root in the tx proof data. - bytes32 txRoot = boltChallenger._decodeBlockHeaderRLPExt(proof.blockHeaderRLP).txRoot; - assertEq(txRoot, vm.parseJsonBytes32(txProof, ".root")); + // check that the inclusion block transactions root matches the root in the tx proof data. + bytes32 inclusionTxRoot = boltChallenger._decodeBlockHeaderRLPExt(proof.inclusionBlockHeaderRLP).txRoot; + assertEq(inclusionTxRoot, vm.parseJsonBytes32(txProof, ".root")); - bytes32 trustedBlockHash = 0x0fc7c840f5b4b451e99dc8adb0d475eab2ac7d36278d9601d7f4b2dd05e8022f; + bytes32 trustedPreviousBlockHash = 0x6be050fe1f6c7ffe8f30a350250a9ecc08ff3c031d129f65e1c10e5119d7a28b; // Resolve the challenge vm.resumeGasMetering(); vm.prank(resolver); - boltChallenger._resolveExt(challengeID, trustedBlockHash, proof); + boltChallenger._resolveExt(challengeID, trustedPreviousBlockHash, proof); vm.pauseGasMetering(); // Check the challenge was resolved diff --git a/bolt-contracts/test/testdata/eth_proof_20785011.json b/bolt-contracts/test/testdata/eth_proof_20785011.json new file mode 100644 index 000000000..bb5045553 --- /dev/null +++ b/bolt-contracts/test/testdata/eth_proof_20785011.json @@ -0,0 +1,22 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "address": "0x0d9f5045b604ba0c050b5eb06d0b25d01c525ea5", + "accountProof": [ + "0xf90211a06c5341556789e53964fe2a77e0bf2b540f9c9aef92d508eedbe496183c5fb0d4a088d9fcfc0d596047f887fe0b6e76a22833ed33d90e475a97515c135afc1a358ca0a2d140e4eb080e7154ab4732922dc09ba5e6f7bb66ca085cce4a33a2485cefb3a01550fc55f997ebd8ff3194813c0a83668219d5aab1adea1e4aa8416ed5e086c2a04ce40dbcc710d91c6583912c4bd9e9980998ef0399346a7d488d4807153ffb7fa0a4aed92a1d67dbd794e18c588b0bf690373517a96d1c2028337a17bdb1955522a08a352007fbbbb15880b3ab7f3732a2f0f35513ec3704a97a1c0d6958a855182fa0ab93816f2baef5e33eda09193295d14a76b68ab71b264bbc145f7bbcbab6de44a08c22010d9488cebd377914768e0f105e4468d86843fc8dbe3f0026ea81ea4428a02748a9e18e6560d907ed69f7313239ab489c443e8517a6da153126c1b1f4295ea07e3eac4ab2437783df515ea778b89489991230aa14724147af45cc36027773eda046487f3e599ab8a8d627425936e9490e26c0242526ac83caa777390783efa078a0b9bac1565b8e64fe494b62be9eaf5ba81cfbb477532b5425cb85cc19cdb664f4a024eb0480fbad22741077e53fd0e209b80f721058e000bec1c67904f0a419dc68a09bba4884ab92553333120f0548bc347db14eeb098a903e533b41940a8b3c48d5a0f400d123b1957c29ce7dcab8ffd18a0da9bf7d754c544452391334b05831707280", + "0xf90211a05b4f1080ce2ddb2a07b1093f8248df994bf78d2e0902def84faa02d3e8b4932ba0c75ea41d0704a50f6ef436fede8ff700a2d3c0edbf5c7066f2e7e2346a1b6b15a0a99a258a63cda78ff4fcb4bcdcef99e8f8f125da55ecaba58034408a3c10c03ba08d6ede7b83392f11041c9e9b6403162ddfe0a87de3a26c903b819d81c5d1d68fa09c33fbdc36ea41febe74608a116bd1a6118f9c2a8802d307525e35b31062ed56a0dfd19dec7ac4c6e74eb81bbfe2e9af6dfdb6b8c131f419dcea0bebfba80c4129a06d302c5ad7c218e7772a6fdd426b07ee6a151529f2993e07fa3fe728e03a1016a010184894262fd8c80d8596760e68e39bbe17864fca10d5caf0803c3f9c77ecf1a02c01f8486b37c883b0169b3593a28b70881b03fc4887048a318feb5141140a3da0ecb89cd38ba3499731c19946c2e108cb2977824058bcda585cca702cb231fbada0218c4bba8f633a0c22aa2fe39cae6fc0ddc83c471099709055c4b3953de34685a008948be35bc1607efce7877f13f1cde290b3ec45e026a180d058fe90a314034ca087d839d5da8e87d3fc45f782e128243a4a555b3850ea790f084d2cfc2d775b3da02d09a195dae74c1075bfd63342af74ff5fe4cb6777fc1cc44ea03ab258ba3e43a0219c2f1156d413196b274acb0234c2fcb41ed66aecd1e5011ad83da73b047ce7a090be7be27cf1be666f7835cd80feecb0904d50403adabb86ad1fae00a8cb29da80", + "0xf90211a0a85deaca1adffdb7713b02a33af6ec05b09237d060963af84d7dc1a269178845a0cfd70f233bd4eb4ac42209a0578808f9e24498297af9a3e902233c1839a3a3cda0d6975a90005a1966ddae9b0f978161fb032ea2ca2d8db2453fc6be2710d48388a0d33abb2b6de28462b483a584224b30c079986bed9599023f28c81db7ee324b29a054911488c612caf55eaa494f0cc84ba25485ee49ce40f49b434098951036517da0cc4c54ed2de6d2b8fcc3bcc6d1fc490e8d26e24d4ca1ff3d81c7492fa23e4c3ba06249e67a29f9ed1713043324faf5b666d790977fad710f1a0a9e660ccb48c97da0ed8f59ba4a2766bb4035cc489fbe5f34795c2c36e16bd2d00b7b08a5a6f42507a0d2d4d82c9452c5b78a807c5da31db61357c211c84e90bdb983940f344a9984e3a05c2e1286d8a66758e1c1fcb3c85f28791f95ee85615a39c1d6780f6a54e1b3a6a0ae4bf704ff1b38570d255bb7c2b2917a54b025e6e8185929b4d977f453a8d9e8a0ac9228bf16f7c15155965d5eb5d7aa2e2f31e8f937d52a7203abef348b843632a0a193e1b44b2441cbed3d8cd20862d7f11b0121b957a82d63168b67809dbaf3b2a09aba1e3d0feb612998294bd3645247790107e619bf8728819fbe463fec342f5ca09336f5d7c3d7ddb6e987ac5f7c0e78e3936e05f6da27a7c87813a6e5250e9c38a021c230f247db23bd8d948d68a24891afd58c29864f1d74551060c10067e386a380", + "0xf90211a09d93666285cfbada2d91c81fb96b5e30ff75a51353c69a30c0a1431f51867880a05efd44e1b30767c66f02d79e93b2c45efcd56018050605c08818645ddb7a46c5a05e5bdbdd63d64f58a34f47a42d70b83febf924c08cdfa8d5673db3c6bd71df67a05d67553cf6620c67dead6ee37a48a5dfddddd4adce333c4a7771c25140ecdd92a07d5eb9550d7f807d7e0dd86a52f3d111b4359ac2928ca3e07c34bd6551055537a0fc0e43113716fdd77ec45220d5afcc0953d8357478872de3c6f36fc2d75c9610a03230ce2ec5fa8173fca250ac63df602e3a6d2bff691edc8552ab8bdea458bf07a01e7ad54a56fcd625d6066ec05b6efc30bf90a65b756277f79a65ceb0eb108a32a0089ff36bd32acfeef1b5faf546e54ac7646086d1c2c764239cc9b98da8c460e7a00905f00f493594ce1c13274c2bb5ad018fab388de860f1cae6af64cb3ad3b46fa0ad7d4f9200d8f0ae34bf0cbece04d61b13c6daa0b7ea0e9d732522c5d46feacda075da3fc2991d7d32cb0be70210c7cbe3bd5883cfc52f58e8e04d07b9977f816fa0c7bc6aab1b71efc9ffc459cfda04d930f1652d53179a38b43f8e0980b40ab48ca0ae012ca5c6a3363d5f850e060001c90cd3a022aa44e325d5d832021ebef4fafea06ee58a40e8abba2a3be093fdefd9d9871558f4fa11f6d6721307117a17260b51a07cc44d713e05b70e6749fedd7cb1b4b47d2102638bf93ffd0e8ebf404af6940080", + "0xf90211a0b1689af6f7f511cc59773684cb467f33c3dcc5dc1a08f451c851544dca6b1a3aa039a80eb019a427f8b32dfdd88a0b5d95a21521444c67fca8f1e875f1898e16c4a06652a79c42cf0d23532b84d47d58b6c898076d88147030581ff121d25407e25ba0cd2b62c08885a27becfd0077ccdfdd5d77d4cdc5a0ab1ec2ad1303c93a9aae3ba0333e03e837b7838cc45d8f30b17ec09933b77debdfceea6cc31033c34cb25b88a025c683f0ff00593e3b96caa6a45153e1907e263c2815064492ea29d2ae467d16a0b1f927c724f5f7425c5d47ff064d099a13982ddb528ac5526829ffefb2502c91a086a4177c69624728a862d160c303dda7010e4d317f60b2945a517f2b8cfe71f5a0436081588c2e0ceec3e3b2298c97085410d971d8da9eedcdc48e62fdbd125a2ba02f24c969da88b0cfe6318383edf10cbf2174c883ecd007d6bb79562663723099a0a9cbc30a1d257e3b651b3c304be3c253948c373f6980b22898af778ef9955b0fa0a343404514e213b6e5832f4621e6a6b1f0eb977811d759f06b7ad6da445092fba02eeb6ec27797e59283781b12f56e315f35b16395560a9c34bdd21280e942cfe1a0c874d2114c306baf91f0d43c2c76e30a6ad0da45273fb6d82246565d84c2538ea071a72e0b7d80490d26f95d3714cf8dd0c8b3c7b232980607aee525a9162e6308a04b4d1cf635eab18bbbb9e64109b0d992d759df23a96aab3d4a40e39a7c3becba80", + "0xf90211a023c9514b2fac99cd172f93d2db4d6578ec69fc2e0ef5c79fc76499ded09130eda00c645327db795a8a9c69282d86e3acece2901bdbb8047528ee8c78681b0e14b1a01e76e1109e1936249fdf08a4f583f91c01c6a4672794657bd74cbdd6ab3c2f3aa0a6b7b10c53412201d9ca7461b7210d6457a6b275b95033e2d8ca9778e5071c05a03e6f50fedb2b9f9f963356fc2899800e46a38169a794a1e6906c02ce8e8f0d59a0d4cec6a796cbab7e9cb28063423fcf157bac02de0cf1f0931303a701006019a2a0fb7e5187937b4203cf97bce2f345c9161bfbb9eb431a105356009e6ce18a1fbca0486a40cde9b6a65b5534609ae4f56cc97c00dbc4c4447385f8a42da3b1ac40d3a01c91aad4f4d391df268007fee771caedccc43369e07d6eb22fd45dab6beb0986a0672ca6ccfd8ac83f8974afefc9c9c2b58376991580158b16fd98cb9076cf8ea9a06cb3234503ad7116bb0e66257c646b2bce7603d774474a4f100cf93dde3eb594a085d0406435a6562f3370dd4b5de47a2b7d1084b9f25f0dbf49f54d2f46a2fc32a08bba9249e073397de4c46dd27688e9467b20fb8e61e04fd2594136cae26cd5c7a0a43fd38e14c38b64b84bb579b9a3cfab05cd5cc69c3a812a68e458acf72ea96ca0d848b40a2dbd5d8a4179e89fe8533f21db91982a0b8ecc1542c08ea27aec522aa0e03bc889898881f7ecb7caf0db5fb95125e26ff9e26bb85c33cbe360b50c9f1f80", + "0xf90191a017e9f741d070bfe3d975719f75663dabd5acdeb3678c50bd4872b817d7713d0b80a073811d2e7576e6bdac9804597d153d4e0a4cbf8400b8331f67605f6810ecbd53a0363162c748099d6ed114ead87c481972c1b0c124730b341c4ef1f963e0835a9880a09ef90c2a9ad234a08cbf4ea4ced535f227bc48becd22c1b05f2cbd586f018b73a0ae59576b0c1089b2828db7e54602ad0fabe4eff312ee5e0c21a04ac09b0e04d180a0fdbde0b4dbc02bb2ec472517c6f543de3673d33a6246a602479b43042ae9222da0c8d33ec13b89d8fd0ba487fe9cd61d18deff49bed48dc073384dc086a7361273a021253bb1110de615cede8c708a94cd6b898a61b5b1aaef8760b3341f232d63f0a06e7fbd4a6404e4acae007bb6147c58aebf6f19b3914a8d48b831bc3e50e5c05ea0c72d502ed522a796f823edefb721e64ff9d38904c9913b5be76f1e759c899b9da0fcc3a12a432daa29cb33f4a198624f6391124579ab7cbfae1eb30de076cfdd7980a0ab5019c60fef28581a93f0707739bbb24dcfbc2ffc550fb57aea59d91c79c04780", + "0xf86f9d3a1698215d94a53d304dacf1ae39aec65065a3307b0d96cd2acd29de94b84ff84d81eb88033ee43ef5fd164da056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ], + "balance": "0x33ee43ef5fd164d", + "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "nonce": "0xeb", + "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "storageProof": [] + } +} diff --git a/bolt-contracts/test/testdata/eth_proof.json b/bolt-contracts/test/testdata/eth_proof_20785012.json similarity index 100% rename from bolt-contracts/test/testdata/eth_proof.json rename to bolt-contracts/test/testdata/eth_proof_20785012.json diff --git a/bolt-contracts/test/testdata/header_20785011.json b/bolt-contracts/test/testdata/header_20785011.json new file mode 100644 index 000000000..c4d63aa8e --- /dev/null +++ b/bolt-contracts/test/testdata/header_20785011.json @@ -0,0 +1,5 @@ +{ + "jsonrpc": "2.0", + "result": "0xf9025aa00595a77df480da86f42f0f339fc55fc6a40886cd320de3a3ec3df6286c867503a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941f9090aae28b8a3dceadf281b0f12828e676c326a0d6b6ae210398562833e4ac414eb120db14aed11fb8adf5ff5939a20bdd9fbc6fa0a9c4dc0b0e382f1f800d1581d4001bfc33bd384a6f50978e19ab60e92a283f37a020fcda2771063409aefb2669c4e7579d270234d44055c996de915e95ec9caf01b90100d0f9414739b00184f00e04f08d310c80718d32249441f88082897d3830b2a854950fc41563202a9022080028471a29204227c0a8af36a184dae6a6662def6988186c1e0a609449e97ab949b9a07134a1a6210ac1d5651e4114a8c6389926140162011995324058a1346953f321800cf3202e50a05c066d218641101d171b10860450625e0b0c67488d4c09fe03e272d75c514595113100c8112984e2fd90a26a3385185d1f8020044a2190c0ac199d070479854502981a2522046e10d58f1a7b7b583c82005cb3cb29a1823a811124d0328101175569421f64d64002d9b1e630b871202e43622604014484b4810b82dccb057441009700f809c07d01903816558084013d27738401c9c38083a09ebf8466ec2a63917273796e632d6275696c6465722e78797aa0cfe02ed4afd27cd0a42cb0f802a57672daac70511e3b860d7a7ea6b69d4e0d6d880000000000000000850537c96c32a073cb76da1e69871e3400088fa509333d96c3ac5fa307e6f64892202222885f6280830a0000a0ede6709250535a0c48e856b76e415c907d018c3c5f4d1b34e2ae14aa61a8eba1", + "id": 1 +} diff --git a/bolt-contracts/test/testdata/raw_header.json b/bolt-contracts/test/testdata/header_20785012.json similarity index 100% rename from bolt-contracts/test/testdata/raw_header.json rename to bolt-contracts/test/testdata/header_20785012.json diff --git a/bolt-contracts/test/testdata/tx_mpt_proof.json b/bolt-contracts/test/testdata/tx_mpt_proof_20785012.json similarity index 100% rename from bolt-contracts/test/testdata/tx_mpt_proof.json rename to bolt-contracts/test/testdata/tx_mpt_proof_20785012.json From 7e08beb0e2ef7f07027b750997182444d25cb896 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:43:05 +0200 Subject: [PATCH 2/9] chore: addressed initial reviews --- .../src/contracts/BoltChallenger.sol | 50 +++++++++---------- .../src/interfaces/IBoltChallenger.sol | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol index 7bf1eb90e..8bac9bb4f 100644 --- a/bolt-contracts/src/contracts/BoltChallenger.sol +++ b/bolt-contracts/src/contracts/BoltChallenger.sol @@ -31,7 +31,7 @@ contract BoltChallenger is IBoltChallenger { // ========= CONSTANTS ========= /// @notice The challenge bond required to open a challenge. - uint256 public constant COMMITMENT_BOND = 1 ether; + uint256 public constant CHALLENGE_BOND = 1 ether; /// @notice The maximum duration of a challenge to be considered valid. uint256 public constant MAX_CHALLENGE_DURATION = 7 days; @@ -108,13 +108,9 @@ contract BoltChallenger is IBoltChallenger { revert EmptyCommitments(); } - // Check that the challenge bond is sufficient - uint256 expectedBond = COMMITMENT_BOND * commitments.length; - if (msg.value < expectedBond) { - revert InsufficientChallengeBond(); - } else if (msg.value > expectedBond) { - // Refund the excess value, if any - payable(msg.sender).transfer(msg.value - expectedBond); + // Check that the attached bond amount is correct + if (msg.value != CHALLENGE_BOND) { + revert IncorrectChallengeBond(); } uint256 targetSlot = commitments[0].slot; @@ -184,6 +180,11 @@ contract BoltChallenger is IBoltChallenger { // ========= CHALLENGE RESOLUTION ========= + /// @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. + /// @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 { // Check that the challenge exists if (!challengeIDs.contains(challengeID)) { @@ -197,10 +198,9 @@ contract BoltChallenger is IBoltChallenger { } // Check that the previous block is within the EVM lookback window for block hashes. - if ( - (proof.inclusionBlockNumber - 1) > block.number - || (proof.inclusionBlockNumber - 1) < block.number - BLOCKHASH_EVM_LOOKBACK - ) { + // Clearly, if the previous block is available, the inclusion one will be too. + uint256 previousBlockNumber = proof.inclusionBlockNumber - 1; + if (previousBlockNumber > block.number || previousBlockNumber < block.number - BLOCKHASH_EVM_LOOKBACK) { revert InvalidBlockNumber(); } @@ -265,16 +265,15 @@ contract BoltChallenger is IBoltChallenger { return; } - // Check theintegrity of the proofs - if ( - proof.txMerkleProofs.length != challenge.committedTxs.length - || proof.txIndexesInBlock.length != challenge.committedTxs.length - ) { + // Check the integrity of the proof data + uint256 committedTxsCount = challenge.committedTxs.length; + if (proof.txMerkleProofs.length != committedTxsCount || proof.txIndexesInBlock.length != committedTxsCount) { revert InvalidProofsLength(); } // Check the integrity of the trusted block hash - if (keccak256(proof.previousBlockHeaderRLP) != trustedPreviousBlockHash) { + bytes32 previousBlockHash = keccak256(proof.previousBlockHeaderRLP); + if (previousBlockHash != trustedPreviousBlockHash) { revert InvalidBlockHash(); } @@ -292,7 +291,7 @@ contract BoltChallenger is IBoltChallenger { BlockHeaderData memory inclusionBlockHeader = _decodeBlockHeaderRLP(proof.inclusionBlockHeaderRLP); // Check that the inclusion block is a child of the previous block - if (inclusionBlockHeader.parentHash != keccak256(proof.previousBlockHeaderRLP)) { + if (inclusionBlockHeader.parentHash != previousBlockHash) { revert InvalidParentBlockHash(); } @@ -311,7 +310,7 @@ contract BoltChallenger is IBoltChallenger { // Loop through each committed transaction and verify its inclusion in the block // along with the sender's balance and nonce (starting from the account state at the top of the block). - for (uint256 i = 0; i < challenge.committedTxs.length; i++) { + for (uint256 i = 0; i < committedTxsCount; i++) { TransactionData memory committedTx = challenge.committedTxs[i]; if (account.nonce > committedTx.nonce) { @@ -338,10 +337,11 @@ contract BoltChallenger is IBoltChallenger { } // Over/Underflow is checked in the previous if statements. - // This is the same logic applied by the Bolt Sidecar's off-chain checks - // before deciding to sign a new commitment. - account.nonce++; + // + // Note: This is the same logic applied by the Bolt Sidecar's off-chain checks + // before deciding to sign a new commitment for a particular account. account.balance -= inclusionBlockHeader.baseFee * committedTx.gasLimit; + account.nonce++; // The key in the transaction trie is the RLP-encoded index of the transaction in the block bytes memory txLeaf = RLPWriter.writeUint(proof.txIndexesInBlock[i]); @@ -446,7 +446,7 @@ contract BoltChallenger is IBoltChallenger { function _transferFullBond( address recipient ) internal { - (bool success,) = payable(recipient).call{value: COMMITMENT_BOND}(""); + (bool success,) = payable(recipient).call{value: CHALLENGE_BOND}(""); if (!success) { revert BondTransferFailed(); } @@ -457,7 +457,7 @@ contract BoltChallenger is IBoltChallenger { function _transferHalfBond( address recipient ) internal { - (bool success,) = payable(recipient).call{value: COMMITMENT_BOND / 2}(""); + (bool success,) = payable(recipient).call{value: CHALLENGE_BOND / 2}(""); if (!success) { revert BondTransferFailed(); } diff --git a/bolt-contracts/src/interfaces/IBoltChallenger.sol b/bolt-contracts/src/interfaces/IBoltChallenger.sol index fb603de89..139e65874 100644 --- a/bolt-contracts/src/interfaces/IBoltChallenger.sol +++ b/bolt-contracts/src/interfaces/IBoltChallenger.sol @@ -67,7 +67,7 @@ interface IBoltChallenger { error SlotInTheFuture(); error BlockIsNotFinalized(); - error InsufficientChallengeBond(); + error IncorrectChallengeBond(); error ChallengeAlreadyExists(); error ChallengeAlreadyResolved(); error ChallengeDoesNotExist(); From 400cbe25a6caa1b94b9b8e3dba5aa8e173fe90e3 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:51:52 +0200 Subject: [PATCH 3/9] fix: broken test --- bolt-contracts/test/BoltChallenger.t.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index c3f2f0fae..9c9c6edd7 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -224,14 +224,14 @@ contract BoltChallengerTest is Test { assertEq(challenge.targetSlot, commitments[0].slot); } - function testOpenChallengeWithInsufficientBond() public { + function testOpenChallengeWithIncorrectBond() public { IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); commitments[0] = _parseTestCommitment(); // Open a challenge with insufficient bond vm.resumeGasMetering(); vm.prank(challenger); - vm.expectRevert(IBoltChallenger.InsufficientChallengeBond.selector); + vm.expectRevert(IBoltChallenger.IncorrectChallengeBond.selector); boltChallenger.openChallenge{value: 0.1 ether}(commitments); vm.pauseGasMetering(); } @@ -243,10 +243,11 @@ contract BoltChallengerTest is Test { // Open a challenge with a large bond, making sure that the rest is refunded vm.resumeGasMetering(); vm.prank(challenger); + vm.expectRevert(IBoltChallenger.IncorrectChallengeBond.selector); boltChallenger.openChallenge{value: 50 ether}(commitments); vm.pauseGasMetering(); - assertEq(challenger.balance, 99 ether); + assertEq(challenger.balance, 100 ether); } function testOpenAlreadyExistingChallenge() public { From d689f13cfc20d41c2aa072f31578fe52be2ca8ce Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:40:03 +0200 Subject: [PATCH 4/9] wip: test stacked transaction proofs --- bolt-contracts/.gas-snapshot | 14 +-- bolt-contracts/test/BoltChallenger.t.sol | 89 +++++++++++++++++-- .../test/testdata/eth_proof_20817617.json | 23 +++++ .../test/testdata/header_20817617.json | 5 ++ .../test/testdata/header_20817618.json | 5 ++ ...gned_tx.json => signed_tx_20785012_1.json} | 0 .../test/testdata/signed_tx_20817618_1.json | 3 + .../test/testdata/signed_tx_20817618_2.json | 3 + .../test/testdata/signed_tx_20817618_3.json | 3 + .../test/testdata/signed_tx_20817618_4.json | 3 + .../test/testdata/signed_tx_20817618_5.json | 3 + .../testdata/tx_mpt_proof_20817618_1.json | 10 +++ .../testdata/tx_mpt_proof_20817618_2.json | 10 +++ .../testdata/tx_mpt_proof_20817618_3.json | 10 +++ .../testdata/tx_mpt_proof_20817618_4.json | 10 +++ .../testdata/tx_mpt_proof_20817618_5.json | 10 +++ 16 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 bolt-contracts/test/testdata/eth_proof_20817617.json create mode 100644 bolt-contracts/test/testdata/header_20817617.json create mode 100644 bolt-contracts/test/testdata/header_20817618.json rename bolt-contracts/test/testdata/{signed_tx.json => signed_tx_20785012_1.json} (100%) create mode 100644 bolt-contracts/test/testdata/signed_tx_20817618_1.json create mode 100644 bolt-contracts/test/testdata/signed_tx_20817618_2.json create mode 100644 bolt-contracts/test/testdata/signed_tx_20817618_3.json create mode 100644 bolt-contracts/test/testdata/signed_tx_20817618_4.json create mode 100644 bolt-contracts/test/testdata/signed_tx_20817618_5.json create mode 100644 bolt-contracts/test/testdata/tx_mpt_proof_20817618_1.json create mode 100644 bolt-contracts/test/testdata/tx_mpt_proof_20817618_2.json create mode 100644 bolt-contracts/test/testdata/tx_mpt_proof_20817618_3.json create mode 100644 bolt-contracts/test/testdata/tx_mpt_proof_20817618_4.json create mode 100644 bolt-contracts/test/testdata/tx_mpt_proof_20817618_5.json diff --git a/bolt-contracts/.gas-snapshot b/bolt-contracts/.gas-snapshot index 5dffb08e7..547c4e8f2 100644 --- a/bolt-contracts/.gas-snapshot +++ b/bolt-contracts/.gas-snapshot @@ -1,15 +1,15 @@ BoltChallengerTest:testCommitmentDigestAndSignature() (gas: 4626) BoltChallengerTest:testCommitmentSignature() (gas: 6754) -BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 111137) -BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 23952) -BoltChallengerTest:testOpenChallengeSingleTx() (gas: 406261) -BoltChallengerTest:testOpenChallengeWithInsufficientBond() (gas: 17220) -BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 415209) -BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 17622) +BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 111032) +BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 23847) +BoltChallengerTest:testOpenChallengeSingleTx() (gas: 406156) +BoltChallengerTest:testOpenChallengeWithIncorrectBond() (gas: 17130) +BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 17141) +BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 17517) BoltChallengerTest:testProveAccountData() (gas: 355542) BoltChallengerTest:testProveHeaderData() (gas: 46228) BoltChallengerTest:testProveTransactionInclusion() (gas: 176543) -BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 698603) +BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 697302) BoltManagerEigenLayerTest:testGetNonExistentProposerStatus() (gas: 921620) BoltManagerEigenLayerTest:testGetWhitelistedCollaterals() (gas: 99988) BoltManagerEigenLayerTest:testNonWhitelistedCollateral() (gas: 103013) diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index 9c9c6edd7..9ea074e39 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -179,7 +179,7 @@ contract BoltChallengerTest is Test { } function testCommitmentSignature() public { - bytes memory signedTx = vm.parseJsonBytes(vm.readFile("./test/testdata/signed_tx.json"), ".raw"); + bytes memory signedTx = vm.parseJsonBytes(vm.readFile("./test/testdata/signed_tx_20785012_1.json"), ".raw"); uint64 slot = 20_728_344; // Reconstruct the commitment digest @@ -298,11 +298,12 @@ contract BoltChallengerTest is Test { // =========== Resolving a challenge =========== function testResolveChallengeFullDefenseSingleTx() public { - // Prove the full defense of a challenge: the block header, account proof, and tx proof + // Prove the full defense of a challenge: the block headers, account proof, and tx proofs // are all valid and the proposer has included the transaction in their slot. + uint256 inclusionBlockNumber = 20_785_012; IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](1); - commitments[0] = _createRecentBoltCommitment(); + commitments[0] = _createRecentBoltCommitment(inclusionBlockNumber, 1); // Open a challenge vm.prank(challenger); @@ -325,7 +326,7 @@ contract BoltChallengerTest is Test { txIndexesInBlock[0] = vm.parseJsonUint(txProof, ".index"); IBoltChallenger.Proof memory proof = IBoltChallenger.Proof({ - inclusionBlockNumber: 20_785_012, + inclusionBlockNumber: inclusionBlockNumber, previousBlockHeaderRLP: vm.parseJsonBytes(rawPreviousHeader, ".result"), inclusionBlockHeaderRLP: vm.parseJsonBytes(rawInclusionHeader, ".result"), accountMerkleProof: _RLPEncodeList(vm.parseJsonBytesArray(ethProof, ".result.accountProof")), @@ -350,14 +351,84 @@ contract BoltChallengerTest is Test { assertEq(uint256(challenge.status), uint256(IBoltChallenger.ChallengeStatus.Defended)); } + function testResolveChallengeFullDefenseStackedTxs() public { + // Prove the full defense of a challenge: the block headers, account proof, and tx proofs + // are all valid and the proposer has included the transaction in their slot. + // This time, the proposer has committed to multiple transactions in their slot. + + uint256 inclusionBlockNumber = 20_817_618; + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](3); + commitments[0] = _createRecentBoltCommitment(inclusionBlockNumber, 1); + commitments[1] = _createRecentBoltCommitment(inclusionBlockNumber, 2); + commitments[2] = _createRecentBoltCommitment(inclusionBlockNumber, 3); + + // Open a challenge + vm.prank(challenger); + boltChallenger.openChallenge{value: 1 ether}(commitments); + + // Get the challenge ID + IBoltChallenger.Challenge[] memory challenges = boltChallenger.getAllChallenges(); + assertEq(challenges.length, 1); + bytes32 challengeID = challenges[0].id; + + string memory rawPreviousHeader = vm.readFile("./test/testdata/header_20817617.json"); + string memory rawInclusionHeader = vm.readFile("./test/testdata/header_20817618.json"); + string memory ethProof = vm.readFile("./test/testdata/eth_proof_20817617.json"); + string memory txProof1 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_1.json"); + string memory txProof2 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_2.json"); + string memory txProof3 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_3.json"); + + bytes[] memory txProofs = new bytes[](3); + txProofs[0] = _RLPEncodeList(vm.parseJsonBytesArray(txProof1, ".proof")); + txProofs[1] = _RLPEncodeList(vm.parseJsonBytesArray(txProof2, ".proof")); + txProofs[2] = _RLPEncodeList(vm.parseJsonBytesArray(txProof3, ".proof")); + + uint256[] memory txIndexesInBlock = new uint256[](3); + txIndexesInBlock[0] = vm.parseJsonUint(txProof1, ".index"); + txIndexesInBlock[1] = vm.parseJsonUint(txProof2, ".index"); + txIndexesInBlock[2] = vm.parseJsonUint(txProof3, ".index"); + + IBoltChallenger.Proof memory proof = IBoltChallenger.Proof({ + inclusionBlockNumber: inclusionBlockNumber, + previousBlockHeaderRLP: vm.parseJsonBytes(rawPreviousHeader, ".result"), + inclusionBlockHeaderRLP: vm.parseJsonBytes(rawInclusionHeader, ".result"), + accountMerkleProof: _RLPEncodeList(vm.parseJsonBytesArray(ethProof, ".result.accountProof")), + txMerkleProofs: txProofs, + txIndexesInBlock: txIndexesInBlock + }); + + // check that the inclusion block transactions root matches the root in the tx proof data. + bytes32 inclusionTxRoot = boltChallenger._decodeBlockHeaderRLPExt(proof.inclusionBlockHeaderRLP).txRoot; + assertEq(inclusionTxRoot, vm.parseJsonBytes32(txProof1, ".root")); + + // block hash of https://etherscan.io/block/20817617 + bytes32 trustedPreviousBlockHash = 0xb410d12f92ed268b184c1e6523b7d3fea5fcd0ba3f9bc6c6cb9a7e5b1523d225; + + // Resolve the challenge + vm.resumeGasMetering(); + vm.prank(resolver); + boltChallenger._resolveExt(challengeID, trustedPreviousBlockHash, proof); + vm.pauseGasMetering(); + + // Check the challenge was resolved + IBoltChallenger.Challenge memory challenge = boltChallenger.getAllChallenges()[0]; + assertEq(uint256(challenge.status), uint256(IBoltChallenger.ChallengeStatus.Defended)); + } + // =========== Helper functions =========== // Helper to create a test commitment with a recent slot, valid for a recent challenge - function _createRecentBoltCommitment() internal view returns (IBoltChallenger.SignedCommitment memory commitment) { - commitment.signedTx = vm.parseJsonBytes(vm.readFile("./test/testdata/signed_tx.json"), ".raw"); - assertEq(commitment.signedTx.decodeEnveloped().recoverSender(), 0x0D9f5045B604bA0c050b5eb06D0b25d01c525Ea5); - - // pick a recent slot + function _createRecentBoltCommitment( + uint256 blockNumber, + uint256 id + ) internal view returns (IBoltChallenger.SignedCommitment memory commitment) { + // pattern: ./test/testdata/signed_tx_{blockNumber}_{id}.json + string memory base = "./test/testdata/signed_tx_"; + string memory extension = string.concat(vm.toString(blockNumber), "_", vm.toString(id), ".json"); + string memory path = string.concat(base, extension); + commitment.signedTx = vm.parseJsonBytes(vm.readFile(path), ".raw"); + + // pick a recent slot, 100 slots behind the current slot commitment.slot = uint64(BeaconChainUtils._getCurrentSlot() - 100); // sign the new commitment with the target's private key diff --git a/bolt-contracts/test/testdata/eth_proof_20817617.json b/bolt-contracts/test/testdata/eth_proof_20817617.json new file mode 100644 index 000000000..e3447e81c --- /dev/null +++ b/bolt-contracts/test/testdata/eth_proof_20817617.json @@ -0,0 +1,23 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "address": "0xc21fb45eeb45d883b838e30abbd2896ae5ac888c", + "accountProof": [ + "0xf90211a09d63f98f913479f3896c84699548d56e7664f7abd187f56544e9b5d8af62d06da09fb4628824ac085d2bd2b5b87ac98aac9fdfe06b8f539f4a811f9127546238fba0908dae3356ca023e940a75e59f9a43ce84aa64144017ec4bbf0c9ce32f8e211da0dead54c42fc1852cf7676bb06c3bf24e7e36a362892eedbb55a2099f46184840a053d3e971dc35dc1d221d50deaf816c55c363748d40b2371762a249eed7177aeba086c7b11320b032936528b27716c71491b08ca99d6020c4969ba41d23229ce0bda0b8318ab3806049d7290b0473916dc890d42cc788c237256e43cf7916eab2d8a9a01d48463ff710835bf05f70632f333a8897fbb98bd94fa11ad0c55e016fcc8e55a02bb21d34e1c563361fac6b818fa815180d93b943284a0094bba706504b09b1e3a0d572335532de9097855e8101578f5e7d784df383c0eccaf1a8e03e8b2531fc0aa01bef35cd30119d0cd52dd30943d60f672ccec38d9e78a526f7147a200a8c920ea0bb7fbbeee717776d53b0605a975f7be626869e3639626edc5d1ea82d7256ceb3a078330e172b00a00b2be82fb1c239a855d3b115c2b05732a0da136b640a4bc991a0034f3050dbf66de7fbc5e7fdb705db7f266136f09d5af946f2ec2d13552e1f60a082a08128a5cd66737106304cad57c04edc61a7cd01e8d8bb34b1aaf2e7d5f384a06e0ee61de15716b08d9bb6b382c7a2935df7aaf3604663c8b141ebe4765f559e80", + "0xf90211a0ca41f42416743bddd631b8d111c47fb6a399adff1dce745e6102c198f42e3934a09b0ed7c6bebeaf23aeb5be74431ff7f654549969cf2657f9be99db7046309e05a0a9432bb9292db7ef90ec70f49fb01604baac0c47a121c8474f6a7b4ac3fee143a0772fca178a87cffb9699d83c4011ea9171fd97f02100dfcfb8104d36419f8ec9a022f78994b65aa6f4c6812c0cbd3dcded7343c5b8e97ce9e31d9595a1809cef52a096b54ba6f10fde5e840add6e9fa04e6243443dee45520f46427d527db7111779a02601c850c714949f180389e6271b74f48db4d1819a97e071d0b930a901e56ebea024d802aea5c6a59daaea1f7633b430a97c5b234e18062eee828e1e80d8f47105a085d478a1022a1e77290b0e7c3603d4af848af379ac734ae98fc01995676d441fa09d016f78675c86c68fe3e8a9c6024d34a753863612c75e1aefd1e83bf1fb7996a0ec7a6ad23010d2524751f8d34e4e14317ebea692f2b3c190989b3c0edfd72a43a00fe8c19621357b2a3cc3aded49523f7e438587b43729e679c5a751382f10a6a7a011e2b91321938e3762866e534fdf8ee6f38addf632f6ab9852f3011f0760da53a0b4142fc246d402037ff282a9ded94b26ca60ed32aee664b9bbcb62f0451bb9b3a0b0a71fbb993cfd1f7d101c0fea2a3221d64e0df142c2a25de846ca70fa04e91da03b3256cc9177a07d8237cc3429fbc0d334a9a723ab6d4e6739aa6ec317fd5b1c80", + "0xf90211a09563f3ad7021bb4ba964dbda89142678f69c9220ddc595a7176fee6a77e6dde2a0c03d7e898d9f019dc509c34a0507beb42e5d81f48d9f89ccfbf888d5622a119fa06c795fb9599852d8a27902d72c94de183fadfa304d8dca88881e5486650e047aa0a1095a0a6206c956ef87213a33863dda73e503eebc6d4d0b0303d304ce380bf5a00c0f870fa662cbabbe0efeb0d7092d794a44ffbc9b9f3dc78a0f5820bdb9ef08a0e3b486d3c7ab4ee5bc82aa53062cc0ac7518e50a28f023a3bec79e098d50a1e1a08fa0db4c80f774c25b45aa693c36ba543a0da17859a5ed298947ff0625d3240ea0719bbb5fa0cbbabf2c58d9de555a4c7fda5a3db3bd8f8dcb52f0331ac50da39fa0aa7b34db1dd20cc8082fa64b4571edf4a425fae384632eb1da1cafe31bf1aa7ea017466a7e9a8b545e8c087f4952f16c07d93ecbacbcd80aa4e9964ee4999da0eca0bfbcee522d04a97e863cb4a143161e6935bf2d9f213caef397f19eb0d444955aa0c603f4da9f3b8ce90e3c1e0692b4203e92d0da7aeb7c199c310573b47353a072a0c69a3e3392e217907b7b2fa341e2d1d753319cc0c6f2f098c6bcda7d07045fcba0a3ffebaca1076434c36a77df31d5149ec533f109e458ec1faf436a8e9cbeb063a0875415f1688fc04aa42448b584d522e647a58a56d8a10754717c91b7792d2079a09be0e333cbdded096276ebacf84a0bec134db2a76c44c909969155dae645bda080", + "0xf90211a0fe04c098ac8b48787e56020b1ecc3b5c5c3c6e5171d10a01cdb0c335c2d51a0ea0fc79b69308a70197e2f0dac13429e9ac40fde63afd94055f99cc4083d7e47a9da07ea9fda13a8b6e625cc3103397841e950c401db0590bffd19c88784d66ae8ac2a0eb80e847a0bb1d9ea51785e9df87b8a3cc2c7952e6b1c8b8936d3dbc711f8903a050c6a7b82b0b885b0e7eba783dcc5b8ae987309cd7dfbca12468b03dd0bac239a0a0ee1c4e530bce49c428d03595a5cbcd1d3a305bf574dc3c31cf7fb5a3c142aba0babb7e1b216a15528e50a34fc0f0df5d7a3f772e16769bfe5cde5dca3676d653a0339248dbbdf1b8ffad0c8a9aa7570d84f9c98f628781a38f91283b0ca3aeb44da04bca75e34b823c7cd1b115f6a9062c86d5cf1adc4eee6c07e1f8d14702f9eee2a0c56ff0772873872ddf79cfc6b5ab00928b5a7fe729007cf862fb5db730479098a0ef63d3c6b20257e68fa2985b5df37f283f264d3cfced52e0616026d67aae5174a05235bf4b2037762e034ae4de3f9ec91a7430c033985b296dd7e4511c38852ed9a0f094fb6c0f946cc1daa5c461b1780f58fc2f7cf8c0267b6ce7dcbc72f706335ba086854e8d0a94808be2decde59cf160dc0d3076f03aa0317572bde03f2b320873a04c4c3c0736b931b89b7852c58058edfc5bbe3d5fa8ecb1b6adf260b5a55169b5a03eb45261c45524b9eb254d1e9be2c454c15dca3e9830c233e65098874962c33680", + "0xf90211a092f305a8404f17b6e931179883c08395739bd9cded1d6d0383c5ea78eba0e9eea01b435be6b1b8953faa119ff904d9dc631a0ce9caefbbcf31238f2f1f85dd3101a08cc07a9faf111aa08259fb765d362816eaf830be9c53f5d0a0f80675ab93b815a0e4d58240ddd3d78e1ce5a2d12e1c45e82dcd2185b13ffc177968099ea757cdf5a0f85d2519788a25516c7686f33b03d76de697ec327cbe5c081f069ad85dd02963a001a4641ea4b49fd406ec717d9cf5bda08da097ff8e8f31f47ed9f81fdb1d277fa015ea349963312a679f9e8a95bd835ab8d10323b8df0e33a0f5649c0c43370646a00eaca4b6b9b0ea916d29627a4fbc27922f319cc1d54e1b79a79e5a577aae02e3a00a036924e0cb949a6f2f9c6bd832875db5c46cf43e1be6349b4b6b6531004928a09ef5c65a8581263ac6664606c7f1a151fa72513a1c42a5c980d34fc7cb52d942a0eb8096a937ff16967616dc01e382a32921301b14f4e342f2db3e0979958fdb93a04432cb0b5a0a6e88c885795324164a59870d38c8784c1c51089a3c667f93dc66a0557b8b232b8845e9b522068e259d5292149773319185be73deb68b2acc00869ba097de648ff70c532cb137c8d451ab7d6f219f4198cea0f7e004a44483290ba517a03b7502565bd4afbbdf3e0e5bf8cdab3bc2d311baf540aedbeb989ea98371e4eca02806b9535ce6e675559225b8d2109260017428129690f6361b6489d87c49fae180", + "0xf90211a0503b618293d23441985112f140f5d7afa5ef9d31d19fa62fb99017d4514902c8a0311ed3758bc90dda00ff970a1660bf6cc4adf0ed5b655077e8eef10e02a20ddda0c97eb281c77a1a0be95849a7e7ef57f1463892c5f1e81c28988e2de89f1003c3a00db3fee85f1671f42cb022c701753a8bd0a327a335706fd98c34fdb191187acda0be40e8d8fbc8e2e2cfe867916e6e29b3b5051b0f9f15a9255dffafc956501d37a049486782a5f47a956d3c393f7373ef8bf89c6dcb605abb3d0554f71ba3f71b47a0ba560b379073ffaac72ede1c7cdae34269b9cf17d21f00587a01d4a1ce05714ca0d77cfbdeeb50804494e76228d8961ddbe6c3d8ea554f2a4aae2a711bddd436a2a0e0d0b4ccc7d6b05e5c32e622a9d55297cf382971b0efd188b717437a677d6e60a039d60e2d14f5bed0190930c46386751cc9469a7016d95273ab09dc0037942cffa02474efc4317192c3188209b5e3a4f0bfe624c586300d65362105aaf02abf3fd4a0990cefa23e70fdd28aaccfb96d9ac861347623dba7ec59d5753bae8e4d0421b8a08207ae46bdb4dc6edf83ca4a30fb9260d9d203e69bb694c11c205c4c924de371a039bb8f12a2ca0e97d5220a439381493a3802117bb73406a85f76e325bcf2ffe9a07e9cf71d04326d2c7344c2cf71a552a75ec095d13af30646e903721baf6f968ea032996daa77895aa8e0383c211146b54270a10aa9b4c58e23880d2a243301eca680", + "0xf90151a0b3ad1158c0656d2fa69da4aa9a3060a85073d90819b84522f0c3377047247540a0a1f7c4b2a015e11c26d62fc727eef67a0faa780040ae0adebe93a1e341a6f4d980a031c20c39dfbdd0c7142ef4e8a9128dec5ac324319ec8475d5a4bd73cecfbe9e780a05ac2833f0e6de75df686dad2955041acdfaa9cbe57b8eb90ee18e0e616e7561f80a0dd57fe766ac44d32e7784778660b650ef59bf3855c6f8ceeb57bac3a14cb4247a0b558e61b73ce931037cb132603bbcd0633d1803aa3df1e6cc4fc6adeb78f117da0fd56b8e86e95cd1b8f123394a96685d273e917f924024f114c1c279599250e9b80a08c1d302bcf60d6253bd7cb2567c5f45aae41ef30cdd0acd16ddac78591c6957680a069f601199a401cb6eb42de37258d32c77956ea7c010c34ea5785b94a264f34b980a0da64c248f908ec75a352f1ddd1992df661cbe60623af5cc90707feda609b111a80", + "0xf8518080808080a031d211162636a06229e43cee3301dc559d43c39dc70a35c1f9db384c8ea8f3b580a052ae106948915f7db6155b87fdc9f0bb2c8cd12e475aa3cccad0b60d18a80794808080808080808080", + "0xf8719d20decd5f57411491cdc23220f75184426f294b3c88918fe2e3b802964fb851f84f823385890936f1a77faadf9ca7a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ], + "balance": "0x936f1a77faadf9ca7", + "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "nonce": "0x3385", + "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "storageProof": [] + } +} diff --git a/bolt-contracts/test/testdata/header_20817617.json b/bolt-contracts/test/testdata/header_20817617.json new file mode 100644 index 000000000..96c10c96f --- /dev/null +++ b/bolt-contracts/test/testdata/header_20817617.json @@ -0,0 +1,5 @@ +{ + "jsonrpc": "2.0", + "result": "0xf90261a0f0c271606d68097149e0d89ff622d2690323d65deded9ac9c435c2ea73ed68e6a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944838b106fce9647bdf1e7877bf73ce8b0bad5f97a07bf2e876a6609017c05940b6020e206c0e80006c0b902e5d073c80efd57ee12aa0f866c5389896ec1b8adb85b5b786841a7f8162d820eb35adc626f932fcd9bbcea06e1b5cb518185b799d3726b482abd5333ad733e5d88019686dd2c2457df5de2ab901001ba5662209ca280836180055961846269b09e1adcc814ca5065145909c6f18f0854367868469022d23305ad00c2439700ef190808bb3282520c92c6785780110e09286187180893f4e0a63aad177d0f48e262201c1d40820aca6b780a5a56604acd13160424ac534093df03402841ff2440f126210f8a404c042a852087b08d1800310d98380c058005c3322f0b9024bf4961ca5b320059c853b44d760932ad59ac48560791aa8520a035ce4ce837d1ccfa34e4642e4a207846e8492421a44414341310ea85090e201028e6c180382d34272082f06086030002bf30a50a8e03068fc2a05ba007640d4dc53604d0f138407ec9100815e40c4903b304050c155568084013da6d18401c9c38083b5043a8466f22aff98546974616e2028746974616e6275696c6465722e78797a29a013ea190d046dbfc659b21f36a6ab93951e04cc4430f979ea1970e828bb02f2878800000000000000008502ebb83b87a0089ec82e885d150f3de310102fb40d5a634dafae12a0dedeaec791ad804c686b80830c0000a08ed15ad048af706cacebcc6acfc3a2a32623f4823fd82a78b9ff5678fdc98b95", + "id": 1 +} diff --git a/bolt-contracts/test/testdata/header_20817618.json b/bolt-contracts/test/testdata/header_20817618.json new file mode 100644 index 000000000..d1a2918d5 --- /dev/null +++ b/bolt-contracts/test/testdata/header_20817618.json @@ -0,0 +1,5 @@ +{ + "jsonrpc": "2.0", + "result": "0xf90262a0b410d12f92ed268b184c1e6523b7d3fea5fcd0ba3f9bc6c6cb9a7e5b1523d225a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944838b106fce9647bdf1e7877bf73ce8b0bad5f97a0ee16bd5a83967bd2073fae8ddffb5f19aa990fb58e1bc912e5d6e53d3279eb4ba0b152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13a0b5f80761011b50c50f0a85ea83895cef3454c84b3c982225ea1754c2c921a7aeb9010013e55e8278cd3cb8f26a28f0c437340411c98280c5e10c8fc60b693d4677c7ab61dfaf029410826e020438d82a1b8155cfe98038c9136d31b12aede0853e6510e090e519149281abffa6c80a902fbcf6a93ae62994450e309c340766ebb44ba09cd2f4221accc4372529cc0e49302d3d5228d8601a260cecd4454cfc11591ae2341932dc2084755459cf7629a2b5c8605dba3805f325a13ce42e71d871b21b659b5502f01613a81082cad8e52e25bdce0553a8544c30ce3a0d578d06a09da3248888ec021d6df371118426a783b1b05c52faba46b7d75a3522139a3af0aa702c9a7ae87799101e4da4d547a80053dec2cf8c581152dc56c00810e2116105174f8084013da6d28401c9c3808401c98c1a8466f22b0b98546974616e2028746974616e6275696c6465722e78797a29a05856c04e6738317c95ef58b272f7e8611bc2e31bda48d54d86a8d169d842d7468800000000000000008502d82c7313a07b49c83936b895f1331b9e71b5314c53e8cce3952ec29d015301c72cf87499688083060000a049ccb3d7e4a517a48fe3aaf8d81c73286b0694598d40b5bf13e7428cea4f0f77", + "id": 1 +} diff --git a/bolt-contracts/test/testdata/signed_tx.json b/bolt-contracts/test/testdata/signed_tx_20785012_1.json similarity index 100% rename from bolt-contracts/test/testdata/signed_tx.json rename to bolt-contracts/test/testdata/signed_tx_20785012_1.json diff --git a/bolt-contracts/test/testdata/signed_tx_20817618_1.json b/bolt-contracts/test/testdata/signed_tx_20817618_1.json new file mode 100644 index 000000000..eb48e2cca --- /dev/null +++ b/bolt-contracts/test/testdata/signed_tx_20817618_1.json @@ -0,0 +1,3 @@ +{ + "raw": "0xf8ab8233858502de59ddef82b59b94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000331b7670016c6be29d0ac211f6f8aceeb38f0d42000000000000000000000000000000000000000000000000000000000726bbf826a0abf062f0d6ebbb4910392d3bdac921b384f12aea2aa43169a2e866674ae90382a02849acdddc6d86036169e250dea502db87e9a91d6e1ffe8a6a30e54fbb327c5e" +} diff --git a/bolt-contracts/test/testdata/signed_tx_20817618_2.json b/bolt-contracts/test/testdata/signed_tx_20817618_2.json new file mode 100644 index 000000000..1dee3381b --- /dev/null +++ b/bolt-contracts/test/testdata/signed_tx_20817618_2.json @@ -0,0 +1,3 @@ +{ + "raw": "0xf86d8233868504216ca9e78252089415086dc2a2ea37c844e554b65d17c5183f15ab74870326bfc40e1c008025a06db3917cf09951d0107da89bc605ae88623a7690647ac8a5bc3c1638c8c9de59a032a2121306e6a5e1713d99595a65c14cfd1b1978922173cf69bf5f2d4a0b1a60" +} diff --git a/bolt-contracts/test/testdata/signed_tx_20817618_3.json b/bolt-contracts/test/testdata/signed_tx_20817618_3.json new file mode 100644 index 000000000..68339c625 --- /dev/null +++ b/bolt-contracts/test/testdata/signed_tx_20817618_3.json @@ -0,0 +1,3 @@ +{ + "raw": "0xf8ab823387850340375aa882fa2b94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000009b26d899b1ac6ec07488929c0fdc5b40455275480000000000000000000000000000000000000000000000000000000006e438a025a06194e5ecbf6742f5f7919b39950f452eb0024344977576bb8ceafeff4410e199a007569f10b4d88cd2e185fb30c10f7a507ee6badebfd65b7e4cb884ca8acc5e11" +} diff --git a/bolt-contracts/test/testdata/signed_tx_20817618_4.json b/bolt-contracts/test/testdata/signed_tx_20817618_4.json new file mode 100644 index 000000000..8b2fc67e9 --- /dev/null +++ b/bolt-contracts/test/testdata/signed_tx_20817618_4.json @@ -0,0 +1,3 @@ +{ + "raw": "0xf86d8233888503439d541b8252089415086dc2a2ea37c844e554b65d17c5183f15ab7487027d84f7cd40008026a0d69512e3b47f74fc7d8a2c02f0497580299494544c53860198510433385d6658a043aed084197ff3a3b05e0a0020e4802da7f50c0078dff2e197ad4b0affb36929" +} diff --git a/bolt-contracts/test/testdata/signed_tx_20817618_5.json b/bolt-contracts/test/testdata/signed_tx_20817618_5.json new file mode 100644 index 000000000..eb01965c4 --- /dev/null +++ b/bolt-contracts/test/testdata/signed_tx_20817618_5.json @@ -0,0 +1,3 @@ +{ + "raw": "0xf86d82338985058af3f6c28252089415086dc2a2ea37c844e554b65d17c5183f15ab7487040a50523970008026a05bee04d6213c636154c3c2e87038ffdb3d384c23fb9af65fcc85a890c9cabfe6a02231204d5fe346497f035994acec2dece4efa5109d466e7ce834b7576c237f9e" +} diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20817618_1.json b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_1.json new file mode 100644 index 000000000..168a80b8f --- /dev/null +++ b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_1.json @@ -0,0 +1,10 @@ +{ + "txHash": "0x7387d381729e956b176a46c892ade005b674f3c1c700eeae7fd5212fd87cf85d", + "root": "0xb152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13", + "proof": [ + "f90131a08054ac06deeff9e3269b58b3cf8cfead2012586326e2d51edfdc64603ea92fcfa0c6974714547104b94fac6f0da4e95b50534d960d1ee5e1f21932a212d9a4f42ca052dae1479e618740ffff9885fe9b55a17026903922d16cfd4b1c0733b18f2f24a0acbe9dc865421d745cb159af2cfcd8be5deaf64683d19c77857e5fc25744ac2ca00a6a250aff5ab4711dc9cea5b812f8080caceafa95d5ec578d43285ab27fd562a09a719127a63e2242a33fc5b9232c0d87892c8314c3ed5933527cb6fb95f50278a018cfbac9d7ed346d221124c0e28dfd4e92be373f3170cc62421327084e2bc221a0b74eae762f631f76b434632d03de4862d45e2d5c6a23f5d8b6b4fb806f0070d7a00cc848fcc361f3e01dd562909867bc8a7ab1d329aab2410a500a888f1b6f968a8080808080808080", + "f90211a089437c8fb79d4e0278f780cd90e1d47d2c25096271a1a680e762077778f498e5a065f5057f982b971e6624cfbb2d2acab8ae4a10425bd81b4401b69bc1a2ab31d6a0418f8fc6dceed29c186fa782f71f88f91b133c9070ec598eff02cbcf93d7d634a0d26549a8dd9285213e60286a8e2f8714c86a02bd13939c33fedde54c505a5573a02755be69f4b8e2560a1e4c8476dda8cb80334118bcbeb1f4621ee96aa7f8a418a0ef2ceefc457b6af22d2da599e6017e3ffe7c64dd6a229cad956189ddfeba521ba0df34b08748c2fad457380901f01d06550edab0fb2f176b85fe1b8e8767b58b08a07d30d5330a32f2831e471ecd3249d9561a6ebb91c17627b3cf836d77c9e2f8b3a087e09e649a43bc9c669e21844332f14d207c13cc4455ace13580bf6ef483a0eea005a73cbf870ca0c564dfa07b9f90cdc7d3e47f8545f6f06b9556e5a6fcf30f22a0eebdad0cdb60e30a3b3626d77ece2219a9f25eea0d8c2bb8de751fd727a371f1a0adc316aafc459d35adea06c3b4df1929fe9e48e043b2adc80154666634038352a04cc9eef47849e1aff9e18af286a80a0a72bfdb67c4d9ccad3866806993caa3b0a0fa37400267248ba993971f439e7cd650c0b5a65477e08b5df55c1b4159d23337a066314a563a10591582bdb2f69e0cb6160a2faa91d73a00aad0b54d7e24c6f3cda0376f6e26446a1600cbaa22f7f611482c109f149cf0d27f9f1bc48664b4d21c2d80", + "f8b020b8adf8ab8233858502de59ddef82b59b94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000331b7670016c6be29d0ac211f6f8aceeb38f0d42000000000000000000000000000000000000000000000000000000000726bbf826a0abf062f0d6ebbb4910392d3bdac921b384f12aea2aa43169a2e866674ae90382a02849acdddc6d86036169e250dea502db87e9a91d6e1ffe8a6a30e54fbb327c5e" + ], + "index": 77 +} diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20817618_2.json b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_2.json new file mode 100644 index 000000000..3b4f33f65 --- /dev/null +++ b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_2.json @@ -0,0 +1,10 @@ +{ + "txHash": "0x9646df26d6c781600205f63717ffc428c78f4e17cf65511c9ce1893bfcc8939e", + "root": "0xb152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13", + "proof": [ + "f90131a08054ac06deeff9e3269b58b3cf8cfead2012586326e2d51edfdc64603ea92fcfa0c6974714547104b94fac6f0da4e95b50534d960d1ee5e1f21932a212d9a4f42ca052dae1479e618740ffff9885fe9b55a17026903922d16cfd4b1c0733b18f2f24a0acbe9dc865421d745cb159af2cfcd8be5deaf64683d19c77857e5fc25744ac2ca00a6a250aff5ab4711dc9cea5b812f8080caceafa95d5ec578d43285ab27fd562a09a719127a63e2242a33fc5b9232c0d87892c8314c3ed5933527cb6fb95f50278a018cfbac9d7ed346d221124c0e28dfd4e92be373f3170cc62421327084e2bc221a0b74eae762f631f76b434632d03de4862d45e2d5c6a23f5d8b6b4fb806f0070d7a00cc848fcc361f3e01dd562909867bc8a7ab1d329aab2410a500a888f1b6f968a8080808080808080", + "f90211a089437c8fb79d4e0278f780cd90e1d47d2c25096271a1a680e762077778f498e5a065f5057f982b971e6624cfbb2d2acab8ae4a10425bd81b4401b69bc1a2ab31d6a0418f8fc6dceed29c186fa782f71f88f91b133c9070ec598eff02cbcf93d7d634a0d26549a8dd9285213e60286a8e2f8714c86a02bd13939c33fedde54c505a5573a02755be69f4b8e2560a1e4c8476dda8cb80334118bcbeb1f4621ee96aa7f8a418a0ef2ceefc457b6af22d2da599e6017e3ffe7c64dd6a229cad956189ddfeba521ba0df34b08748c2fad457380901f01d06550edab0fb2f176b85fe1b8e8767b58b08a07d30d5330a32f2831e471ecd3249d9561a6ebb91c17627b3cf836d77c9e2f8b3a087e09e649a43bc9c669e21844332f14d207c13cc4455ace13580bf6ef483a0eea005a73cbf870ca0c564dfa07b9f90cdc7d3e47f8545f6f06b9556e5a6fcf30f22a0eebdad0cdb60e30a3b3626d77ece2219a9f25eea0d8c2bb8de751fd727a371f1a0adc316aafc459d35adea06c3b4df1929fe9e48e043b2adc80154666634038352a04cc9eef47849e1aff9e18af286a80a0a72bfdb67c4d9ccad3866806993caa3b0a0fa37400267248ba993971f439e7cd650c0b5a65477e08b5df55c1b4159d23337a066314a563a10591582bdb2f69e0cb6160a2faa91d73a00aad0b54d7e24c6f3cda0376f6e26446a1600cbaa22f7f611482c109f149cf0d27f9f1bc48664b4d21c2d80", + "f87220b86ff86d8233868504216ca9e78252089415086dc2a2ea37c844e554b65d17c5183f15ab74870326bfc40e1c008025a06db3917cf09951d0107da89bc605ae88623a7690647ac8a5bc3c1638c8c9de59a032a2121306e6a5e1713d99595a65c14cfd1b1978922173cf69bf5f2d4a0b1a60" + ], + "index": 78 +} diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20817618_3.json b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_3.json new file mode 100644 index 000000000..5f8c1475d --- /dev/null +++ b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_3.json @@ -0,0 +1,10 @@ +{ + "txHash": "0x134665d7d342a678a6c1033df5fe09a4f993708dfca7d1208f4b97662840efc9", + "root": "0xb152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13", + "proof": [ + "f90131a08054ac06deeff9e3269b58b3cf8cfead2012586326e2d51edfdc64603ea92fcfa0c6974714547104b94fac6f0da4e95b50534d960d1ee5e1f21932a212d9a4f42ca052dae1479e618740ffff9885fe9b55a17026903922d16cfd4b1c0733b18f2f24a0acbe9dc865421d745cb159af2cfcd8be5deaf64683d19c77857e5fc25744ac2ca00a6a250aff5ab4711dc9cea5b812f8080caceafa95d5ec578d43285ab27fd562a09a719127a63e2242a33fc5b9232c0d87892c8314c3ed5933527cb6fb95f50278a018cfbac9d7ed346d221124c0e28dfd4e92be373f3170cc62421327084e2bc221a0b74eae762f631f76b434632d03de4862d45e2d5c6a23f5d8b6b4fb806f0070d7a00cc848fcc361f3e01dd562909867bc8a7ab1d329aab2410a500a888f1b6f968a8080808080808080", + "f90211a089437c8fb79d4e0278f780cd90e1d47d2c25096271a1a680e762077778f498e5a065f5057f982b971e6624cfbb2d2acab8ae4a10425bd81b4401b69bc1a2ab31d6a0418f8fc6dceed29c186fa782f71f88f91b133c9070ec598eff02cbcf93d7d634a0d26549a8dd9285213e60286a8e2f8714c86a02bd13939c33fedde54c505a5573a02755be69f4b8e2560a1e4c8476dda8cb80334118bcbeb1f4621ee96aa7f8a418a0ef2ceefc457b6af22d2da599e6017e3ffe7c64dd6a229cad956189ddfeba521ba0df34b08748c2fad457380901f01d06550edab0fb2f176b85fe1b8e8767b58b08a07d30d5330a32f2831e471ecd3249d9561a6ebb91c17627b3cf836d77c9e2f8b3a087e09e649a43bc9c669e21844332f14d207c13cc4455ace13580bf6ef483a0eea005a73cbf870ca0c564dfa07b9f90cdc7d3e47f8545f6f06b9556e5a6fcf30f22a0eebdad0cdb60e30a3b3626d77ece2219a9f25eea0d8c2bb8de751fd727a371f1a0adc316aafc459d35adea06c3b4df1929fe9e48e043b2adc80154666634038352a04cc9eef47849e1aff9e18af286a80a0a72bfdb67c4d9ccad3866806993caa3b0a0fa37400267248ba993971f439e7cd650c0b5a65477e08b5df55c1b4159d23337a066314a563a10591582bdb2f69e0cb6160a2faa91d73a00aad0b54d7e24c6f3cda0376f6e26446a1600cbaa22f7f611482c109f149cf0d27f9f1bc48664b4d21c2d80", + "f8b020b8adf8ab823387850340375aa882fa2b94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000009b26d899b1ac6ec07488929c0fdc5b40455275480000000000000000000000000000000000000000000000000000000006e438a025a06194e5ecbf6742f5f7919b39950f452eb0024344977576bb8ceafeff4410e199a007569f10b4d88cd2e185fb30c10f7a507ee6badebfd65b7e4cb884ca8acc5e11" + ], + "index": 79 +} diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20817618_4.json b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_4.json new file mode 100644 index 000000000..45a9a9ae7 --- /dev/null +++ b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_4.json @@ -0,0 +1,10 @@ +{ + "txHash": "0x66cd8977690a1b8bcc4c0dccf4e241146d8ecd08f65aeaaa2b96002fa06a8b28", + "root": "0xb152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13", + "proof": [ + "f90131a08054ac06deeff9e3269b58b3cf8cfead2012586326e2d51edfdc64603ea92fcfa0c6974714547104b94fac6f0da4e95b50534d960d1ee5e1f21932a212d9a4f42ca052dae1479e618740ffff9885fe9b55a17026903922d16cfd4b1c0733b18f2f24a0acbe9dc865421d745cb159af2cfcd8be5deaf64683d19c77857e5fc25744ac2ca00a6a250aff5ab4711dc9cea5b812f8080caceafa95d5ec578d43285ab27fd562a09a719127a63e2242a33fc5b9232c0d87892c8314c3ed5933527cb6fb95f50278a018cfbac9d7ed346d221124c0e28dfd4e92be373f3170cc62421327084e2bc221a0b74eae762f631f76b434632d03de4862d45e2d5c6a23f5d8b6b4fb806f0070d7a00cc848fcc361f3e01dd562909867bc8a7ab1d329aab2410a500a888f1b6f968a8080808080808080", + "f90211a035c6919f9a75bb19e1af47bef00f47c3f702234fe69a3c2e596711cb9aa129a9a0fa06ad9a8d7a5c5bb841ccdbda97bf92dafa1288837287eb7f1fefac33d00980a0197b18a0170309d9510ec708102c29dd0a06fbfe3a7ed8ec7aacf4cf5ab7a625a0a1be1616a0c134caba1d7d24e3bdb10ad529017eb0f3b98f64ef6a8ab3562674a03eaafff1a78cb8ae018ccd58e8c318e928666c6499ad08cdb693fbab847adc59a032d58358743500d2fa2383200527972324910b8ecee76a859003163aee609549a09f0da441bddf2bbb5fce05c8a07385d29a09e21e3423401bce63fcdfb85cc97ea0e464722c37f377e3999e77caa5d38bd6240b9462457e93faf2d3b1bda3d788dfa05149eee71192d612c65d00ac434ce8b6f2f7d4f4c77012e919a37f4fb9ebc4f5a019dcf5d5b265d9263758e66afb5c24d7fd36cbbf55b7ff7799f361230a066361a06166f16fd0dc59da6313987d228e6a1d178ad5b0f2d3bfaceafd45c09780020fa07e460796b13f8e4855b8d0aac4f517872aad6b5d8e6aadddd0a799c23cd9deaca09f8690880be03f5f355f2cbcc4a8925d1f8d3b42d082f874098ce41581fec299a0ae79026df571e097a3a9e181e49a2a392a3a01f2fb882398c0df93b3b0668641a0479579fc287b5558020279332ed9735dbfbcb46eaff28b0bdca8950f6eec7c30a0baa5c5d8f1b5dd556ab4149ba70d847727b8755b6bba972ae6791d1c8d9751f480", + "f87220b86ff86d8233888503439d541b8252089415086dc2a2ea37c844e554b65d17c5183f15ab7487027d84f7cd40008026a0d69512e3b47f74fc7d8a2c02f0497580299494544c53860198510433385d6658a043aed084197ff3a3b05e0a0020e4802da7f50c0078dff2e197ad4b0affb36929" + ], + "index": 80 +} diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20817618_5.json b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_5.json new file mode 100644 index 000000000..6264b659d --- /dev/null +++ b/bolt-contracts/test/testdata/tx_mpt_proof_20817618_5.json @@ -0,0 +1,10 @@ +{ + "txHash": "0x5f505bec39cdbbe03b9566085e87a4fc19f9a3346e128d01fa0088a6b274f654", + "root": "0xb152ed53b02e03a51211b1f1260d58f5a90ff87a6f209954bd70e8a108112c13", + "proof": [ + "f90131a08054ac06deeff9e3269b58b3cf8cfead2012586326e2d51edfdc64603ea92fcfa0c6974714547104b94fac6f0da4e95b50534d960d1ee5e1f21932a212d9a4f42ca052dae1479e618740ffff9885fe9b55a17026903922d16cfd4b1c0733b18f2f24a0acbe9dc865421d745cb159af2cfcd8be5deaf64683d19c77857e5fc25744ac2ca00a6a250aff5ab4711dc9cea5b812f8080caceafa95d5ec578d43285ab27fd562a09a719127a63e2242a33fc5b9232c0d87892c8314c3ed5933527cb6fb95f50278a018cfbac9d7ed346d221124c0e28dfd4e92be373f3170cc62421327084e2bc221a0b74eae762f631f76b434632d03de4862d45e2d5c6a23f5d8b6b4fb806f0070d7a00cc848fcc361f3e01dd562909867bc8a7ab1d329aab2410a500a888f1b6f968a8080808080808080", + "f90211a035c6919f9a75bb19e1af47bef00f47c3f702234fe69a3c2e596711cb9aa129a9a0fa06ad9a8d7a5c5bb841ccdbda97bf92dafa1288837287eb7f1fefac33d00980a0197b18a0170309d9510ec708102c29dd0a06fbfe3a7ed8ec7aacf4cf5ab7a625a0a1be1616a0c134caba1d7d24e3bdb10ad529017eb0f3b98f64ef6a8ab3562674a03eaafff1a78cb8ae018ccd58e8c318e928666c6499ad08cdb693fbab847adc59a032d58358743500d2fa2383200527972324910b8ecee76a859003163aee609549a09f0da441bddf2bbb5fce05c8a07385d29a09e21e3423401bce63fcdfb85cc97ea0e464722c37f377e3999e77caa5d38bd6240b9462457e93faf2d3b1bda3d788dfa05149eee71192d612c65d00ac434ce8b6f2f7d4f4c77012e919a37f4fb9ebc4f5a019dcf5d5b265d9263758e66afb5c24d7fd36cbbf55b7ff7799f361230a066361a06166f16fd0dc59da6313987d228e6a1d178ad5b0f2d3bfaceafd45c09780020fa07e460796b13f8e4855b8d0aac4f517872aad6b5d8e6aadddd0a799c23cd9deaca09f8690880be03f5f355f2cbcc4a8925d1f8d3b42d082f874098ce41581fec299a0ae79026df571e097a3a9e181e49a2a392a3a01f2fb882398c0df93b3b0668641a0479579fc287b5558020279332ed9735dbfbcb46eaff28b0bdca8950f6eec7c30a0baa5c5d8f1b5dd556ab4149ba70d847727b8755b6bba972ae6791d1c8d9751f480", + "f87220b86ff86d82338985058af3f6c28252089415086dc2a2ea37c844e554b65d17c5183f15ab7487040a50523970008026a05bee04d6213c636154c3c2e87038ffdb3d384c23fb9af65fcc85a890c9cabfe6a02231204d5fe346497f035994acec2dece4efa5109d466e7ce834b7576c237f9e" + ], + "index": 81 +} From f1f361c3662f63c3f571ba09d08252bc5578fcb9 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:52:31 +0200 Subject: [PATCH 5/9] wip: test stacked multiple commitments --- .../src/contracts/BoltChallenger.sol | 12 ++++++++++++ bolt-contracts/test/BoltChallenger.t.sol | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol index 8bac9bb4f..fe77a2f89 100644 --- a/bolt-contracts/src/contracts/BoltChallenger.sol +++ b/bolt-contracts/src/contracts/BoltChallenger.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; +import {console} from "forge-std/Test.sol"; + import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; @@ -126,6 +128,11 @@ contract BoltChallenger is IBoltChallenger { (address txSender, address commitmentSigner, TransactionData memory firstTransactionData) = _recoverCommitmentData(commitments[0]); + console.log("first"); + console.log(txSender); + console.log(commitmentSigner); + console.logBytes32(firstTransactionData.txHash); + transactionsData[0] = firstTransactionData; for (uint256 i = 1; i < commitments.length; i++) { @@ -134,6 +141,11 @@ contract BoltChallenger is IBoltChallenger { transactionsData[i] = otherTransactionData; + console.log("other"); + console.log(otherTxSender); + console.log(otherCommitmentSigner); + console.logBytes32(otherTransactionData.txHash); + // check that all commitments are for the same slot if (commitments[i].slot != targetSlot) { revert UnexpectedMixedSlots(); diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index 9ea074e39..0d759a412 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -355,6 +355,10 @@ contract BoltChallengerTest is Test { // Prove the full defense of a challenge: the block headers, account proof, and tx proofs // are all valid and the proposer has included the transaction in their slot. // This time, the proposer has committed to multiple transactions in their slot. + // + // The test data for this test was generated by querying for an Ethereum block with a + // sender that has sent multiple transactions in the same block. + // Check out https://etherscan.io/block/20817618 uint256 inclusionBlockNumber = 20_817_618; IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](3); @@ -362,6 +366,18 @@ contract BoltChallengerTest is Test { commitments[1] = _createRecentBoltCommitment(inclusionBlockNumber, 2); commitments[2] = _createRecentBoltCommitment(inclusionBlockNumber, 3); + // Sanity check senders of the transactions: they should all be the same + for (uint256 i = 0; i < commitments.length; i++) { + address recovered = commitments[i].signedTx.decodeEnveloped().recoverSender(); + assertEq(recovered, 0xc21fb45Eeb45D883B838E30ABBd2896aE5AC888c); + } + + // Sanity check signers of the commitments: they should all be the same + for (uint256 i = 0; i < commitments.length; i++) { + address signer = ECDSA.recover(_computeCommitmentID(commitments[i].signedTx, commitments[i].slot), commitments[i].signature); + assertEq(signer, target); + } + // Open a challenge vm.prank(challenger); boltChallenger.openChallenge{value: 1 ether}(commitments); @@ -436,6 +452,9 @@ contract BoltChallengerTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(targetPK, commitmentID); commitment.signature = abi.encodePacked(r, s, v); + // Sanity check + assertEq(ECDSA.recover(commitmentID, commitment.signature), target); + return commitment; } From cab76d5afaab9f3e5483af0750a1c32756272294 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:07:16 +0200 Subject: [PATCH 6/9] fix: stacked commitments test --- .../src/contracts/BoltChallenger.sol | 27 ++++++++++--------- bolt-contracts/test/BoltChallenger.t.sol | 19 ++++++++----- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol index fe77a2f89..e1ef6ca01 100644 --- a/bolt-contracts/src/contracts/BoltChallenger.sol +++ b/bolt-contracts/src/contracts/BoltChallenger.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {console} from "forge-std/Test.sol"; - import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; @@ -128,11 +126,6 @@ contract BoltChallenger is IBoltChallenger { (address txSender, address commitmentSigner, TransactionData memory firstTransactionData) = _recoverCommitmentData(commitments[0]); - console.log("first"); - console.log(txSender); - console.log(commitmentSigner); - console.logBytes32(firstTransactionData.txHash); - transactionsData[0] = firstTransactionData; for (uint256 i = 1; i < commitments.length; i++) { @@ -141,11 +134,6 @@ contract BoltChallenger is IBoltChallenger { transactionsData[i] = otherTransactionData; - console.log("other"); - console.log(otherTxSender); - console.log(otherCommitmentSigner); - console.logBytes32(otherTransactionData.txHash); - // check that all commitments are for the same slot if (commitments[i].slot != targetSlot) { revert UnexpectedMixedSlots(); @@ -423,7 +411,20 @@ contract BoltChallenger is IBoltChallenger { function _computeCommitmentID( SignedCommitment calldata commitment ) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(keccak256(commitment.signedTx), abi.encodePacked(commitment.slot))); + return keccak256(abi.encodePacked(keccak256(commitment.signedTx), _toLittleEndian(commitment.slot))); + } + + /// @notice Helper to convert a u64 to a little-endian bytes + /// @param x The u64 to convert + /// @return b The little-endian bytes + function _toLittleEndian( + uint64 x + ) internal pure returns (bytes memory) { + bytes memory b = new bytes(8); + for (uint256 i = 0; i < 8; i++) { + b[i] = bytes1(uint8(x >> (8 * i))); + } + return b; } /// @notice Decode the block header fields from an RLP-encoded block header. diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index 0d759a412..ea47e199e 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -126,7 +126,8 @@ contract BoltChallengerTest is Test { // The transaction we want to prove inclusion of bytes32 txHash = 0x9ec2c56ca36e445a46bc77ca77510f0ef21795d00834269f3752cbd29d63ba1f; - // MPT proof, obtained with the `eth-trie-proof` CLI tool + // MPT proof, obtained with the `trie-proofs` CLI tool from HerodotusDev + // ref: string memory file = vm.readFile("./test/testdata/tx_mpt_proof_20785012.json"); bytes[] memory txProofJson = vm.parseJsonBytesArray(file, ".proof"); bytes memory txProof = _RLPEncodeList(txProofJson); @@ -163,6 +164,8 @@ contract BoltChallengerTest is Test { // =========== Verifying Signatures =========== function testCommitmentDigestAndSignature() public { + // The test commitment has been created in the Bolt sidecar using the Rust + // methods to compute the digest() and recover the signer from the signature. IBoltChallenger.SignedCommitment memory commitment = _parseTestCommitment(); // Reconstruct the commitment digest: `keccak( keccak(signed tx) || le_bytes(slot) )` @@ -220,7 +223,7 @@ contract BoltChallengerTest is Test { assertEq(challenge.openedAt, block.timestamp); assertEq(uint256(challenge.status), 0); assertEq(challenge.challenger, challenger); - assertEq(challenge.commitmentSigner, 0x71f7D1B81E297816cf6691B2396060Ede49eFA5e); + assertEq(challenge.commitmentSigner, 0x27083ED52464625660f3e30Aa5B9C20A30D7E110); assertEq(challenge.targetSlot, commitments[0].slot); } @@ -374,7 +377,8 @@ contract BoltChallengerTest is Test { // Sanity check signers of the commitments: they should all be the same for (uint256 i = 0; i < commitments.length; i++) { - address signer = ECDSA.recover(_computeCommitmentID(commitments[i].signedTx, commitments[i].slot), commitments[i].signature); + bytes32 cid = _computeCommitmentID(commitments[i].signedTx, commitments[i].slot); + address signer = ECDSA.recover(cid, commitments[i].signature); assertEq(signer, target); } @@ -452,6 +456,11 @@ contract BoltChallengerTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(targetPK, commitmentID); commitment.signature = abi.encodePacked(r, s, v); + // Normalize v to 27 or 28 + if (uint8(commitment.signature[64]) < 27) { + commitment.signature[64] = bytes1(uint8(commitment.signature[64]) + 0x1B); + } + // Sanity check assertEq(ECDSA.recover(commitmentID, commitment.signature), target); @@ -477,9 +486,7 @@ contract BoltChallengerTest is Test { // Helper to compute the commitment ID function _computeCommitmentID(bytes memory signedTx, uint64 slot) internal pure returns (bytes32) { - bytes32 txHash = keccak256(signedTx); - bytes memory leSlot = _toLittleEndian(slot); - return keccak256(abi.encodePacked(txHash, leSlot)); + return keccak256(abi.encodePacked(keccak256(signedTx), _toLittleEndian(slot))); } // Helper to encode a list of bytes[] into an RLP list with each item RLP-encoded From 9f62934db28081d34d8152ea6801d16ac828c9cc Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:15:31 +0200 Subject: [PATCH 7/9] feat: update snapshot with 5 commitments --- bolt-contracts/.gas-snapshot | 9 +++++---- bolt-contracts/test/BoltChallenger.t.sol | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/bolt-contracts/.gas-snapshot b/bolt-contracts/.gas-snapshot index 547c4e8f2..bd8495e94 100644 --- a/bolt-contracts/.gas-snapshot +++ b/bolt-contracts/.gas-snapshot @@ -1,15 +1,16 @@ BoltChallengerTest:testCommitmentDigestAndSignature() (gas: 4626) BoltChallengerTest:testCommitmentSignature() (gas: 6754) -BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 111032) -BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 23847) -BoltChallengerTest:testOpenChallengeSingleTx() (gas: 406156) +BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 112649) +BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 25461) +BoltChallengerTest:testOpenChallengeSingleTx() (gas: 407773) BoltChallengerTest:testOpenChallengeWithIncorrectBond() (gas: 17130) BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 17141) BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 17517) BoltChallengerTest:testProveAccountData() (gas: 355542) BoltChallengerTest:testProveHeaderData() (gas: 46228) BoltChallengerTest:testProveTransactionInclusion() (gas: 176543) -BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 697302) +BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 697131) +BoltChallengerTest:testResolveChallengeFullDefenseStackedTxs() (gas: 1166625) BoltManagerEigenLayerTest:testGetNonExistentProposerStatus() (gas: 921620) BoltManagerEigenLayerTest:testGetWhitelistedCollaterals() (gas: 99988) BoltManagerEigenLayerTest:testNonWhitelistedCollateral() (gas: 103013) diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index ea47e199e..1d5c7bf49 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -364,10 +364,12 @@ contract BoltChallengerTest is Test { // Check out https://etherscan.io/block/20817618 uint256 inclusionBlockNumber = 20_817_618; - IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](3); + IBoltChallenger.SignedCommitment[] memory commitments = new IBoltChallenger.SignedCommitment[](5); commitments[0] = _createRecentBoltCommitment(inclusionBlockNumber, 1); commitments[1] = _createRecentBoltCommitment(inclusionBlockNumber, 2); commitments[2] = _createRecentBoltCommitment(inclusionBlockNumber, 3); + commitments[3] = _createRecentBoltCommitment(inclusionBlockNumber, 4); + commitments[4] = _createRecentBoltCommitment(inclusionBlockNumber, 5); // Sanity check senders of the transactions: they should all be the same for (uint256 i = 0; i < commitments.length; i++) { @@ -391,22 +393,33 @@ contract BoltChallengerTest is Test { assertEq(challenges.length, 1); bytes32 challengeID = challenges[0].id; + // headers string memory rawPreviousHeader = vm.readFile("./test/testdata/header_20817617.json"); string memory rawInclusionHeader = vm.readFile("./test/testdata/header_20817618.json"); + + // account string memory ethProof = vm.readFile("./test/testdata/eth_proof_20817617.json"); + + // transactions string memory txProof1 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_1.json"); string memory txProof2 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_2.json"); string memory txProof3 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_3.json"); + string memory txProof4 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_4.json"); + string memory txProof5 = vm.readFile("./test/testdata/tx_mpt_proof_20817618_5.json"); - bytes[] memory txProofs = new bytes[](3); + bytes[] memory txProofs = new bytes[](5); txProofs[0] = _RLPEncodeList(vm.parseJsonBytesArray(txProof1, ".proof")); txProofs[1] = _RLPEncodeList(vm.parseJsonBytesArray(txProof2, ".proof")); txProofs[2] = _RLPEncodeList(vm.parseJsonBytesArray(txProof3, ".proof")); + txProofs[3] = _RLPEncodeList(vm.parseJsonBytesArray(txProof4, ".proof")); + txProofs[4] = _RLPEncodeList(vm.parseJsonBytesArray(txProof5, ".proof")); - uint256[] memory txIndexesInBlock = new uint256[](3); + uint256[] memory txIndexesInBlock = new uint256[](5); txIndexesInBlock[0] = vm.parseJsonUint(txProof1, ".index"); txIndexesInBlock[1] = vm.parseJsonUint(txProof2, ".index"); txIndexesInBlock[2] = vm.parseJsonUint(txProof3, ".index"); + txIndexesInBlock[3] = vm.parseJsonUint(txProof4, ".index"); + txIndexesInBlock[4] = vm.parseJsonUint(txProof5, ".index"); IBoltChallenger.Proof memory proof = IBoltChallenger.Proof({ inclusionBlockNumber: inclusionBlockNumber, From d129e31141327855fd4bd18c912709abae2f809c Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:55:17 +0200 Subject: [PATCH 8/9] chore: add tests, change challenge id formula --- .../src/contracts/BoltChallenger.sol | 83 ++++++++++--------- .../src/interfaces/IBoltChallenger.sol | 3 +- bolt-contracts/test/BoltChallenger.t.sol | 49 +++++++++++ 3 files changed, 95 insertions(+), 40 deletions(-) diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol index e1ef6ca01..068b7c8fe 100644 --- a/bolt-contracts/src/contracts/BoltChallenger.sol +++ b/bolt-contracts/src/contracts/BoltChallenger.sol @@ -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. @@ -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({ @@ -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(); @@ -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. @@ -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 @@ -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; } @@ -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. @@ -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. diff --git a/bolt-contracts/src/interfaces/IBoltChallenger.sol b/bolt-contracts/src/interfaces/IBoltChallenger.sol index 139e65874..7ec5189a3 100644 --- a/bolt-contracts/src/interfaces/IBoltChallenger.sol +++ b/bolt-contracts/src/interfaces/IBoltChallenger.sol @@ -80,6 +80,7 @@ interface IBoltChallenger { error InvalidBlockNumber(); error BondTransferFailed(); error ChallengeNotExpired(); + error ChallengeExpired(); error EmptyCommitments(); error UnexpectedMixedSenders(); error UnexpectedMixedSlots(); @@ -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; } diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol index 1d5c7bf49..a211d1003 100644 --- a/bolt-contracts/test/BoltChallenger.t.sol +++ b/bolt-contracts/test/BoltChallenger.t.sol @@ -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 From 420027c9d14562a321ed5d07e9c532fc3528877d Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:11:26 +0200 Subject: [PATCH 9/9] chore: update docs --- bolt-contracts/README.md | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/bolt-contracts/README.md b/bolt-contracts/README.md index 13979a299..2deae92fe 100644 --- a/bolt-contracts/README.md +++ b/bolt-contracts/README.md @@ -95,7 +95,7 @@ for handling fault attribution in the case of a validator failing to meet their In short, the challenger contract allows any user to challenge a validator's commitment by opening a dispute with the following inputs: -1. The signed commitment made by the validator +1. The signed commitment made by the validator (or a list of commitments on the same slot) 2. An ETH bond to cover the cost of the dispute and disincentivize frivolous challenges The entrypoint is the `openChallenge` function. Once a challenge is opened, a `ChallengeOpened` event @@ -119,22 +119,24 @@ The inputs to the resolution process are as follows: 1. The ID of the challenge to respond to: this is emitted in the `ChallengeOpened` event and is unique. 2. The [inclusion proofs](https://github.com/chainbound/bolt/blob/6c0f1b696cfe3de7e7e3830ac28c369c6ddf271e/bolt-contracts/src/interfaces/IBoltChallenger.sol#L39), consisting of the following components: - a. the block number of the block containing the included transaction - b. the RLP-encoded block header of the block containing the included transaction - c. the account merkle proof of the sender of the included transaction - d. the transaction merkle proof of the included transaction against the header's transaction root - e. the transaction index in the block of the included transaction + a. the block number of the block containing the committed transactions (we call it "inclusionBlock") + b. the RLP-encoded block header of the block **before** the one containing the committed transactions (we call it "previousBlock") + b. the RLP-encoded block header of the block containing the included transactions (aka "inclusionBlock") + c. the account merkle proofs of the sender of the committed transactions against the previousBlock's state root + d. the transaction merkle proofs of the included transactions against the inclusionBlock's transaction root + e. the transaction index in the block of each included transaction If the arbitrator submits a valid response that satisfies the requirements for the challenge, the -challenge is considered DEFENDED and the challenger's bond is slashed to cover the cost of the dispute +challenge is considered `DEFENDED` and the challenger's bond is slashed to cover the cost of the dispute and to incentivize speedy resolution. If no arbitrators respond successfully within the challenge time window, the challenge is considered -LOST and the `BoltChallenger` will keep track of this information for future reference. +`BREACHED` and anyone can call the `resolveExpiredChallenge()` method. The `BoltChallenger` will keep +track of this information for future reference. ### Slashing of validators -If a challenge is LOST (as per the above definition), the validator's stake should be slashed to cover +If a challenge is `BREACHED` (as per the above definition), the validator's stake should be slashed to cover the cost of a missed commitment. This is done by calling the `slash` function on the correct staking adapter and reading into the `BoltChallenger` contract to trustlessly determine if the challenge was lost. @@ -142,8 +144,8 @@ In practice, slashing behaviour is abstracted behind any staking adapter – an which will receive a request to slash a validator's stake and will have a last opportunity to veto the slashing request before it is executed on-chain. -Subscribing to lost challenges from the `BoltChallenger` is a trustless way to determine if a slashing request -is valid according to Bolt Protocol rules. +Subscribing to breached challenge events from the `BoltChallenger` is a trustless way to determine if a slashing +request is valid according to Bolt Protocol rules. ## Testing @@ -178,11 +180,3 @@ The following considerations should be taken into account before interacting wit - Restaking is a complex process that involves trusting external systems and smart contracts. - Validators should be aware of the potential for slashing if they fail to meet their commitments or engage in malicious behavior. - Smart contracts are susceptible to bugs and vulnerabilities that could be exploited by attackers. - -## Conclusion - -The Bolt smart contracts provide a robust and flexible framework for integrating validator registration, -delegation, and restaking mechanism within the Bolt Ecosystem. - -By leveraging the power and security of Symbiotic and Eigenlayer solutions, Bolt offers a sophisticated -solution for staking pools that wish to opt-in to multiple conditions with extreme granularity.