diff --git a/.github/workflows/contracts_ci.yml b/.github/workflows/contracts_ci.yml
new file mode 100644
index 000000000..c09da57e7
--- /dev/null
+++ b/.github/workflows/contracts_ci.yml
@@ -0,0 +1,27 @@
+name: Bolt-contracts CI
+
+on: [push, pull_request]
+
+jobs:
+ check:
+ name: Foundry project
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+ with:
+ version: nightly
+
+ - name: Run tests
+ run: forge test --via-ir -vvv
+ working-directory: bolt-contracts
+
+ - name: Run forge fmt
+ run: forge fmt --check
+ working-directory: bolt-contracts
diff --git a/.gitignore b/.gitignore
index da4d6e6a2..98cbb2478 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*_dump.log
+logs/
target/
.vscode
.idea
diff --git a/.gitmodules b/.gitmodules
index 7fe3b3345..eda39c33e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,6 +4,15 @@
[submodule "bolt-contracts/lib/openzeppelin-contracts"]
path = bolt-contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
-[submodule "bolt-contracts/lib/relic-sdk"]
- path = bolt-contracts/lib/relic-sdk
- url = https://github.com/Relic-Protocol/relic-sdk
+[submodule "bolt-contracts/lib/core"]
+ path = bolt-contracts/lib/core
+ url = https://github.com/symbioticfi/core
+[submodule "bolt-contracts/lib/eigenlayer-contracts"]
+ path = bolt-contracts/lib/eigenlayer-contracts
+ url = https://github.com/layr-labs/eigenlayer-contracts
+[submodule "bolt-contracts/lib/openzeppelin-contracts-upgradeable"]
+ path = bolt-contracts/lib/openzeppelin-contracts-upgradeable
+ url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
+[submodule "bolt-contracts/lib/openzeppelin-foundry-upgrades"]
+ path = bolt-contracts/lib/openzeppelin-foundry-upgrades
+ url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
diff --git a/bolt-contracts/.gas-snapshot b/bolt-contracts/.gas-snapshot
new file mode 100644
index 000000000..f33a01fb9
--- /dev/null
+++ b/bolt-contracts/.gas-snapshot
@@ -0,0 +1,34 @@
+BoltChallengerTest:testCannotResolveChallengeBeforeExpiration() (gas: 434902)
+BoltChallengerTest:testCommitmentDigestAndSignature() (gas: 4626)
+BoltChallengerTest:testCommitmentSignature() (gas: 6761)
+BoltChallengerTest:testOpenAlreadyExistingChallenge() (gas: 13688)
+BoltChallengerTest:testOpenChallengeInvalidSignature() (gas: 45916)
+BoltChallengerTest:testOpenChallengeSingleTx() (gas: 424427)
+BoltChallengerTest:testOpenChallengeWithIncorrectBond() (gas: 24909)
+BoltChallengerTest:testOpenChallengeWithLargebond() (gas: 24920)
+BoltChallengerTest:testOpenChallengeWithSlotInTheFuture() (gas: 25008)
+BoltChallengerTest:testProveAccountData() (gas: 355542)
+BoltChallengerTest:testProveHeaderData() (gas: 46360)
+BoltChallengerTest:testProveTransactionInclusion() (gas: 176543)
+BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 562694)
+BoltChallengerTest:testResolveChallengeFullDefenseStackedTxs() (gas: 939716)
+BoltChallengerTest:testResolveExpiredChallenge() (gas: 426457)
+BoltManagerEigenLayerTest:testDeregisterOperatorFromAVS() (gas: 834106)
+BoltManagerEigenLayerTest:testGetOperatorStake() (gas: 998169)
+BoltManagerEigenLayerTest:testNonExistentProposerStatus() (gas: 980157)
+BoltManagerEigenLayerTest:testNonWhitelistedCollateral() (gas: 103778)
+BoltManagerEigenLayerTest:testProposerStatus() (gas: 1007174)
+BoltManagerEigenLayerTest:testProposersLookaheadStatus() (gas: 2302759)
+BoltManagerEigenLayerTest:testWhitelistedCollaterals() (gas: 99260)
+BoltManagerSymbioticTest:testGetNonExistentProposerStatus() (gas: 1265832)
+BoltManagerSymbioticTest:testGetProposerStatus() (gas: 1508557)
+BoltManagerSymbioticTest:testGetWhitelistedCollaterals() (gas: 17213)
+BoltManagerSymbioticTest:testNonWhitelistedCollateral() (gas: 43806)
+BoltManagerSymbioticTest:testProposersLookaheadStatus() (gas: 2582043)
+BoltManagerSymbioticTest:testReadOperatorStake() (gas: 1541503)
+BoltValidatorsTest:testUnsafeRegistration() (gas: 149361)
+BoltValidatorsTest:testUnsafeRegistrationFailsIfAlreadyRegistered() (gas: 148862)
+BoltValidatorsTest:testUnsafeRegistrationInvalidOperator() (gas: 22820)
+BoltValidatorsTest:testUnsafeRegistrationWhenNotAllowed() (gas: 33183)
+TransactionDecoderTest:testDecodeAllTestCases() (gas: 0)
+TransactionDecoderTest:testDecodeGasUsage() (gas: 53281)
\ No newline at end of file
diff --git a/bolt-contracts/.gitignore b/bolt-contracts/.gitignore
index ccb098322..319411769 100644
--- a/bolt-contracts/.gitignore
+++ b/bolt-contracts/.gitignore
@@ -1,3 +1,9 @@
cache/
out/
broadcast/
+logs/
+
+.env
+
+node_modules/
+target/
diff --git a/bolt-contracts/README.md b/bolt-contracts/README.md
index 0b75ab5c8..6c27065ad 100644
--- a/bolt-contracts/README.md
+++ b/bolt-contracts/README.md
@@ -1,62 +1,283 @@
# Bolt Contracts
-## Registry
-[`BoltRegistry.sol`](./src/contracts/BoltRegistry.sol) keeps track of registered proposers and operators. It allows an operator
-to register by providing a list of validator indexes and depositing some collateral. It also exposes some view methods for off-chain actors to read.
-
-### Registration
-
-```js
-function register(
- uint64[] calldata validatorIndexes,
- string calldata rpc,
- bytes calldata extra
-) external payable;
-
-```
-Besides validatorIndexes, `register` also registers an RPC endpoint and some optional other information in `extra`.
-
-### Exiting
-The exit process is a 2-step process. The first step is triggering the exit, which will put the registrant into an `EXITING` status after which the registrant should be considered inactive. After the `EXIT_COOLDOWN` of 1 day, the exit can be confirmed and the deposit will be returned.
-
-```js
-function startExit() external;
-
-function confirmExit(address payable recipient) external;
-```
-
-### View Methods
-```js
-function isActiveOperator(address _operator) external view returns (bool);
-
-function getOperatorStatus(
- address _operator
-) external view returns (Status);
-
-function getOperatorForValidator(
- uint64 _validatorIndex
-) external view returns (Registrant memory);
-```
-
-## Challenger
-WIP
-
-## Deploying
-```bash
-# Example for Helder devnet. Set PRIVATE_KEY to your hex-encoded private key.
-PRIVATE_KEY=$PRIVATE_KEY forge script script/DeployRegistry.s.sol --rpc-url https://rpc.helder-devnets.xyz --broadcast --legacy
-```
-
-## Registering
-```bash
-# Example for Helder devnet. Set PRIVATE_KEY to your hex-encoded private key.
-export PRIVATE_KEY="0x..."
-export RPC_ADDR="http://test.com"
-export VALIDATOR_INDEXES="1,2,3,4"
-forge script script/RegisterValidators.s.sol --rpc-url https://rpc.helder-devnets.xyz --broadcast --legacy
-```
-
-## Deployments
-| Contract | Network | Address |
-| -------- | ------- | ------- |
-| `BoltRegistry.sol` | Helder (7014190335) | 0xdF11D829eeC4C192774F3Ec171D822f6Cb4C14d9 |
\ No newline at end of file
+## Table of Contents
+
+- [Overview](#overview)
+ - [Architecture](#architecture)
+- [Admin Privileges](#admin-privileges)
+- [Validator Registration: `BoltValidators`](#validator-registration-boltvalidators)
+- [Bolt Network Entrypoint: `BoltManager`](#bolt-network-entrypoint-boltmanager)
+ - [Symbiotic Integration guide for Staking Pools](#symbiotic-integration-guide-for-staking-pools)
+ - [Symbiotic Integration guide for Operators](#symbiotic-integration-guide-for-operators)
+ - [Eigenlayer Integration guides](#eigenlayer-integration-guides)
+- [Fault Proof Challenge and Slashing: `BoltChallenger`](#fault-proof-challenge-and-slashing-boltchallenger)
+- [Testing](#testing)
+- [Security Considerations](#security-considerations)
+- [Conclusion](#conclusion)
+
+## Overview
+
+The Bolt smart contracts cover the following components:
+
+- Registration and delegation logic for validators to authenticate and opt-in to Bolt
+- Operator registration and collateral deposits through flexible restaking protocol integrations (EigenLayer & Symbiotic)
+- Fault proof challenges and resolution *without slashing*
+
+### Architecture
+A high-level overview of architecture is depicted in the diagram below:
+
+
+
+**Notes**
+- All contracts are upgradeable by implementing [ERC1967Proxy](https://docs.openzeppelin.com/contracts/4.x/api/proxy#erc1967).
+- Storage layout safety is maintained with the use of [storage gaps](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps) and validated with the [OpenZeppelin Foundry Upgrades toolkit](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades).
+- There is a single admin address operated by the Bolt team to facilitate upgrades and update system-wide parameters.
+
+## Admin Privileges
+
+The smart contracts are deployed with a single administrator account operated by the Bolt team. In this testnet deployment, all contracts are upgradeable
+and multiple system-wide parameters can be changed by this administrator in the case of bugs, hacks, or other critical events.
+
+## System-wide Parameters: `BoltParameters`
+
+[`BoltParameters`](./src/contracts/BoltParameters.sol) is an upgradeable storage contract that stores system-wide parameters that the other
+contracts can read from. An overview is given in the table below:
+
+| Parameter | Initial Value | Mutable after deployment |
+| -------------------- | --------------- | ------------------------ |
+| `EPOCH_DURATION` | 86400 (1 day) | No |
+| `SLASHING_WINDOW` | 604800 (1 week) | No |
+| `BLOCKHASH_EVM_LOOKBACK` | 256 | No |
+| `ETH2_GENESIS_TIMESTAMP` | 1694786400 | No |
+| `SLOT_TIME` | 12 | No |
+| `JUSTIFICATION_DELAY` | 32 | Yes (by admin) |
+| `MINIMUM_OPERATOR_STAKE` | 1 ether | Yes (by admin) |
+| `MAX_CHALLENGE_DURATION` | 604800 (1 week) | Yes (by admin) |
+| `CHALLENGE_BOND` | 1 ether | Yes (by admin) |
+| `ALLOW_UNSAFE_REGISTRATION` | `true` | Yes (by admin) |
+
+The values of these parameters can also be found in [`config.holesky.json`](./config/config.holesky.json).
+
+## Validator Registration: `BoltValidators`
+
+The [`BoltValidators`](./src/contracts/BoltValidators.sol) contract is the only point of entry for
+validators to signal their intent to participate in Bolt Protocol and authenticate with their BLS private key.
+
+The registration process includes the following steps:
+
+1. Validator signs a message with their BLS private key. This is required to prove that the
+ validator private key is under their control and that they are indeed its owner.
+2. Validator calls the `registerValidator` function providing:
+ 1. Their BLS public key
+ 2. The BLS signature of the registration message
+ 3. The address of the authorized collateral provider
+ 4. The address of the authorized operator
+
+Until the Pectra hard-fork will be activated, the contract will also expose a `registerValidatorUnsafe` function
+that will not check the BLS signature. This is gated by a feature flag that will be turned off post-Pectra and
+will allow us to test the registration flow in a controlled environment.
+
+## Bolt Network Entrypoint: `BoltManager`
+
+The [`BoltManager`](./src/contracts/BoltManager.sol) contract is a crucial component of Bolt that
+integrates with restaking ecosystems Symbiotic and Eigenlayer. It manages the registration and
+coordination of validators, operators, and vaults within the Bolt network.
+
+Key features include:
+
+1. Retrieval of operator stake and proposer status from their pubkey
+2. Integration with Symbiotic
+3. Integration with Eigenlayer
+
+Specific functionalities about the restaking protocols are handled inside
+the `IBoltMiddleware` contracts, such as `BoltSymbioticMiddleware` and `BoltEigenlayerMiddleware`.
+
+### Symbiotic Integration guide for Staking Pools
+
+As a staking pool, it is assumed that you are already in control of a Symbiotic Vault.
+If not, please refer to the [Symbiotic docs](https://docs.symbiotic.fi/handbooks/Handbook%20for%20Vaults)
+on how to spin up a Vault and start receiving stake from your node operators.
+
+Opting into Bolt works as any other Symbiotic middleware integration. Here are the steps:
+
+1. Make sure your vault collateral is whitelisted in `BoltSymbioticMiddleware` by calling `isCollateralWhitelisted`.
+2. Register as a vault in `BoltSymbioticMiddleware` by calling `registerVault`.
+3. Verify that your vault is active in `BoltSymbioticMiddleware` by calling `isVaultEnabled`.
+4. Set the network limit for your vault in Symbiotic with `Vault.delegator().setNetworkLimit()`.
+5. You can now start approving operators that opt in to your vault directly in Symbiotic.
+6. When you assign shares to operators, they are able to provide commitments on behalf of your collateral.
+
+### Symbiotic Integration guide for Operators
+
+As an operator, you will need to opt-in to the Bolt Network and any Vault that trusts you to provide
+commitments on their behalf.
+
+The opt-in process requires the following steps:
+
+1. register in Symbiotic with `OperatorRegistry.registerOperator()`.
+2. opt-in to the Bolt network with `OperatorNetworkOptInService.optIn(networkAddress)`.
+3. opt-in to any vault with `OperatorVaultOptInService.optIn(vaultAddress)`.
+4. register in Bolt with `BoltSymbioticMiddleware.registerOperator(operatorAddress)`.
+5. get approved by the vault.
+6. start providing commitments with the stake provided by the vault.
+
+### EigenLayer Integration Guide for Node Operators and Solo Stakers
+
+> [!NOTE]
+> Without loss of generality, we will assume the reader of this guide is a Node
+> Operator (NO), since the same steps apply to solo stakers.
+
+As a Node Operator you will be an ["Operator"](https://docs.eigenlayer.xyz/eigenlayer/overview/key-terms)
+in the Bolt AVS built on top of EigenLayer. This requires
+running an Ethereum validator and the Bolt sidecar in order issue
+preconfirmations.
+
+The Operator will be represented by an Ethereum address that needs
+to follow the standard procedure outlined in the
+[EigenLayer documentation](https://docs.eigenlayer.xyz/) to opt into EigenLayer. Let's go through the steps:
+
+1. As an Operator, you register into EigenLayer using [`DelegationManager.registerAsOperator`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/mainnet/src/contracts/core/DelegationManager.sol#L107-L119).
+
+2. As an Ethereum validator offering precofirmations a NO needs some collateral in
+ order to be economically credible. In order to do that, some entities known as a "stakers"
+ need to deposit whitelisted Liquid Staking Tokens (LSTs)
+ into an appropriate "Strategy" associated to the LST via the
+ [`StrategyManager.depositIntoStrategy`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/mainnet/src/contracts/core/StrategyManager.sol#L105-L110),
+ so that the Operator has a `min_amount` (TBD) of collateral associated to it.
+ Whitelisted LSTs are exposed by the `BoltEigenLayerMiddleware` contract
+ in the `getWhitelistedCollaterals` function.
+ Note that NOs and stakers can be two different entities
+ _but there is fully trusted relationship as stakers will be slashed if a NO misbehaves_.
+
+3. After the stakers have deposited their collateral into a strategy they need
+ to choose you as their operator. To do that, they need to call the function
+ [`DelegationManager.delegateTo`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/mainnet/src/contracts/core/DelegationManager.sol#L154-L163).
+
+4. As an Operator you finally opt into the Bolt AVS by interacting with the `BoltEigenLayerMiddleware`.
+ This consists in calling the function `BoltEigenLayerMiddleware.registerOperatorToAVS`.
+ The payload is a signature whose digest consists of:
+
+ 1. your operator address
+ 2. the `BoltEigenLayerMiddleware` contract address
+ 3. a salt
+ 4. an expiry 2.
+
+ The contract will then forward the call to the [`AVSDirectory.registerOperatorToAVS`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/mainnet/src/contracts/core/AVSDirectory.sol#L64-L108)
+ with the `msg.sender` set to the Bolt AVS contract. Upon successful verification of the signature,
+ the operator is considered `REGISTERED` in a mapping `avsOperatorStatus[msg.sender][operator]`.
+
+Lastly, a NO needs to interact with both the `BoltValidators` and `BoltEigenLayerMiddleware`
+contract. This is needed for internal functioning of the AVS and to make RPCs aware that you are a
+registered operator and so that they can forward you preconfirmation requests.
+
+The steps required are the following:
+
+1. Register all the validator public keys you want to use with Bolt via the `BoltValidators.registerValidator`.
+ If you own more than one validator public key,
+ you can use the more gas-efficient `BoltValidators.batchRegisterValidators` function.
+ The `authorizedOperator` argument must be the same Ethereum address used for
+ opting into EigenLayer as an Operator.
+
+2. Register the same Operator address in the `BoltEigenLayerMiddleware` contract by calling
+ the `BoltEigenLayerMiddleware.registerOperator` function. This formalizes your role within the Bolt network
+ and allows you to manage operations effectively, such as pausing or resuming
+ your service.
+
+3. Register the EigenLayer strategy you are using for restaking _if it has not been done by someone else already_.
+ This ensures that your restaked assets are correctly integrated with Bolt’s system.
+
+## Fault Proof Challenge: `BoltChallenger`
+
+The [`BoltChallenger`](./src/contracts/BoltChallenger.sol) contract is the component responsible
+for handling fault attribution in the case of a validator failing to meet their commitments.
+
+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 (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
+is emitted, and any arbitrator has a time window to submit a valid response to settle the dispute.
+
+### Dispute resolution
+
+The dispute resolution process is one-shot and requires the arbitrator to submit all necessary evidence
+of the validator's correct behaviour within the challenge time window.
+
+The arbitrator is _anyone_ who can submit a valid response to the challenge. It doesn't have to be the
+validator themselves. There is however one limitation: the time window for submitting a response must be
+respected in the following way:
+
+- Start: the target block must be justified by LMD-GHOST: a minimum of 32 slots must have passed
+- End: depending on the EVM block hash oracle:
+ - . If using the `BLOCKHASH` EVM opcode, the window is limited to 256 blocks (roughly 1 hour)
+ - . If using the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) historical oracle, the window is limited to 8192 blocks (roughly 1 day)
+
+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 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
+and to incentivize speedy resolution.
+
+If no arbitrators respond successfully within the challenge time window, the challenge is considered
+`BREACHED` and anyone can call the `resolveExpiredChallenge()` method. The `BoltChallenger` will keep
+track of this information for future reference.
+
+
+
+## Testing
+
+We use Forge, a fast and flexible Ethereum testing framework, for our smart contract tests.
+Here's a guide to running the test suite for the Bolt contracts:
+
+1. Make sure you have Forge installed. If not, follow the [installation guide](https://book.getfoundry.sh/getting-started/installation).
+
+2. Navigate to the `bolt-contracts` directory
+
+3. Run all tests
+
+ ```
+ forge test
+ ```
+
+4. Run tests with verbose output:
+
+ ```
+ forge test -vvv
+ ```
+
+## Security Considerations
+
+While the Bolt Contracts have been designed with security best practices in mind, it's important
+to note that they are still undergoing audits and should not be used in production environments without
+thorough review and testing. As with any smart contract system, users should exercise caution and conduct
+their own due diligence before interacting with these contracts.
+
+The following considerations should be taken into account before interacting with smart contracts:
+
+- 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.
diff --git a/bolt-contracts/config/holesky/deployments.json b/bolt-contracts/config/holesky/deployments.json
new file mode 100644
index 000000000..2aa89b3ed
--- /dev/null
+++ b/bolt-contracts/config/holesky/deployments.json
@@ -0,0 +1,31 @@
+{
+ "bolt": {
+ "validators": "0x05275a4799cd1B07D81319390fC62Bc7BDbDf269"
+ },
+ "symbiotic": {
+ "network": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
+ "operatorRegistry": "0xAdFC41729fF447974cE27DdFa358A0f2096c3F39",
+ "networkOptInService": "0xF5AFc9FA3Ca63a07E529DDbB6eae55C665cCa83E",
+ "vaultFactory": "0x18C659a269a7172eF78BBC19Fe47ad2237Be0590",
+ "networkRegistry": "0xac5acD8A105C8305fb980734a5AD920b5920106A",
+ "networkMiddlewareService": "0x683F470440964E353b389391CdDDf8df381C282f",
+ "middleware": "",
+ "supportedVaults": [
+ "0x1df2fbfcD600ADd561013f44B2D055E2e974f605",
+ "0xf427d00c34609053d97167352061DD2F0F27F853"
+ ]
+ },
+ "eigenLayer": {
+ "avsDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf",
+ "delegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7",
+ "strategyManager": "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6",
+ "middleware": "",
+ "supportedStrategies": [
+ "0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3",
+ "0x3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0",
+ "0x80528D6e9A2BAbFc766965E0E26d5aB08D9CFaF9",
+ "0x70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6",
+ "0xaccc5A86732BE85b5012e8614AF237801636F8e5"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/bolt-contracts/config/holesky/operator.json b/bolt-contracts/config/holesky/operator.json
new file mode 100644
index 000000000..0b6373c92
--- /dev/null
+++ b/bolt-contracts/config/holesky/operator.json
@@ -0,0 +1,5 @@
+{
+ "rpc": "localhost:50051",
+ "salt": "0x000000000000000abc0000000000000000000000000000000000000000000000",
+ "expiry": null
+}
\ No newline at end of file
diff --git a/bolt-contracts/config/holesky/parameters.json b/bolt-contracts/config/holesky/parameters.json
new file mode 100644
index 000000000..0bdd76e4f
--- /dev/null
+++ b/bolt-contracts/config/holesky/parameters.json
@@ -0,0 +1,12 @@
+{
+ "epochDuration": 86400,
+ "slashingWindow": 604800,
+ "maxChallengeDuration": 604800,
+ "challengeBond": 1000000000000000000,
+ "blockhashEvmLookback": 256,
+ "justificationDelay": 32,
+ "eth2GenesisTimestamp": 1694786400,
+ "slotTime": 12,
+ "allowUnsafeRegistration": true,
+ "minimumOperatorStake": 1000000000000000000
+}
\ No newline at end of file
diff --git a/bolt-contracts/config/holesky/validators.json b/bolt-contracts/config/holesky/validators.json
new file mode 100644
index 000000000..5f065521e
--- /dev/null
+++ b/bolt-contracts/config/holesky/validators.json
@@ -0,0 +1,8 @@
+{
+ "maxCommittedGasLimit": 10000000,
+ "authorizedOperator": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ "pubkeys": [
+ "0xad02df40e24e8534dc4c175163f7a034f99f2e81c43b7ea19904006cb05d001b5301d3444f1812b101803b7f72e340fa",
+ "0xa4c0a8b5dd70d0fe8f05580143bea4c40badb176c2b0c20001cddf897a11bb832137fa4050af1d0420a7dc27568697df"
+ ]
+}
\ No newline at end of file
diff --git a/bolt-contracts/config/test/parameters.json b/bolt-contracts/config/test/parameters.json
new file mode 100644
index 000000000..c9768df44
--- /dev/null
+++ b/bolt-contracts/config/test/parameters.json
@@ -0,0 +1,12 @@
+{
+ "epochDuration": 86400,
+ "slashingWindow": 604800,
+ "maxChallengeDuration": 604800,
+ "challengeBond": 1000000000000000000,
+ "blockhashEvmLookback": 256,
+ "justificationDelay": 32,
+ "eth2GenesisTimestamp": 1606824023,
+ "slotTime": 12,
+ "allowUnsafeRegistration": true,
+ "minimumOperatorStake": 1000000000000000000
+}
\ No newline at end of file
diff --git a/bolt-contracts/docs/admin/avs.json b/bolt-contracts/docs/admin/avs.json
new file mode 100644
index 000000000..8461c6510
--- /dev/null
+++ b/bolt-contracts/docs/admin/avs.json
@@ -0,0 +1,7 @@
+{
+ "name": "Bolt Protocol",
+ "website": "https://boltprotocol.xyz",
+ "description": "Bolt Protocol enables proposers to make credible commitments about their blocks.",
+ "logo": "http://boltprotocol.xyz/wp-content/uploads/2024/07/Bolt-Logo.png",
+ "twitter": "https://twitter.com/boltprotocol_"
+}
\ No newline at end of file
diff --git a/bolt-contracts/docs/admin/deploying.md b/bolt-contracts/docs/admin/deploying.md
new file mode 100644
index 000000000..56de63f22
--- /dev/null
+++ b/bolt-contracts/docs/admin/deploying.md
@@ -0,0 +1,73 @@
+# Deployment Guide
+
+**This should only be done once with the `V1` contracts. For upgrades, refer to the [upgrading doc](./upgrading.md).**
+
+## Configuration
+
+There are 2 JSON configuration files:
+- [`config/holesky/deployments.json`](../../config/holesky/deployments.json): contains deployment addresses of EigenLayer ([here](https://github.com/Layr-Labs/eigenlayer-contracts/blob/dev/README.md#deployments)) and Symbiotic ([here](https://docs.symbiotic.fi/deployments)).
+- [`config/holesky/parameters.json`](../../config/holesky/parameters.json): contains the launch parameters for `BoltParameters`.
+
+
+
+## Deployment Guide
+Make sure we have a full compilation for the Foundry Upgrades Toolkit:
+```bash
+forge clean && forge build
+```
+
+And have a local Anvil fork running to test and validate deployments:
+
+```bash
+anvil --fork-url $HOLESKY_RPC
+```
+
+> [!IMPORTANT]
+> Run everything on the local Anvil fork first! This requires just replacing the $HOLESKY_RPC with the $ANVIL_RPC.
+
+Also set your private keys as environment variables:
+
+```bash
+export NETWORK_PRIVATE_KEY=0x...
+export ADMIN_PRIVATE_KEY=0x...
+```
+
+### Pre-deployment
+
+Register a Symbiotic network for Bolt with the Symbiotic `NetworkRegistry`. The private key with which the script is run will determine the network address. This private key will also need to be used later.
+
+```bash
+forge script script/holesky/admin/helpers/Symbiotic.s.sol --rpc-url $HOLESKY_RPC --private-key $NETWORK_PRIVATE_KEY --broadcast -vvvv --sig "run(string memory arg)" registerNetwork
+```
+
+Make sure `deployments.json` contains the correct address for the Symbiotic network.
+
+### Deployment
+
+Run the following script to deploy Bolt V1:
+```bash
+forge script script/holesky/admin/Deploy.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --broadcast -vvvv
+```
+
+This will deploy all the contracts. The address corresponding to the private key will be the system admin.
+
+Now update `deployments.json` with the Symbiotic and EigenLayer middleware contracts, because we'll need to register it in the next step. Also update the `bolt` section with the correct addresses.
+
+### Post-deployment
+
+Register the deployed `SymbioticMiddleware` with the Symbiotic `NetworkMiddlewareService`. IMPORTANT: this script needs
+to be run with the network private key!
+
+```bash
+forge script script/holesky/admin/helpers/Symbiotic.s.sol --rpc-url $HOLESKY_RPC --private-key $NETWORK_PRIVATE_KEY --broadcast -vvvv --sig "run(string memory arg)" registerMiddleware
+```
+
+Also set the AVS metadata in the EigenLayer AVS Directory, needs to be run with the **admin private key** used at deployment.
+
+```bash
+forge script script/holesky/admin/helpers/RegisterAVS.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --broadcast -vvvv
+```
+
+> [!IMPORTANT]
+> After the `deployments.json` file has been fully updated with the correct contract addresses, push it to Github.
+
diff --git a/bolt-contracts/docs/admin/upgrading.md b/bolt-contracts/docs/admin/upgrading.md
new file mode 100644
index 000000000..65466d91b
--- /dev/null
+++ b/bolt-contracts/docs/admin/upgrading.md
@@ -0,0 +1,16 @@
+# Upgrading Guide
+
+When upgrading contracts, always keep the old implementation of the contracts around and increment the version number.
+For example, when upgrading `BoltManagerV1`, copy it into a new file called `BoltManagerV2` and make your changes.
+
+This is needed to reference check the new contracts with the old contracts so that the OpenZeppelin Upgrades library can
+validate the safety of the upgrade. You MUST add this reference when upgrading a contract:
+
+```solidity
+Options memory opts;
+opts.referenceContract = "BoltManagerV1.sol";
+bytes memory initManager = abi.encodeCall(BoltManagerV2.initialize, (params));
+Upgrades.upgradeProxy(proxy, "BoltManagerV2.sol", initManager, opts);
+```
+
+Before an upgrade, update the [`Upgrade.s.sol`](../script/holesky/Upgrade.s.sol) script to include the correct contracts, references and configurations.
\ No newline at end of file
diff --git a/bolt-contracts/docs/erd.png b/bolt-contracts/docs/erd.png
new file mode 100644
index 000000000..b89c9deeb
Binary files /dev/null and b/bolt-contracts/docs/erd.png differ
diff --git a/bolt-contracts/foundry.toml b/bolt-contracts/foundry.toml
index 25b918f9c..b19612e4a 100644
--- a/bolt-contracts/foundry.toml
+++ b/bolt-contracts/foundry.toml
@@ -1,6 +1,73 @@
[profile.default]
+solc = "0.8.25"
+via_ir = true
src = "src"
out = "out"
libs = ["lib"]
+fs_permissions = [{ access = "read-write", path = "./" }]
+gas_reports = ["*"]
+# Required options for the OpenZeppelin Foundry Upgrades toolkit: https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
+ffi = true
+ast = true
+build_info = true
+extra_output = ["storageLayout"]
+
+# silence some warnings during compilation
+# https://book.getfoundry.sh/reference/config/solidity-compiler#ignored_error_codes
+ignored_error_codes = [3628, 1878, 5574]
+ignored_warnings_from = [
+ "lib/openzeppelin-contracts/contracts",
+ "lib/core",
+ "lib/eigenlayer-contracts",
+]
+
+remappings = [
+ # Bolt remappings
+ "@relic/=lib/relic-sdk/packages/contracts",
+ "@symbiotic/=lib/core/src/",
+ "@eigenlayer/=lib/eigenlayer-contracts/",
+ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
+ "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/",
+ "@openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/",
+
+ # Symbiotic remappings contexts
+ "lib/core/:forge-std/=lib/core/lib/forge-std/src/",
+ "lib/core/:@openzeppelin/contracts/=lib/core/lib/openzeppelin-contracts/contracts/",
+ "lib/core/:@openzeppelin/contracts-upgradeable/=lib/core/lib/openzeppelin-contracts-upgradeable/contracts/",
+
+ # Eigenlayer remappings contexts
+ "lib/eigenlayer-contracts/:@openzeppelin-upgrades/=lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable/",
+ "lib/eigenlayer-contracts/:@openzeppelin/=lib/eigenlayer-contracts/lib/openzeppelin-contracts/",
+ "lib/eigenlayer-contracts/:@openzeppelin-v4.9.0/=lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/",
+ "lib/eigenlayer-contracts/:@openzeppelin-upgrades-v4.9.0/=lib/eigenlayer-contracts/lib/openzeppelin-contracts-upgradeable-v4.9.0/",
+ "lib/eigenlayer-contracts/:ds-test/=lib/eigenlayer-contracts/lib/ds-test/src/",
+ "lib/eigenlayer-contracts/:forge-std/=lib/eigenlayer-contracts/lib/forge-std/src/",
+ "lib/eigenlayer-contracts/lib/openzeppelin-contracts/:@openzeppelin/contracts/=lib/eigenlayer-contracts/lib/openzeppelin-contracts/contracts/",
+
+ # OpenZeppelin remappings contexts
+ "lib/openzeppelin-contracts/:@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
+]
+
+
+[rpc_endpoints]
+mainnet = "${ETH_RPC_URL}"
+holesky = "${ETH_RPC_URL_HOLESKY}"
+
+[fmt]
+bracket_spacing = false
+int_types = "long"
+line_length = 120
+multiline_func_header = "params_first"
+number_underscore = "thousands"
+quote_style = "double"
+tab_width = 4
+
+[fuzz]
+runs = 4096
+max_test_rejects = 262144
+
+# [etherscan]
+# mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}" }
+# holesky = { key = "${ETHERSCAN_API_KEY_HOLESKY}" }
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/bolt-contracts/go.mod b/bolt-contracts/go.mod
new file mode 100644
index 000000000..f5b65da25
--- /dev/null
+++ b/bolt-contracts/go.mod
@@ -0,0 +1,5 @@
+module bolt-contracts
+
+go 1.22.7
+
+require github.com/supranational/blst v0.3.11
diff --git a/bolt-contracts/go.sum b/bolt-contracts/go.sum
new file mode 100644
index 000000000..6f41ee08a
--- /dev/null
+++ b/bolt-contracts/go.sum
@@ -0,0 +1,2 @@
+github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4=
+github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
diff --git a/bolt-contracts/lib/core b/bolt-contracts/lib/core
new file mode 160000
index 000000000..76bf70945
--- /dev/null
+++ b/bolt-contracts/lib/core
@@ -0,0 +1 @@
+Subproject commit 76bf709458410b2682f7bc20c6e3a90845bf4b51
diff --git a/bolt-contracts/lib/eigenlayer-contracts b/bolt-contracts/lib/eigenlayer-contracts
new file mode 160000
index 000000000..00fc4b95e
--- /dev/null
+++ b/bolt-contracts/lib/eigenlayer-contracts
@@ -0,0 +1 @@
+Subproject commit 00fc4b95e9c1a5c4f370e41f56d01052d186da07
diff --git a/bolt-contracts/lib/forge-std b/bolt-contracts/lib/forge-std
index bb4ceea94..978ac6fad 160000
--- a/bolt-contracts/lib/forge-std
+++ b/bolt-contracts/lib/forge-std
@@ -1 +1 @@
-Subproject commit bb4ceea94d6f10eeb5b41dc2391c6c8bf8e734ef
+Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801
diff --git a/bolt-contracts/lib/openzeppelin-contracts-upgradeable b/bolt-contracts/lib/openzeppelin-contracts-upgradeable
new file mode 160000
index 000000000..723f8cab0
--- /dev/null
+++ b/bolt-contracts/lib/openzeppelin-contracts-upgradeable
@@ -0,0 +1 @@
+Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1
diff --git a/bolt-contracts/lib/openzeppelin-foundry-upgrades b/bolt-contracts/lib/openzeppelin-foundry-upgrades
new file mode 160000
index 000000000..16e0ae21e
--- /dev/null
+++ b/bolt-contracts/lib/openzeppelin-foundry-upgrades
@@ -0,0 +1 @@
+Subproject commit 16e0ae21e0e39049f619f2396fa28c57fad07368
diff --git a/bolt-contracts/lib/relic-sdk b/bolt-contracts/lib/relic-sdk
deleted file mode 160000
index 8d6c88e44..000000000
--- a/bolt-contracts/lib/relic-sdk
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 8d6c88e4437eade19a08aca6457d6bd14b06e454
diff --git a/bolt-contracts/main.go b/bolt-contracts/main.go
new file mode 100644
index 000000000..527d6f3d5
--- /dev/null
+++ b/bolt-contracts/main.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "encoding/hex"
+ "fmt"
+ "os"
+ "strings"
+
+ blst "github.com/supranational/blst/bindings/go"
+)
+
+type blsPublicKey = blst.P1Affine
+
+func main() {
+ if len(os.Args) != 2 {
+ fmt.Println("Usage: pubkey_to_g1 ")
+ os.Exit(1)
+ }
+
+ pubkey := strings.TrimPrefix(os.Args[1], "0x")
+
+ pubkeyBytes, err := hex.DecodeString(pubkey)
+ if err != nil {
+ fmt.Println("Failed to decode pubkey:", err)
+ os.Exit(1)
+ }
+
+ if len(pubkeyBytes) != 48 {
+ fmt.Println("Invalid pubkey length")
+ os.Exit(1)
+ }
+
+ G1 := new(blsPublicKey).Uncompress(pubkeyBytes)
+
+ serialized := G1.Serialize()
+
+ x := serialized[0:48]
+ y := serialized[48:]
+
+ fmt.Printf("0x%x,0x%x\n", x, y)
+}
diff --git a/bolt-contracts/script/DeployRegistry.s.sol b/bolt-contracts/script/DeployRegistry.s.sol
deleted file mode 100644
index 28a31ffe8..000000000
--- a/bolt-contracts/script/DeployRegistry.s.sol
+++ /dev/null
@@ -1,21 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {Script, console} from "forge-std/Script.sol";
-
-import {BoltRegistry} from "../src/contracts/BoltRegistry.sol";
-import {BoltChallenger} from "../src/contracts/BoltChallenger.sol";
-
-contract DeployRegistry is Script {
- uint256 public signerKey;
-
- function run() public {
- signerKey = vm.envUint("PRIVATE_KEY");
- vm.startBroadcast(signerKey);
-
- BoltRegistry registry = new BoltRegistry(10 ether);
- console.log("BoltRegistry deployed at", address(registry));
-
- vm.stopBroadcast();
- }
-}
diff --git a/bolt-contracts/script/ReadRegistry.s.sol b/bolt-contracts/script/ReadRegistry.s.sol
deleted file mode 100644
index 0e26d204e..000000000
--- a/bolt-contracts/script/ReadRegistry.s.sol
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {Script, console} from "forge-std/Script.sol";
-
-import {BoltRegistry, IBoltRegistry} from "../src/contracts/BoltRegistry.sol";
-
-contract ReadRegistry is Script {
- address public registryAddress = 0xdF11D829eeC4C192774F3Ec171D822f6Cb4C14d9;
-
- function run() public view {
- console.log("Bolt registry address:", registryAddress);
- BoltRegistry registry = BoltRegistry(registryAddress);
-
- console.log(
- "Bolt registry minimum collateral:",
- registry.MINIMUM_COLLATERAL()
- );
-
- for (uint64 i = 0; i < 2000; i++) {
- try registry.getOperatorForValidator(i) returns (
- IBoltRegistry.Registrant memory operator
- ) {
- console.log(
- "Operator for validator found",
- i,
- ":",
- operator.operator
- );
- console.log("Operator RPC:", operator.metadata.rpc);
- } catch {
- // console.log("No operator for validator", i);
- }
- }
- }
-}
diff --git a/bolt-contracts/script/RegisterValidators.s.sol b/bolt-contracts/script/RegisterValidators.s.sol
deleted file mode 100644
index e4554fd88..000000000
--- a/bolt-contracts/script/RegisterValidators.s.sol
+++ /dev/null
@@ -1,111 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {Script, console} from "forge-std/Script.sol";
-
-import {BoltRegistry} from "../src/contracts/BoltRegistry.sol";
-
-contract RegisterValidators is Script {
- uint256 public signerKey;
- uint64[] public validatorIndexes;
- address public registryAddress = 0xdF11D829eeC4C192774F3Ec171D822f6Cb4C14d9;
-
- function run() public {
- signerKey = vm.envUint("PRIVATE_KEY");
- string memory rpc = vm.envString("RPC_ADDR");
- vm.startBroadcast(signerKey);
-
- string memory validatorIndexesEnv = vm.envString("VALIDATOR_INDEXES");
- uint256[] memory indexes = StringToUintArrayLib.fromStr(validatorIndexesEnv);
- for (uint256 i = 0; i < indexes.length; i++) {
- validatorIndexes.push(uint64(indexes[i]));
- }
-
- console.log("Bolt registry address:", registryAddress);
- BoltRegistry registry = BoltRegistry(registryAddress);
-
- console.log(
- "Bolt registry minimum collateral:",
- registry.MINIMUM_COLLATERAL()
- );
-
- address sender = vm.addr(signerKey);
-
- console.log("Sender address:", sender);
- console.log("Sender balance:", sender.balance);
-
- if (sender.balance < registry.MINIMUM_COLLATERAL()) {
- revert("Insufficient balance");
- }
-
- // Register with minimal collateral
- registry.register{value: registry.MINIMUM_COLLATERAL()}(
- validatorIndexes,
- rpc,
- ""
- );
-
- vm.stopBroadcast();
- }
-}
-
-library StringToUintArrayLib {
- // Maximum number of validators parsed in a single function call
- uint256 constant MAX_VALIDATORS = 256;
-
- function fromStr(string memory s) internal pure returns (uint256[] memory) {
- bytes memory strBytes = bytes(s);
- uint256[] memory vec = new uint256[](MAX_VALIDATORS); // Initial allocation, will resize later
- uint256 vecIndex = 0;
- uint256 tempNum;
- bool parsingRange = false;
- uint256 rangeStart;
-
- for (uint256 i = 0; i < strBytes.length; i++) {
- if (strBytes[i] == ',') {
- if (parsingRange) {
- // Handle end of range
- for (uint256 j = rangeStart; j <= tempNum; j++) {
- vec[vecIndex] = j;
- vecIndex++;
- }
- parsingRange = false;
- } else {
- // Handle single number
- vec[vecIndex] = tempNum;
- vecIndex++;
- }
- tempNum = 0;
- } else if (strBytes[i] == '.') {
- if (i + 1 < strBytes.length && strBytes[i + 1] == '.') {
- // Handle start of range
- parsingRange = true;
- rangeStart = tempNum;
- tempNum = 0;
- i++; // Skip next dot
- }
- } else if (strBytes[i] >= '0' && strBytes[i] <= '9') {
- tempNum = tempNum * 10 + (uint8(strBytes[i]) - 48); // Convert ASCII to integer
- }
- }
-
- // Handle the last part after the final comma (or single number/range end)
- if (parsingRange) {
- for (uint256 j = rangeStart; j <= tempNum; j++) {
- vec[vecIndex] = j;
- vecIndex++;
- }
- } else {
- vec[vecIndex] = tempNum;
- vecIndex++;
- }
-
- // Resize array to actual size
- uint256[] memory result = new uint256[](vecIndex);
- for (uint256 i = 0; i < vecIndex; i++) {
- result[i] = vec[i];
- }
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/bolt-contracts/script/holesky/admin/Deploy.s.sol b/bolt-contracts/script/holesky/admin/Deploy.s.sol
new file mode 100644
index 000000000..554791237
--- /dev/null
+++ b/bolt-contracts/script/holesky/admin/Deploy.s.sol
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {Upgrades, Options} from "@openzeppelin-foundry-upgrades/src/Upgrades.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {IVault} from "@symbiotic/interfaces/vault/IVault.sol";
+import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol";
+
+import {BoltParametersV1} from "../../../src/contracts/BoltParametersV1.sol";
+import {BoltValidatorsV1} from "../../../src/contracts/BoltValidatorsV1.sol";
+import {BoltManagerV1} from "../../../src/contracts/BoltManagerV1.sol";
+import {BoltEigenLayerMiddlewareV1} from "../../../src/contracts/BoltEigenLayerMiddlewareV1.sol";
+import {BoltSymbioticMiddlewareV1} from "../../../src/contracts/BoltSymbioticMiddlewareV1.sol";
+import {BoltConfig} from "../../../src/lib/Config.sol";
+
+/// @notice Script to deploy the Bolt contracts.
+contract DeployBolt is Script {
+ function run() public {
+ // The admin address will be authorized to call the adminOnly functions
+ // on the contract implementations, as well as upgrade the contracts.
+ address admin = msg.sender;
+ console.log("Deploying with admin", admin);
+
+ BoltConfig.Parameters memory config = readParameters();
+ BoltConfig.Deployments memory deployments = readDeployments();
+
+ vm.startBroadcast(admin);
+
+ // TODO: Fix safe deploy, currently failing with `ASTDereferencerError` from openzeppelin
+ Options memory opts;
+ opts.unsafeSkipAllChecks = true;
+
+ bytes memory initParameters = abi.encodeCall(
+ BoltParametersV1.initialize,
+ (
+ admin,
+ config.epochDuration,
+ config.slashingWindow,
+ config.maxChallengeDuration,
+ config.allowUnsafeRegistration,
+ config.challengeBond,
+ config.blockhashEvmLookback,
+ config.justificationDelay,
+ config.eth2GenesisTimestamp,
+ config.slotTime,
+ config.minimumOperatorStake
+ )
+ );
+ address parametersProxy = Upgrades.deployUUPSProxy("BoltParametersV1.sol", initParameters, opts);
+ console.log("BoltParametersV1 proxy deployed at", parametersProxy);
+
+ // Generate the `initialize` call data for the contract.
+ bytes memory initValidators = abi.encodeCall(BoltValidatorsV1.initialize, (admin, parametersProxy));
+ // Deploy the UUPSProxy through the `Upgrades` library, with the correct `initialize` call data.
+ address validatorsProxy = Upgrades.deployUUPSProxy("BoltValidatorsV1.sol", initValidators, opts);
+ console.log("BoltValidatorsV1 proxy deployed at", validatorsProxy);
+
+ bytes memory initManager = abi.encodeCall(BoltManagerV1.initialize, (admin, parametersProxy, validatorsProxy));
+ address managerProxy = Upgrades.deployUUPSProxy("BoltManagerV1.sol", initManager, opts);
+ console.log("BoltManagerV1 proxy deployed at", managerProxy);
+
+ bytes memory initEigenLayerMiddleware = abi.encodeCall(
+ BoltEigenLayerMiddlewareV1.initialize,
+ (
+ admin,
+ parametersProxy,
+ managerProxy,
+ deployments.eigenLayerAVSDirectory,
+ deployments.eigenLayerDelegationManager,
+ deployments.eigenLayerStrategyManager
+ )
+ );
+ address eigenLayerMiddlewareProxy =
+ Upgrades.deployUUPSProxy("BoltEigenLayerMiddlewareV1.sol", initEigenLayerMiddleware, opts);
+ console.log("BoltEigenLayerMiddlewareV1 proxy deployed at", eigenLayerMiddlewareProxy);
+
+ bytes memory initSymbioticMiddleware = abi.encodeCall(
+ BoltSymbioticMiddlewareV1.initialize,
+ (
+ admin,
+ parametersProxy,
+ managerProxy,
+ deployments.symbioticNetwork,
+ deployments.symbioticOperatorRegistry,
+ deployments.symbioticOperatorNetOptIn,
+ deployments.symbioticVaultFactory
+ )
+ );
+ address symbioticMiddlewareProxy =
+ Upgrades.deployUUPSProxy("BoltSymbioticMiddlewareV1.sol", initSymbioticMiddleware, opts);
+ console.log("BoltSymbioticMiddlewareV1 proxy deployed at", address(symbioticMiddlewareProxy));
+
+ console.log("Core contracts deployed succesfully, whitelisting middleware contracts in BoltManager...");
+ console.log("EigenLayer middleware:", address(eigenLayerMiddlewareProxy));
+ console.log("Symbiotic middleware:", address(symbioticMiddlewareProxy));
+ BoltManagerV1(managerProxy).addRestakingProtocol(address(eigenLayerMiddlewareProxy));
+ BoltManagerV1(managerProxy).addRestakingProtocol(address(symbioticMiddlewareProxy));
+
+ console.log("Whitelisted middleware contracts in BoltManager");
+ console.log("Registering supported Symbiotic Vaults...");
+
+ for (uint256 i = 0; i < deployments.supportedVaults.length; i++) {
+ IVault vault = IVault(deployments.supportedVaults[i]);
+ console.log("Registering vault with collateral: %s (address: %s)", vault.collateral(), address(vault));
+ BoltSymbioticMiddlewareV1(symbioticMiddlewareProxy).registerVault(address(deployments.supportedVaults[i]));
+ }
+
+ console.log("Registering supported EigenLayer Strategies...");
+
+ for (uint256 i = 0; i < deployments.supportedStrategies.length; i++) {
+ IStrategy strategy = IStrategy(deployments.supportedStrategies[i]);
+ console.log(
+ "Registering strategy with collateral: %s (address: %s)",
+ address(strategy.underlyingToken()),
+ address(strategy)
+ );
+ BoltEigenLayerMiddlewareV1(eigenLayerMiddlewareProxy).registerStrategy(address(strategy));
+ }
+
+ vm.stopBroadcast();
+ }
+
+ function readParameters() public view returns (BoltConfig.Parameters memory) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/parameters.json");
+ string memory json = vm.readFile(path);
+
+ uint48 epochDuration = uint48(vm.parseJsonUint(json, ".epochDuration"));
+ uint48 slashingWindow = uint48(vm.parseJsonUint(json, ".slashingWindow"));
+ uint48 maxChallengeDuration = uint48(vm.parseJsonUint(json, ".maxChallengeDuration"));
+ bool allowUnsafeRegistration = vm.parseJsonBool(json, ".allowUnsafeRegistration");
+ uint256 challengeBond = vm.parseJsonUint(json, ".challengeBond");
+ uint256 blockhashEvmLookback = vm.parseJsonUint(json, ".blockhashEvmLookback");
+ uint256 justificationDelay = vm.parseJsonUint(json, ".justificationDelay");
+ uint256 eth2GenesisTimestamp = vm.parseJsonUint(json, ".eth2GenesisTimestamp");
+ uint256 slotTime = vm.parseJsonUint(json, ".slotTime");
+ uint256 minimumOperatorStake = vm.parseJsonUint(json, ".minimumOperatorStake");
+
+ return BoltConfig.Parameters({
+ epochDuration: epochDuration,
+ slashingWindow: slashingWindow,
+ maxChallengeDuration: maxChallengeDuration,
+ challengeBond: challengeBond,
+ blockhashEvmLookback: blockhashEvmLookback,
+ justificationDelay: justificationDelay,
+ eth2GenesisTimestamp: eth2GenesisTimestamp,
+ slotTime: slotTime,
+ allowUnsafeRegistration: allowUnsafeRegistration,
+ minimumOperatorStake: minimumOperatorStake
+ });
+ }
+
+ function readDeployments() public view returns (BoltConfig.Deployments memory) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return BoltConfig.Deployments({
+ symbioticNetwork: vm.parseJsonAddress(json, ".symbiotic.network"),
+ symbioticOperatorRegistry: vm.parseJsonAddress(json, ".symbiotic.operatorRegistry"),
+ symbioticOperatorNetOptIn: vm.parseJsonAddress(json, ".symbiotic.networkOptInService"),
+ symbioticVaultFactory: vm.parseJsonAddress(json, ".symbiotic.vaultFactory"),
+ supportedVaults: vm.parseJsonAddressArray(json, ".symbiotic.supportedVaults"),
+ eigenLayerAVSDirectory: vm.parseJsonAddress(json, ".eigenLayer.avsDirectory"),
+ eigenLayerDelegationManager: vm.parseJsonAddress(json, ".eigenLayer.delegationManager"),
+ eigenLayerStrategyManager: vm.parseJsonAddress(json, ".eigenLayer.strategyManager"),
+ supportedStrategies: vm.parseJsonAddressArray(json, ".eigenLayer.supportedStrategies")
+ });
+ }
+}
diff --git a/bolt-contracts/script/holesky/admin/Upgrade.s.sol b/bolt-contracts/script/holesky/admin/Upgrade.s.sol
new file mode 100644
index 000000000..6ca5b0518
--- /dev/null
+++ b/bolt-contracts/script/holesky/admin/Upgrade.s.sol
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {Upgrades} from "@openzeppelin-foundry-upgrades/src/Upgrades.sol";
+
+import {BoltParametersV1} from "../../../src/contracts/BoltParametersV1.sol";
+import {BoltValidatorsV1} from "../../../src/contracts/BoltValidatorsV1.sol";
+import {BoltManagerV1} from "../../../src/contracts/BoltManagerV1.sol";
+import {BoltEigenLayerMiddlewareV1} from "../../../src/contracts/BoltEigenLayerMiddlewareV1.sol";
+import {BoltSymbioticMiddlewareV1} from "../../../src/contracts/BoltSymbioticMiddlewareV1.sol";
+import {BoltConfig} from "../../../src/lib/Config.sol";
+
+contract UpgradeBolt is Script {
+ function run() public {
+ // TODO: Validate upgrades with Upgrades.validateUpgrade
+
+ // TODO: Upgrade contracts with Upgrades.upgradeProxy
+ }
+}
diff --git a/bolt-contracts/script/holesky/admin/helpers/RegisterAVS.s.sol b/bolt-contracts/script/holesky/admin/helpers/RegisterAVS.s.sol
new file mode 100644
index 000000000..f4a3117d4
--- /dev/null
+++ b/bolt-contracts/script/holesky/admin/helpers/RegisterAVS.s.sol
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {BoltEigenLayerMiddlewareV1} from "../../../../src/contracts/BoltEigenLayerMiddlewareV1.sol";
+
+contract RegisterAVS is Script {
+ function run() public {
+ address admin = msg.sender;
+ console.log("Running with admin address:", admin);
+
+ BoltEigenLayerMiddlewareV1 middleware = BoltEigenLayerMiddlewareV1(readMiddleware());
+
+ string memory avsURI = "https://boltprotocol.xyz/avs.json";
+ console.log("Setting AVS metadata URI to:", avsURI);
+
+ vm.startBroadcast(admin);
+
+ middleware.updateAVSMetadataURI(avsURI);
+ vm.stopBroadcast();
+ }
+
+ function readMiddleware() public view returns (address) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return vm.parseJsonAddress(json, ".eigenLayer.middleware");
+ }
+}
diff --git a/bolt-contracts/script/holesky/admin/helpers/Symbiotic.s.sol b/bolt-contracts/script/holesky/admin/helpers/Symbiotic.s.sol
new file mode 100644
index 000000000..c899a9e79
--- /dev/null
+++ b/bolt-contracts/script/holesky/admin/helpers/Symbiotic.s.sol
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {INetworkRegistry} from "@symbiotic/interfaces/INetworkRegistry.sol";
+import {INetworkMiddlewareService} from "@symbiotic/interfaces/service/INetworkMiddlewareService.sol";
+
+/// forge script script/holesky/Symbiotic.s.sol --rpc-url $RPC_HOLESKY --private-key $NETWORK_PRIVATE_KEY --broadcast -vvvv --sig "run(string memory arg)" registerNetwork
+/// forge script script/holesky/Symbiotic.s.sol --rpc-url $RPC_HOLESKY --private-key $NETWORK_PRIVATE_KEY --broadcast -vvvv --sig "run(string memory arg)" registerMiddleware
+contract SymbioticHelper is Script {
+ function run(
+ string memory arg
+ ) public {
+ address networkAdmin = msg.sender;
+ console.log("Running with network admin", networkAdmin);
+
+ vm.startBroadcast(networkAdmin);
+
+ if (keccak256(abi.encode(arg)) == keccak256(abi.encode("registerNetwork"))) {
+ INetworkRegistry networkRegistry = INetworkRegistry(readNetworkRegistry());
+
+ console.log("Registering network with NetworkRegistry (%s)", address(networkRegistry));
+
+ networkRegistry.registerNetwork();
+ } else if (keccak256(abi.encode(arg)) == keccak256(abi.encode("registerMiddleware"))) {
+ INetworkMiddlewareService middlewareService = INetworkMiddlewareService(readMiddlewareService());
+
+ address middleware = readMiddleware();
+
+ console.log(
+ "Registering network middleware (%s) with MiddlewareService (%s)",
+ middleware,
+ address(middlewareService)
+ );
+
+ middlewareService.setMiddleware(middleware);
+ }
+
+ vm.stopBroadcast();
+ }
+
+ function readNetworkRegistry() public view returns (address) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return vm.parseJsonAddress(json, ".symbiotic.networkRegistry");
+ }
+
+ function readMiddlewareService() public view returns (address) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return vm.parseJsonAddress(json, ".symbiotic.networkMiddlewareService");
+ }
+
+ function readMiddleware() public view returns (address) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return vm.parseJsonAddress(json, ".symbiotic.middleware");
+ }
+}
diff --git a/bolt-contracts/script/holesky/validators/RegisterEigenLayerOperator.s.sol b/bolt-contracts/script/holesky/validators/RegisterEigenLayerOperator.s.sol
new file mode 100644
index 000000000..3acbe2d81
--- /dev/null
+++ b/bolt-contracts/script/holesky/validators/RegisterEigenLayerOperator.s.sol
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol";
+import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol";
+
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+import {BoltEigenLayerMiddlewareV1} from "../../../src/contracts/BoltEigenLayerMiddlewareV1.sol";
+import {IBoltMiddlewareV1} from "../../../src/interfaces/IBoltMiddlewareV1.sol";
+
+contract RegisterEigenLayerOperator is Script {
+ struct OperatorConfig {
+ string rpc;
+ bytes32 salt;
+ uint256 expiry;
+ }
+
+ function run() public {
+ uint256 operatorSk = vm.envUint("OPERATOR_SK");
+
+ address operator = vm.addr(operatorSk);
+
+ BoltEigenLayerMiddlewareV1 middleware = _readMiddleware();
+ IAVSDirectory avsDirectory = _readAvsDirectory();
+ OperatorConfig memory config = _readConfig("config/holesky/operator.json");
+
+ console.log("Registering EigenLayer operator");
+ console.log("Operator address:", operator);
+ console.log("Operator RPC:", config.rpc);
+
+ bytes32 digest = avsDirectory.calculateOperatorAVSRegistrationDigestHash({
+ operator: operator,
+ avs: address(middleware),
+ salt: config.salt,
+ expiry: config.expiry
+ });
+
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(operatorSk, digest);
+ bytes memory rawSignature = abi.encodePacked(r, s, v);
+
+ ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature =
+ ISignatureUtils.SignatureWithSaltAndExpiry(rawSignature, config.salt, config.expiry);
+
+ vm.startBroadcast(operatorSk);
+
+ middleware.registerOperator(config.rpc, operatorSignature);
+ console.log("Successfully registered EigenLayer operator");
+
+ vm.stopBroadcast();
+ }
+
+ function _readMiddleware() public view returns (BoltEigenLayerMiddlewareV1) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return BoltEigenLayerMiddlewareV1(vm.parseJsonAddress(json, ".eigenLayer.middleware"));
+ }
+
+ function _readAvsDirectory() public view returns (IAVSDirectory) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return IAVSDirectory(vm.parseJsonAddress(json, ".eigenLayer.avsDirectory"));
+ }
+
+ function _readConfig(
+ string memory path
+ ) public view returns (OperatorConfig memory) {
+ string memory json = vm.readFile(path);
+
+ bytes32 salt = bytes32(0);
+ uint256 expiry = UINT256_MAX;
+
+ try vm.parseJsonBytes32(json, ".salt") returns (bytes32 val) {
+ salt = val;
+ } catch {
+ console.log("No salt found in config, using 0");
+ }
+
+ try vm.parseJsonUint(json, ".expiry") returns (uint256 val) {
+ expiry = val;
+ } catch {
+ console.log("No expiry found in config, using UINT256_MAX");
+ }
+
+ return OperatorConfig({rpc: vm.parseJsonString(json, ".rpc"), salt: salt, expiry: expiry});
+ }
+}
diff --git a/bolt-contracts/script/holesky/validators/RegisterSymbioticOperator.s.sol b/bolt-contracts/script/holesky/validators/RegisterSymbioticOperator.s.sol
new file mode 100644
index 000000000..0089432c0
--- /dev/null
+++ b/bolt-contracts/script/holesky/validators/RegisterSymbioticOperator.s.sol
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {BoltSymbioticMiddlewareV1} from "../../../src/contracts/BoltSymbioticMiddlewareV1.sol";
+import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol";
+
+contract RegisterSymbioticOperator is Script {
+ struct Config {
+ string rpc;
+ BoltSymbioticMiddlewareV1 symbioticMiddleware;
+ IOptInService symbioticNetworkOptInService;
+ address symbioticNetwork;
+ }
+
+ function run() public {
+ uint256 operatorSk = vm.envUint("OPERATOR_SK");
+
+ address operator = vm.addr(operatorSk);
+
+ Config memory config = _readConfig();
+
+ // First, make sure the operator is opted into the network
+ if (!config.symbioticNetworkOptInService.isOptedIn(operator, config.symbioticNetwork)) {
+ console.log("Operator is not opted into the network yet. Opting in...");
+ vm.startBroadcast(operatorSk);
+ config.symbioticNetworkOptInService.optIn(config.symbioticNetwork);
+ vm.stopBroadcast();
+ console.log("Operator successfully opted into the network");
+ }
+
+ console.log("Registering Symbiotic operator");
+ console.log("Operator address:", operator);
+ console.log("Operator RPC:", config.rpc);
+
+ vm.startBroadcast(operatorSk);
+ config.symbioticMiddleware.registerOperator(config.rpc);
+ console.log("Successfully registered Symbiotic operator");
+
+ vm.stopBroadcast();
+ }
+
+ function _readConfig() public view returns (Config memory) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ string memory operatorPath = string.concat(root, "/config/holesky/operator.json");
+ string memory operatorJson = vm.readFile(operatorPath);
+
+ return Config({
+ rpc: vm.parseJsonString(operatorJson, ".rpc"),
+ symbioticNetwork: vm.parseJsonAddress(json, ".symbiotic.network"),
+ symbioticMiddleware: BoltSymbioticMiddlewareV1(vm.parseJsonAddress(json, ".symbiotic.middleware")),
+ symbioticNetworkOptInService: IOptInService(vm.parseJsonAddress(json, ".symbiotic.networkOptInService"))
+ });
+ }
+}
diff --git a/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol b/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol
new file mode 100644
index 000000000..52fd1d0b3
--- /dev/null
+++ b/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {IBoltValidatorsV1} from "../../../src/interfaces/IBoltValidatorsV1.sol";
+import {BLS12381} from "../../../src/lib/bls/BLS12381.sol";
+
+import {Script, console} from "forge-std/Script.sol";
+
+/// @notice Script to register Ethereum validators to Bolt
+/// @dev this script reads from the config file in /config/holesky/register_validators.json
+contract RegisterValidators is Script {
+ using BLS12381 for BLS12381.G1Point;
+
+ struct RegisterValidatorsConfig {
+ uint128 maxCommittedGasLimit;
+ address authorizedOperator;
+ BLS12381.G1Point[] pubkeys;
+ }
+
+ function run() public {
+ address controller = msg.sender;
+
+ console.log("Registering validators to Bolt");
+ console.log("Controller address: ", controller);
+
+ IBoltValidatorsV1 validators = _readValidators();
+ RegisterValidatorsConfig memory config = _parseConfig();
+
+ vm.startBroadcast(controller);
+ validators.batchRegisterValidatorsUnsafe(config.pubkeys, config.maxCommittedGasLimit, config.authorizedOperator);
+ vm.stopBroadcast();
+
+ console.log("Validators registered successfully");
+ }
+
+ function _readValidators() public view returns (IBoltValidatorsV1) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/deployments.json");
+ string memory json = vm.readFile(path);
+
+ return IBoltValidatorsV1(vm.parseJsonAddress(json, ".bolt.validators"));
+ }
+
+ function _parseConfig() public returns (RegisterValidatorsConfig memory config) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/holesky/validators.json");
+ string memory json = vm.readFile(path);
+
+ config.authorizedOperator = vm.parseJsonAddress(json, ".authorizedOperator");
+ config.maxCommittedGasLimit = uint128(vm.parseJsonUint(json, ".maxCommittedGasLimit"));
+
+ string[] memory pubkeysRaw = vm.parseJsonStringArray(json, ".pubkeys");
+ BLS12381.G1Point[] memory pubkeys = new BLS12381.G1Point[](pubkeysRaw.length);
+
+ for (uint256 i = 0; i < pubkeysRaw.length; i++) {
+ string memory pubkey = pubkeysRaw[i];
+
+ string[] memory convertCmd = new string[](2);
+ convertCmd[0] = "./script/pubkey_to_g1_wrapper.sh";
+ convertCmd[1] = pubkey;
+
+ bytes memory output = vm.ffi(convertCmd);
+ string memory outputStr = string(output);
+ string[] memory array = vm.split(outputStr, ",");
+
+ uint256[2] memory x = _bytesToParts(vm.parseBytes(array[0]));
+ uint256[2] memory y = _bytesToParts(vm.parseBytes(array[1]));
+
+ pubkeys[i] = BLS12381.G1Point(x, y);
+
+ console.log("Registering pubkey:", vm.toString(abi.encodePacked(pubkeys[i].compress())));
+ }
+
+ config.pubkeys = pubkeys;
+ }
+
+ function _bytesToParts(
+ bytes memory data
+ ) public pure returns (uint256[2] memory out) {
+ require(data.length == 48, "Invalid data length");
+
+ uint256 value1;
+ uint256 value2;
+
+ // Load the first 32 bytes into value1
+ assembly {
+ value1 := mload(add(data, 32))
+ }
+ value1 = value1 >> 128; // Clear unwanted upper bits
+
+ // Load the next 16 bytes into value2
+ assembly {
+ value2 := mload(add(data, 48))
+ }
+ // value2 = value2 >> 128;
+
+ out[0] = value1;
+ out[1] = value2;
+ }
+}
diff --git a/bolt-contracts/script/pubkey_to_g1-darwin-amd64 b/bolt-contracts/script/pubkey_to_g1-darwin-amd64
new file mode 100755
index 000000000..d0a8b990e
Binary files /dev/null and b/bolt-contracts/script/pubkey_to_g1-darwin-amd64 differ
diff --git a/bolt-contracts/script/pubkey_to_g1-darwin-arm64 b/bolt-contracts/script/pubkey_to_g1-darwin-arm64
new file mode 100755
index 000000000..fd51c95cc
Binary files /dev/null and b/bolt-contracts/script/pubkey_to_g1-darwin-arm64 differ
diff --git a/bolt-contracts/script/pubkey_to_g1-linux-amd64 b/bolt-contracts/script/pubkey_to_g1-linux-amd64
new file mode 100755
index 000000000..62679828f
Binary files /dev/null and b/bolt-contracts/script/pubkey_to_g1-linux-amd64 differ
diff --git a/bolt-contracts/script/pubkey_to_g1-linux-arm64 b/bolt-contracts/script/pubkey_to_g1-linux-arm64
new file mode 100755
index 000000000..234db8fb6
Binary files /dev/null and b/bolt-contracts/script/pubkey_to_g1-linux-arm64 differ
diff --git a/bolt-contracts/script/pubkey_to_g1_wrapper.sh b/bolt-contracts/script/pubkey_to_g1_wrapper.sh
new file mode 100755
index 000000000..260304707
--- /dev/null
+++ b/bolt-contracts/script/pubkey_to_g1_wrapper.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Notes on cross-compiling the binaries:
+#
+# Linux for arm64:
+# - sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
+# - GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 go build -o pubkey_to_g1-linux-arm64
+#
+# MacOS for amd64:
+# - GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -o pubkey_to_g1-darwin-amd64
+
+
+arch=$(uname -i 2>/dev/null) || arch=$(uname -p)
+if [[ $OSTYPE == linux* ]]; then
+ if [[ $arch == x86_64 ]]; then
+ ./script/pubkey_to_g1-linux-amd64 "$1"
+ elif [[ $arch == aarch64 ]]; then
+ ./script/pubkey_to_g1-linux-arm64 "$1"
+ else
+ exit 1
+ fi
+elif [[ $OSTYPE == darwin* ]]; then
+ if [[ $arch == arm* ]]; then
+ ./script/pubkey_to_g1-darwin-arm64 "$1"
+ elif [[ $arch == x86_64 ]]; then
+ ./script/pubkey_to_g1-darwin-amd64 "$1"
+ else
+ exit 1
+ fi
+else
+ exit 1
+fi
\ No newline at end of file
diff --git a/bolt-contracts/src/contracts/BoltChallenger.sol b/bolt-contracts/src/contracts/BoltChallenger.sol
deleted file mode 100644
index d66784cc9..000000000
--- a/bolt-contracts/src/contracts/BoltChallenger.sol
+++ /dev/null
@@ -1,413 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {IProver} from "relic-sdk/packages/contracts/interfaces/IProver.sol";
-import {IReliquary} from "relic-sdk/packages/contracts/interfaces/IReliquary.sol";
-import {Facts, Fact, FactSignature} from "relic-sdk/packages/contracts/lib/Facts.sol";
-import {FactSigs} from "relic-sdk/packages/contracts/lib/FactSigs.sol";
-import {CoreTypes} from "relic-sdk/packages/contracts/lib/CoreTypes.sol";
-
-import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
-
-import {IBoltRegistry} from "../interfaces/IBoltRegistry.sol";
-import {IBoltChallenger} from "../interfaces/IBoltChallenger.sol";
-
-import {SSZ} from "../lib/SSZ.sol";
-import {SSZContainers} from "../lib/SSZContainers.sol";
-import {BeaconChainUtils} from "../lib/BeaconChainUtils.sol";
-
-contract BoltChallenger is IBoltChallenger {
- /// @notice The max duration of a challenge, after which it is considered resolved
- /// even if no one has provided a valid proof for it.
- uint256 public constant MAX_CHALLENGE_DURATION = 7 days;
-
- /// @notice The bond required to open a challenge. This is to avoid spamming
- /// and DOS attacks on proposers. If a challenge is successful, the bond is
- /// returned to the challenger, otherwise it is sent to the based proposer.
- uint256 public constant CHALLENGE_BOND = 1 ether;
-
- /// @notice The max number of slots that can pass after which a challenge cannot
- /// be opened anymore. This corresponds to about 1 day.
- /// @dev This is a limitation of the `BEACON_ROOTS` contract (see EIP-4788 for more info).
- uint256 internal constant CHALLENGE_RETROACTIVE_TARGET_SLOT_WINDOW = 8190;
-
- /// @notice The address of the BoltRegistry contract
- IBoltRegistry public immutable boltRegistry;
-
- /// @notice The address of the Relic Reliquary contract
- IReliquary public immutable reliquary;
-
- /// @notice The address of the block header prover contract
- IProver public immutable blockHeaderProver;
-
- /// @notice The address of the account info prover contract
- IProver public immutable accountInfoProver;
-
- // Struct to hold all challenge details in stoage
- struct Challenge {
- // The address of the based proposer being challenged
- address basedProposer;
- // The signed commitment that the proposer supposedly failed to honor
- SignedCommitment signedCommitment;
- // The address of the challenger
- address challenger;
- // The beacon root object of the target slot's block header.
- // This is directly fetched from the on-chain BEACON_ROOTS oracle.
- bytes32 targetSlotBeaconRoot;
- // The status of the challenge
- ChallengeStatus status;
- // The timestamp at which the challenge was opened
- uint256 openTimestamp;
- }
-
- /// @notice The struct to hold the inclusion commitment, including the proposer's signature
- /// @dev there can be different kinds of commitments, this is just an example
- struct SignedCommitment {
- uint256 slot;
- uint256 nonce;
- uint256 gasUsed;
- bytes32 transactionHash;
- bytes signedRawTransaction;
- bytes signature;
- }
-
- /// @notice The mapping of challenges, indexed by the unique ID of their inclusion commitment
- mapping(bytes32 => Challenge) public challenges;
-
- /// @notice Constructor
- /// @param _boltRegistry The address of the BoltRegistry contract
- /// @param _reliquary The address of the Relic Reliquary contract
- /// @param _blockHeaderProver The address of the Relic block header prover contract
- constructor(
- address _boltRegistry,
- address _reliquary,
- address _blockHeaderProver,
- address _accountInfoProver
- ) {
- boltRegistry = IBoltRegistry(_boltRegistry);
- reliquary = IReliquary(_reliquary);
-
- // Check if the provided provers are valid
- // TODO: readd this for mainnet deployment (currently disabled for testing)
- // reliquary.checkProver(reliquary.provers(_blockHeaderProver));
- // reliquary.checkProver(reliquary.provers(_accountInfoProver));
-
- blockHeaderProver = IProver(_blockHeaderProver);
- accountInfoProver = IProver(_accountInfoProver);
- }
-
- /// @notice Challenge a proposer if it hasn't honored a preconfirmation.
- /// @notice A challenge requires a bond to be transferred to this contract to avoid spamming.
- /// @param _basedProposer The address of the proposer to challenge
- /// @param _signedCommitment The signed commitment that the proposer is getting challenged for
- function challengeProposer(
- address _basedProposer,
- SignedCommitment calldata _signedCommitment
- ) public payable {
- // First sanity checks
- if (_basedProposer == address(0) || _signedCommitment.slot == 0) {
- revert InvalidChallenge();
- }
-
- // Check if there is a sufficient bond attached to the transaction
- if (msg.value < CHALLENGE_BOND) {
- revert InsufficientBond();
- } else if (msg.value > CHALLENGE_BOND) {
- // Refund the excess bond
- payable(msg.sender).transfer(msg.value - CHALLENGE_BOND);
- }
-
- // Check if the target slot is not too far in the past
- if (
- BeaconChainUtils._getSlotFromTimestamp(block.timestamp) -
- _signedCommitment.slot >
- CHALLENGE_RETROACTIVE_TARGET_SLOT_WINDOW
- ) {
- // Challenges cannot be opened for slots that are too far in the past, because we rely
- // on the BEACON_ROOTS ring buffer to be available for the challenge to be resolved.
- revert TargetSlotTooFarInThePast();
- }
-
- // Check if the proposer is an active based proposer
- if (!boltRegistry.isActiveOperator(_basedProposer)) {
- revert InvalidProposerAddress();
- }
-
- bytes32 commitmentID = _getCommitmentID(_signedCommitment);
-
- // Check if a challenge already exists for the given commitment
- // Challenge duplicates are not allowed
- if (challenges[commitmentID].basedProposer != address(0)) {
- revert ChallengeAlreadyExists();
- }
-
- // Check if the signed commitment was made by the challenged based proposer
- if (
- _recoverCommitmentSigner(
- commitmentID,
- _signedCommitment.signature
- ) != _basedProposer
- ) {
- revert InvalidCommitmentSigner();
- }
-
- // Note: we don't check if the based proposer was actually scheduled for proposal at their
- // target slot. Proposers are expected to not preconfirm if they are not the scheduled proposer,
- // as they would be penalized for it.
-
- // Get the beacon block root for the target slot. We store it in the Challenge so that
- // it can be used even after 8192 slots have passed (the limit of the BEACON_ROOTS contract)
- bytes32 beaconBlockRoot = BeaconChainUtils._getBeaconBlockRoot(
- _signedCommitment.slot
- );
-
- // ==== Create a new challenge ====
-
- challenges[commitmentID] = Challenge({
- basedProposer: _basedProposer,
- challenger: msg.sender,
- signedCommitment: _signedCommitment,
- targetSlotBeaconRoot: beaconBlockRoot,
- status: ChallengeStatus.Pending,
- openTimestamp: block.timestamp
- });
-
- emit NewChallenge(_basedProposer, commitmentID, _signedCommitment.slot);
- }
-
- /// @notice Resolve a challenge by providing a valid proof for the preconfirmation.
- /// @param _challengeID The unique ID of the challenge to resolve
- /// @param _blockHeaderProof The proof of the block header of the target slot
- /// @param _accountDataProof The proof of the account data of the preconfirmed sender
- /// @param _transactionIndex The index of the transaction in the block
- /// @param _inclusionProof The Merkle proof of the transaction's inclusion in the block
- /// @dev anyone can call this function on a pending challenge, but only the challenged based proposer
- /// @dev will be able to provide a valid proof to counter it. If the challenge expires or the proof is invalid,
- /// @dev the challenger will be rewarded with the bond + a portion of the slashed amount.
- function resolveChallenge(
- bytes32 _challengeID,
- bytes calldata _blockHeaderProof,
- bytes calldata _accountDataProof,
- uint256 _transactionIndex,
- bytes32[] calldata _inclusionProof
- ) public {
- Challenge memory challenge = challenges[_challengeID];
-
- // Check if the challenge exists
- if (challenge.basedProposer == address(0)) {
- revert ChallengeNotFound();
- }
-
- // Check if the challenge is still pending
- if (challenge.status != ChallengeStatus.Pending) {
- revert ChallengeAlreadyResolved();
- }
-
- // Check if the challenge has expired.
- // This means that the validator failed to honor the commitment and will get slashed.
- if (
- block.timestamp - challenge.openTimestamp > MAX_CHALLENGE_DURATION
- ) {
- // Part of the slashed amount will also be returned to the challenger as a reward.
- // This is the reason we don't have access control in this function.
- // TODO: slash the based proposer.
- _onChallengeSuccess(_challengeID);
- return;
- }
-
- // From here on, we assume the function was called by the based proposer
- if (msg.sender != challenge.basedProposer) {
- revert Unauthorized();
- }
-
- // Derive the block header data of the target block from the block header proof
- CoreTypes.BlockHeaderData
- memory verifiedHeader = _deriveBlockHeaderInfo(_blockHeaderProof);
-
- // Derive the preconfirmed sender's account data from the account data proof
- CoreTypes.AccountData memory verifiedAccount = _deriveAccountData(
- _accountDataProof,
- verifiedHeader.Number
- );
-
- // Check that the nonce of the preconfirmed sender is valid (not too low)
- // at the time of the based proposer's slot.
- if (verifiedAccount.Nonce > challenge.signedCommitment.nonce) {
- // consider the challenge unsuccessful: the user sent a transaction before
- // the proposer could include it, as such it is not at fault.
- _onChallengeFailure(_challengeID);
- return;
- }
-
- // Check that the balance of the preconfirmed sender is enough to cover the base fee
- // of the block.
- if (
- verifiedAccount.Balance <
- challenge.signedCommitment.gasUsed * verifiedHeader.BaseFee
- ) {
- // consider the challenge unsuccessful: the user doesn't have enough balance to cover the gas
- // thus invalidating the preconfirmation: the proposer is not at fault.
- _onChallengeFailure(_challengeID);
- return;
- }
-
- // TODO: we could use the beacon root oracle to check that the based proposer proposed a block
- // at the target slot or if it was reorged. This could be useful to differentiate between a
- // safety vs liveness fault.
-
- // Check if the block header timestamp is UP TO the challenge's target slot.
- // It can be earlier, in case the transaction was included before the based proposer's slot.
- if (
- verifiedHeader.Time >
- BeaconChainUtils._getTimestampFromSlot(
- challenge.signedCommitment.slot
- )
- ) {
- // The block header timestamp is after the target slot, so the proposer didn't
- // honor the preconfirmation and the challenge is successful.
- // TODO: slash the based proposer
- _onChallengeSuccess(_challengeID);
- return;
- }
-
- bool isValid = _verifyInclusionProof(
- verifiedHeader.TxHash,
- _transactionIndex,
- _inclusionProof,
- challenge.signedCommitment.signedRawTransaction
- );
-
- if (!isValid) {
- // The challenge was successful: the proposer failed to honor the preconfirmation
- // TODO: slash the based proposer
- _onChallengeSuccess(_challengeID);
- } else {
- // The challenge was unsuccessful: the proposer honored the preconfirmation
- _onChallengeFailure(_challengeID);
- }
- }
-
- /// @notice Handle the success of a challenge
- /// @param _challengeID The unique ID of the challenge
- function _onChallengeSuccess(bytes32 _challengeID) internal {
- Challenge storage challenge = challenges[_challengeID];
- challenge.status = ChallengeStatus.Resolved;
- payable(challenge.challenger).transfer(CHALLENGE_BOND);
- emit ChallengeResolved(_challengeID, ChallengeResult.Success);
- }
-
- /// @notice Handle the failure of a challenge
- /// @param _challengeID The unique ID of the challenge
- function _onChallengeFailure(bytes32 _challengeID) internal {
- Challenge storage challenge = challenges[_challengeID];
- challenge.status = ChallengeStatus.Resolved;
- payable(challenge.basedProposer).transfer(CHALLENGE_BOND);
- emit ChallengeResolved(_challengeID, ChallengeResult.Failure);
- }
-
- /// @notice Fetch trustlessly valid block header data
- /// @param _proof The ABI-encoded proof of the block header
- /// @return header The block header data
- function _deriveBlockHeaderInfo(
- bytes calldata _proof
- ) internal returns (CoreTypes.BlockHeaderData memory header) {
- // TODO: handle fee for proving. make payable?
-
- Fact memory fact = blockHeaderProver.prove(_proof, false);
- header = abi.decode(fact.data, (CoreTypes.BlockHeaderData));
-
- if (
- FactSignature.unwrap(fact.sig) !=
- FactSignature.unwrap(FactSigs.blockHeaderSig(header.Number))
- ) {
- revert UnexpectedFactSignature();
- }
- }
-
- /// @notice Fetch trustlessly valid account data at a given block number
- /// @param _proof The ABI-encoded proof of the account data
- /// @param _blockNumber The block number for which the account data is being proven
- /// @return account The account data
- function _deriveAccountData(
- bytes calldata _proof,
- uint256 _blockNumber
- ) internal returns (CoreTypes.AccountData memory account) {
- // TODO: handle fee for proving. make payable?
-
- Fact memory fact = accountInfoProver.prove(_proof, false);
- account = abi.decode(fact.data, (CoreTypes.AccountData));
-
- // verify that the account data proof was provided for the correct block
- if (
- FactSignature.unwrap(fact.sig) !=
- FactSignature.unwrap(FactSigs.accountFactSig(_blockNumber))
- ) {
- revert UnexpectedFactSignature();
- }
- }
-
- /// @notice Verify the inclusion proof of a transaction in a block
- /// @param _transactionsRoot The transactions root of the block
- /// @param _transactionIndex The index of the transaction in the block
- /// @param _inclusionProof The Merkle proof of the transaction's inclusion in the block
- /// @param _signedRawTransaction The signed raw transaction being proven
- /// @return isValid true if the proof is valid, false otherwise
- function _verifyInclusionProof(
- bytes32 _transactionsRoot,
- uint256 _transactionIndex,
- bytes32[] calldata _inclusionProof,
- bytes memory _signedRawTransaction
- ) internal view returns (bool isValid) {
- // Check if the transactions root matches the signed commitment
-
- // The genelized index is the index of the merkle tree generated by the merkleization
- // process of a SSZ list of transactions. Since this list is dynamic and can be of maximum
- // length of 2^21 = 2_097_152, the merkleization process fills the tree with empty hashes,
- // therefore this number is an offset from where transactions hash tree root starts.
- // To read more, check out https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization
- uint256 generalizedIndex = 2_097_152 + _transactionIndex;
-
- bytes32 leaf = SSZContainers._transactionHashTreeRoot(
- _signedRawTransaction
- );
-
- isValid = SSZ._verifyProof(
- _inclusionProof,
- _transactionsRoot,
- leaf,
- generalizedIndex
- );
- }
-
- /// @notice Recover the signer of a commitment
- /// @param _commitmentSignature The signature of the commitment
- /// @param _commitmentHash The keccak hash of an unsigned message
- function _recoverCommitmentSigner(
- bytes32 _commitmentHash,
- bytes calldata _commitmentSignature
- ) internal pure returns (address) {
- (address signer, ECDSA.RecoverError err, ) = ECDSA.tryRecover(
- _commitmentHash,
- _commitmentSignature
- );
- if (err != ECDSA.RecoverError.NoError || signer == address(0)) {
- revert InvalidCommitmentSignature();
- }
-
- return signer;
- }
-
- /// @notice Hashes the inclusion commitment to a unique ID to index the challenge
- function _getCommitmentID(
- SignedCommitment memory _commitment
- ) internal pure returns (bytes32) {
- return
- keccak256(
- abi.encodePacked(
- _commitment.slot,
- _commitment.transactionHash,
- _commitment.signedRawTransaction
- )
- );
- }
-}
diff --git a/bolt-contracts/src/contracts/BoltChallengerV1.sol b/bolt-contracts/src/contracts/BoltChallengerV1.sol
new file mode 100644
index 000000000..66594566d
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltChallengerV1.sol
@@ -0,0 +1,577 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.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";
+
+import {SecureMerkleTrie} from "../lib/trie/SecureMerkleTrie.sol";
+import {MerkleTrie} from "../lib/trie/MerkleTrie.sol";
+import {RLPReader} from "../lib/rlp/RLPReader.sol";
+import {RLPWriter} from "../lib/rlp/RLPWriter.sol";
+import {TransactionDecoder} from "../lib/TransactionDecoder.sol";
+import {IBoltChallengerV1} from "../interfaces/IBoltChallengerV1.sol";
+import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol";
+
+/// @title Bolt Challenger
+/// @notice Contract for managing (creating & resolving) challenges for Bolt inclusion commitments.
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltChallengerV1 is IBoltChallengerV1, OwnableUpgradeable, UUPSUpgradeable {
+ using RLPReader for bytes;
+ using RLPReader for RLPReader.RLPItem;
+ using TransactionDecoder for bytes;
+ using TransactionDecoder for TransactionDecoder.Transaction;
+ using EnumerableSet for EnumerableSet.Bytes32Set;
+
+ // ========= STORAGE =========
+
+ /// @notice Bolt Parameters contract.
+ IBoltParametersV1 public parameters;
+
+ /// @notice The set of existing unique challenge IDs.
+ EnumerableSet.Bytes32Set internal challengeIDs;
+
+ /// @notice The mapping of challenge IDs to their respective challenges.
+ mapping(bytes32 => Challenge) internal challenges;
+
+ // --> Storage layout marker: 4 slots
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[46] private __gap;
+
+ // ========= INITIALIZER =========
+
+ /// @notice Initializer
+ /// @param _owner Address of the owner of the contract
+ /// @param _parameters Address of the Bolt Parameters contract
+ function initialize(address _owner, address _parameters) public initializer {
+ __Ownable_init(_owner);
+
+ parameters = IBoltParametersV1(_parameters);
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= VIEW FUNCTIONS =========
+
+ /// @notice Get all existing challenges.
+ /// @dev Should be used in view contexts only to avoid unnecessary gas costs.
+ /// @return allChallenges The array of all existing challenges.
+ function getAllChallenges() public view returns (Challenge[] memory) {
+ Challenge[] memory allChallenges = new Challenge[](challengeIDs.length());
+
+ for (uint256 i = 0; i < challengeIDs.length(); i++) {
+ allChallenges[i] = challenges[challengeIDs.at(i)];
+ }
+
+ return allChallenges;
+ }
+
+ /// @notice Get all currently non-resolved challenges.
+ /// @dev Should be used in view contexts only to avoid unnecessary gas costs.
+ /// @return openChallenges The array of all currently non-resolved challenges.
+ function getOpenChallenges() public view returns (Challenge[] memory) {
+ uint256 openCount = 0;
+ for (uint256 i = 0; i < challengeIDs.length(); i++) {
+ if (challenges[challengeIDs.at(i)].status == ChallengeStatus.Open) {
+ openCount++;
+ }
+ }
+
+ Challenge[] memory openChallenges = new Challenge[](openCount);
+
+ uint256 j = 0;
+ for (uint256 i = 0; i < challengeIDs.length(); i++) {
+ Challenge memory challenge = challenges[challengeIDs.at(i)];
+ if (challenge.status == ChallengeStatus.Open) {
+ openChallenges[j] = challenge;
+ j++;
+ }
+ }
+
+ return openChallenges;
+ }
+
+ /// @notice Get a challenge by its ID.
+ /// @param challengeID The ID of the challenge to get.
+ /// @return challenge The challenge with the given ID.
+ function getChallengeByID(
+ bytes32 challengeID
+ ) public view returns (Challenge memory) {
+ if (!challengeIDs.contains(challengeID)) {
+ revert ChallengeDoesNotExist();
+ }
+
+ return challenges[challengeID];
+ }
+
+ // ========= CHALLENGE CREATION =========
+
+ /// @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 commitments
+ ) public payable {
+ if (commitments.length == 0) {
+ revert EmptyCommitments();
+ }
+
+ // Check that the attached bond amount is correct
+ if (msg.value != parameters.CHALLENGE_BOND()) {
+ 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 > _getCurrentSlot() - parameters.JUSTIFICATION_DELAY()) {
+ // 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();
+ }
+
+ // 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();
+ }
+ }
+
+ // Add the challenge to the set of challenges
+ challengeIDs.add(challengeID);
+ challenges[challengeID] = Challenge({
+ id: challengeID,
+ openedAt: Time.timestamp(),
+ status: ChallengeStatus.Open,
+ targetSlot: targetSlot,
+ challenger: msg.sender,
+ commitmentSigner: commitmentSigner,
+ commitmentReceiver: txSender,
+ committedTxs: transactionsData
+ });
+ emit ChallengeOpened(challengeID, msg.sender, commitmentSigner);
+ }
+
+ // ========= 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
+ /// 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 resolveOpenChallenge(bytes32 challengeID, Proof calldata proof) public {
+ // Check that the challenge exists
+ if (!challengeIDs.contains(challengeID)) {
+ revert ChallengeDoesNotExist();
+ }
+
+ // 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].targetSlot < _getCurrentSlot() - parameters.BLOCKHASH_EVM_LOOKBACK()) {
+ revert BlockIsTooOld();
+ }
+
+ // Check that the previous block is within the EVM lookback window for block hashes.
+ // 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 - parameters.BLOCKHASH_EVM_LOOKBACK()
+ ) {
+ revert InvalidBlockNumber();
+ }
+
+ // 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, trustedPreviousBlockHash, proof);
+ }
+
+ /// @notice Resolve a challenge that has expired without being resolved.
+ /// @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 {
+ if (!challengeIDs.contains(challengeID)) {
+ revert ChallengeDoesNotExist();
+ }
+
+ Challenge storage challenge = challenges[challengeID];
+
+ if (challenge.status != ChallengeStatus.Open) {
+ revert ChallengeAlreadyResolved();
+ }
+
+ if (challenge.openedAt + parameters.MAX_CHALLENGE_DURATION() >= Time.timestamp()) {
+ revert ChallengeNotExpired();
+ }
+
+ // If the challenge has expired without being resolved, it is considered breached.
+ _settleChallengeResolution(ChallengeStatus.Breached, challenge);
+ }
+
+ /// @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) {
+ revert ChallengeAlreadyResolved();
+ }
+
+ if (challenge.openedAt + parameters.MAX_CHALLENGE_DURATION() < Time.timestamp()) {
+ // If the challenge has expired without being resolved, it is considered breached.
+ // This should be handled by calling the `resolveExpiredChallenge()` function instead.
+ revert ChallengeExpired();
+ }
+
+ // 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
+ bytes32 previousBlockHash = keccak256(proof.previousBlockHeaderRLP);
+ if (previousBlockHash != trustedPreviousBlockHash) {
+ revert InvalidBlockHash();
+ }
+
+ // 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 != previousBlockHash) {
+ revert InvalidParentBlockHash();
+ }
+
+ // 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(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);
+
+ // 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 < committedTxsCount; 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.
+ _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.
+ _settleChallengeResolution(ChallengeStatus.Defended, challenge);
+ return;
+ }
+
+ // Over/Underflow is checked in the previous if statements.
+ //
+ // 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]);
+
+ // 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);
+
+ 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 provided valid proofs.
+ _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);
+ }
+
+ // Remove the challenge from the set of challenges
+ delete challenges[challenge.id];
+ challengeIDs.remove(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.
+ /// @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.
+ /// @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 signatures = new bytes32[](commitments.length);
+ for (uint256 i = 0; i < commitments.length; i++) {
+ signatures[i] = keccak256(commitments[i].signature);
+ }
+
+ return keccak256(abi.encodePacked(signatures));
+ }
+
+ /// @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.
+ function _computeCommitmentID(
+ SignedCommitment calldata commitment
+ ) internal pure returns (bytes32) {
+ 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.
+ /// @param headerRLP The RLP-encoded block header to decode
+ function _decodeBlockHeaderRLP(
+ bytes calldata headerRLP
+ ) 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();
+ blockHeader.timestamp = headerFields[11].readUint256();
+ blockHeader.baseFee = headerFields[15].readUint256();
+ }
+
+ /// @notice Decode the account fields from an RLP-encoded account.
+ /// @param accountRLP The RLP-encoded account to decode
+ /// @return account The decoded account data.
+ function _decodeAccountRLP(
+ bytes memory accountRLP
+ ) internal pure returns (AccountData memory account) {
+ RLPReader.RLPItem[] memory accountFields = accountRLP.toRLPItem().readList();
+
+ account.nonce = accountFields[0].readUint256();
+ account.balance = accountFields[1].readUint256();
+ }
+
+ /// @notice Transfer the full challenge bond to a recipient.
+ /// @param recipient The address to transfer the bond to.
+ function _transferFullBond(
+ address recipient
+ ) internal {
+ (bool success,) = payable(recipient).call{value: parameters.CHALLENGE_BOND()}("");
+ if (!success) {
+ revert BondTransferFailed();
+ }
+ }
+
+ /// @notice Transfer half of the challenge bond to a recipient.
+ /// @param recipient The address to transfer half of the bond to.
+ function _transferHalfBond(
+ address recipient
+ ) internal {
+ (bool success,) = payable(recipient).call{value: parameters.CHALLENGE_BOND() / 2}("");
+ if (!success) {
+ revert BondTransferFailed();
+ }
+ }
+
+ /// @notice Get the slot number from a given timestamp
+ /// @param _timestamp The timestamp
+ /// @return The slot number
+ function _getSlotFromTimestamp(
+ uint256 _timestamp
+ ) internal view returns (uint256) {
+ return (_timestamp - parameters.ETH2_GENESIS_TIMESTAMP()) / parameters.SLOT_TIME();
+ }
+
+ /// @notice Get the timestamp from a given slot
+ /// @param _slot The slot number
+ /// @return The timestamp
+ function _getTimestampFromSlot(
+ uint256 _slot
+ ) internal view returns (uint256) {
+ return parameters.ETH2_GENESIS_TIMESTAMP() + _slot * parameters.SLOT_TIME();
+ }
+
+ /// @notice Get the beacon block root for a given slot
+ /// @param _slot The slot number
+ /// @return The beacon block root
+ function _getBeaconBlockRootAtSlot(
+ uint256 _slot
+ ) internal view returns (bytes32) {
+ uint256 slotTimestamp = parameters.ETH2_GENESIS_TIMESTAMP() + _slot * parameters.SLOT_TIME();
+ return _getBeaconBlockRootAtTimestamp(slotTimestamp);
+ }
+
+ function _getBeaconBlockRootAtTimestamp(
+ uint256 _timestamp
+ ) internal view returns (bytes32) {
+ (bool success, bytes memory data) = parameters.BEACON_ROOTS_CONTRACT().staticcall(abi.encode(_timestamp));
+
+ if (!success || data.length == 0) {
+ revert BeaconRootNotFound();
+ }
+
+ return abi.decode(data, (bytes32));
+ }
+
+ /// @notice Get the latest beacon block root
+ /// @return The beacon block root
+ function _getLatestBeaconBlockRoot() internal view returns (bytes32) {
+ uint256 latestSlot = _getSlotFromTimestamp(block.timestamp);
+ return _getBeaconBlockRootAtSlot(latestSlot);
+ }
+
+ /// @notice Get the current slot
+ /// @return The current slot
+ function _getCurrentSlot() internal view returns (uint256) {
+ return _getSlotFromTimestamp(block.timestamp);
+ }
+
+ /// @notice Check if a timestamp is within the EIP-4788 window
+ /// @param _timestamp The timestamp
+ /// @return True if the timestamp is within the EIP-4788 window, false otherwise
+ function _isWithinEIP4788Window(
+ uint256 _timestamp
+ ) internal view returns (bool) {
+ return _getSlotFromTimestamp(_timestamp) <= _getCurrentSlot() + parameters.EIP4788_WINDOW();
+ }
+}
diff --git a/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol
new file mode 100644
index 000000000..750501777
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
+import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
+import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+
+import {MapWithTimeData} from "../lib/MapWithTimeData.sol";
+import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol";
+import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol";
+import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol";
+import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol";
+
+import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol";
+import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol";
+import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol";
+import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol";
+import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol";
+import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol";
+import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol";
+import {StrategyManagerStorage} from "@eigenlayer/src/contracts/core/StrategyManagerStorage.sol";
+
+/// @title Bolt Manager
+/// @notice This contract is responsible for interfacing with the EigenLayer restaking protocol.
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltEigenLayerMiddlewareV1 is IBoltMiddlewareV1, OwnableUpgradeable, UUPSUpgradeable {
+ using EnumerableSet for EnumerableSet.AddressSet;
+ using EnumerableMap for EnumerableMap.AddressToUintMap;
+ using MapWithTimeData for EnumerableMap.AddressToUintMap;
+
+ // ========= STORAGE =========
+
+ /// @notice Start timestamp of the first epoch.
+ uint48 public START_TIMESTAMP;
+
+ /// @notice Bolt Parameters contract.
+ IBoltParametersV1 public parameters;
+
+ /// @notice Validators registry, where validators are registered via their
+ /// BLS pubkey and are assigned a sequence number.
+ IBoltManagerV1 public manager;
+
+ /// @notice Set of EigenLayer protocol strategies that are used in Bolt Protocol.
+ EnumerableMap.AddressToUintMap private strategies;
+
+ /// @notice Address of the EigenLayer AVS Directory contract.
+ IAVSDirectory public AVS_DIRECTORY;
+
+ /// @notice Address of the EigenLayer Delegation Manager contract.
+ DelegationManagerStorage public DELEGATION_MANAGER;
+
+ /// @notice Address of the EigenLayer Strategy Manager contract.
+ StrategyManagerStorage public STRATEGY_MANAGER;
+
+ /// @notice Name hash of the restaking protocol for identifying the instance of `IBoltMiddleware`.
+ bytes32 public NAME_HASH;
+
+ // --> Storage layout marker: 9 slots
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[41] private __gap;
+
+ // ========= ERRORS =========
+
+ error StrategyNotAllowed();
+ error OperatorAlreadyRegisteredToAVS();
+
+ // ========= INITIALIZER & PROXY FUNCTIONALITY ========= //
+
+ /// @notice Constructor for the BoltEigenLayerMiddleware contract.
+ /// @param _parameters The address of the Bolt Parameters contract.
+ /// @param _manager The address of the Bolt Manager contract.
+ /// @param _eigenlayerAVSDirectory The address of the EigenLayer AVS Directory contract.
+ /// @param _eigenlayerDelegationManager The address of the EigenLayer Delegation Manager contract.
+ /// @param _eigenlayerStrategyManager The address of the EigenLayer Strategy Manager.
+ function initialize(
+ address _owner,
+ address _parameters,
+ address _manager,
+ address _eigenlayerAVSDirectory,
+ address _eigenlayerDelegationManager,
+ address _eigenlayerStrategyManager
+ ) public initializer {
+ __Ownable_init(_owner);
+ parameters = IBoltParametersV1(_parameters);
+ manager = IBoltManagerV1(_manager);
+ START_TIMESTAMP = Time.timestamp();
+
+ AVS_DIRECTORY = IAVSDirectory(_eigenlayerAVSDirectory);
+ DELEGATION_MANAGER = DelegationManagerStorage(_eigenlayerDelegationManager);
+ STRATEGY_MANAGER = StrategyManagerStorage(_eigenlayerStrategyManager);
+ NAME_HASH = keccak256("EIGENLAYER");
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= VIEW FUNCTIONS =========
+
+ /// @notice Get the start timestamp of an epoch.
+ function getEpochStartTs(
+ uint48 epoch
+ ) public view returns (uint48 timestamp) {
+ return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the epoch at a given timestamp.
+ function getEpochAtTs(
+ uint48 timestamp
+ ) public view returns (uint48 epoch) {
+ return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the current epoch.
+ function getCurrentEpoch() public view returns (uint48 epoch) {
+ return getEpochAtTs(Time.timestamp());
+ }
+
+ function getWhitelistedStrategies() public view returns (address[] memory) {
+ return strategies.keys();
+ }
+
+ // ========= ADMIN FUNCTIONS =========
+ /// @notice Register a strategy to work in Bolt Protocol.
+ /// @param strategy The EigenLayer strategy address
+ function registerStrategy(
+ address strategy
+ ) public onlyOwner {
+ if (strategies.contains(strategy)) {
+ revert AlreadyRegistered();
+ }
+
+ if (!STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy))) {
+ revert StrategyNotAllowed();
+ }
+
+ strategies.add(strategy);
+ strategies.enable(strategy);
+ }
+
+ /// @notice Deregister a strategy from working in Bolt Protocol.
+ /// @param strategy The EigenLayer strategy address.
+ function deregisterStrategy(
+ address strategy
+ ) public onlyOwner {
+ if (!strategies.contains(strategy)) {
+ revert NotRegistered();
+ }
+
+ strategies.remove(strategy);
+ }
+
+ // ========= EIGENLAYER MIDDLEWARE LOGIC =========
+
+ /// @notice Allow an operator to signal opt-in to Bolt Protocol.
+ /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator.
+ /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator.
+ /// The msg.sender of this call will be the operator address.
+ function registerOperator(
+ string calldata rpc,
+ ISignatureUtils.SignatureWithSaltAndExpiry calldata operatorSignature
+ ) public {
+ if (manager.isOperator(msg.sender)) {
+ revert AlreadyRegistered();
+ }
+
+ if (!DELEGATION_MANAGER.isOperator(msg.sender)) {
+ revert NotOperator();
+ }
+
+ // Register the operator to the AVS directory for this AVS
+ AVS_DIRECTORY.registerOperatorToAVS(msg.sender, operatorSignature);
+
+ // Register the operator in the manager
+ manager.registerOperator(msg.sender, rpc);
+ }
+
+ /// @notice Deregister an EigenLayer operator from working in Bolt Protocol.
+ /// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator.
+ /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator.
+ function deregisterOperator() public {
+ if (!manager.isOperator(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ AVS_DIRECTORY.deregisterOperatorFromAVS(msg.sender);
+
+ manager.deregisterOperator(msg.sender);
+ }
+
+ /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol.
+ /// @dev Pausing activity does not prevent the operator from being slashable for
+ /// the current network epoch until the end of the slashing window.
+ function pauseOperator() public {
+ manager.pauseOperator(msg.sender);
+ }
+
+ /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol.
+ function unpauseOperator() public {
+ manager.unpauseOperator(msg.sender);
+ }
+
+ /// @notice Allow a strategy to signal indefinite opt-out from Bolt Protocol.
+ function pauseStrategy() public {
+ if (!strategies.contains(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ strategies.disable(msg.sender);
+ }
+
+ /// @notice Allow a disabled strategy to signal opt-in to Bolt Protocol.
+ function unpauseStrategy() public {
+ if (!strategies.contains(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ strategies.enable(msg.sender);
+ }
+
+ /// @notice Check if a strategy is currently enabled to work in Bolt Protocol.
+ /// @param strategy The strategy address to check the enabled status for.
+ /// @return True if the strategy is enabled, false otherwise.
+ function isStrategyEnabled(
+ address strategy
+ ) public view returns (bool) {
+ (uint48 enabledTime, uint48 disabledTime) = strategies.getTimes(strategy);
+ return enabledTime != 0 && disabledTime == 0;
+ }
+
+ /// @notice Get the collaterals and amounts staked by an operator across the supported strategies.
+ ///
+ /// @param operator The operator address to get the collaterals and amounts staked for.
+ /// @return collaterals The collaterals staked by the operator.
+ /// @dev Assumes that the operator is registered and enabled.
+ function getOperatorCollaterals(
+ address operator
+ ) public view returns (address[] memory, uint256[] memory) {
+ address[] memory collateralTokens = new address[](strategies.length());
+ uint256[] memory amounts = new uint256[](strategies.length());
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp()));
+
+ IStrategy[] memory strategyImpls = new IStrategy[](strategies.length());
+
+ for (uint256 i = 0; i < strategies.length(); ++i) {
+ (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i);
+
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ continue;
+ }
+
+ IStrategy strategyImpl = IStrategy(strategy);
+
+ address collateral = address(strategyImpl.underlyingToken());
+ collateralTokens[i] = collateral;
+
+ strategyImpls[i] = strategyImpl;
+ }
+
+ // NOTE: order is preserved, which is why we can use the same index for both arrays below
+ uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategyImpls);
+
+ for (uint256 i = 0; i < strategyImpls.length; ++i) {
+ amounts[i] = strategyImpls[i].sharesToUnderlyingView(shares[i]);
+ }
+
+ return (collateralTokens, amounts);
+ }
+
+ /// @notice Get the amount of tokens delegated to an operator across the allowed strategies.
+ /// @param operator The operator address to get the stake for.
+ /// @param collateral The collateral address to get the stake for.
+ /// @return amount The amount of tokens delegated to the operator of the specified collateral.
+ function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) {
+ uint48 timestamp = Time.timestamp();
+ return getOperatorStakeAt(operator, collateral, timestamp);
+ }
+
+ /// @notice Get the stake of an operator in EigenLayer protocol at a given timestamp.
+ /// @param operator The operator address to check the stake for.
+ /// @param collateral The collateral address to check the stake for.
+ /// @param timestamp The timestamp to check the stake at.
+ /// @return amount The stake of the operator at the given timestamp, in collateral token.
+ function getOperatorStakeAt(
+ address operator,
+ address collateral,
+ uint48 timestamp
+ ) public view returns (uint256 amount) {
+ if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) {
+ revert InvalidQuery();
+ }
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp));
+
+ // NOTE: Can this be done more gas-efficiently?
+ IStrategy[] memory strategyMem = new IStrategy[](1);
+
+ for (uint256 i = 0; i < strategies.length(); i++) {
+ (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i);
+
+ if (collateral != address(IStrategy(strategy).underlyingToken())) {
+ continue;
+ }
+
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ continue;
+ }
+
+ strategyMem[0] = IStrategy(strategy);
+ // NOTE: order is preserved i.e., shares[i] corresponds to strategies[i]
+ uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategyMem);
+ amount += IStrategy(strategy).sharesToUnderlyingView(shares[0]);
+ }
+
+ return amount;
+ }
+
+ // ========= EIGENLAYER AVS FUNCTIONS =========
+
+ /// @notice emits an `AVSMetadataURIUpdated` event indicating the information has updated.
+ /// @param metadataURI The URI for metadata associated with an avs
+ function updateAVSMetadataURI(
+ string calldata metadataURI
+ ) public onlyOwner {
+ AVS_DIRECTORY.updateAVSMetadataURI(metadataURI);
+ }
+
+ // ========= HELPER FUNCTIONS =========
+
+ /// @notice Check if a map entry was active at a given timestamp.
+ /// @param enabledTime The enabled time of the map entry.
+ /// @param disabledTime The disabled time of the map entry.
+ /// @param timestamp The timestamp to check the map entry status at.
+ /// @return True if the map entry was active at the given timestamp, false otherwise.
+ function _wasEnabledAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) {
+ return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp);
+ }
+}
diff --git a/bolt-contracts/src/contracts/BoltManagerV1.sol b/bolt-contracts/src/contracts/BoltManagerV1.sol
new file mode 100644
index 000000000..8f4ac86bb
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltManagerV1.sol
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
+import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {OperatorMapWithTime} from "../lib/OperatorMapWithTime.sol";
+import {EnumerableMap} from "../lib/EnumerableMap.sol";
+import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol";
+import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol";
+import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol";
+import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol";
+
+/// @title Bolt Manager
+/// @notice The Bolt Manager contract is responsible for managing operators & restaking middlewares, and is the
+/// entrypoint contract for all Bolt-related queries for off-chain consumers.
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltManagerV1 is IBoltManagerV1, OwnableUpgradeable, UUPSUpgradeable {
+ using EnumerableSet for EnumerableSet.AddressSet;
+ using EnumerableMap for EnumerableMap.OperatorMap;
+ using OperatorMapWithTime for EnumerableMap.OperatorMap;
+
+ // ========= STORAGE =========
+ /// @notice Start timestamp of the first epoch.
+ uint48 public START_TIMESTAMP;
+
+ /// @notice Bolt Parameters contract.
+ IBoltParametersV1 public parameters;
+
+ /// @notice Validators registry, where validators are registered via their
+ /// BLS pubkey and are assigned a sequence number.
+ IBoltValidatorsV1 public validators;
+
+ /// @notice Set of operator addresses that have opted in to Bolt Protocol.
+ EnumerableMap.OperatorMap private operators;
+
+ /// @notice Set of restaking protocols supported. Each address corresponds to the
+ /// associated Bolt Middleware contract.
+ EnumerableSet.AddressSet private restakingProtocols;
+
+ // --> Storage layout marker: 7 slots
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[43] private __gap;
+
+ modifier onlyMiddleware() {
+ if (!restakingProtocols.contains(msg.sender)) {
+ revert UnauthorizedMiddleware();
+ }
+ _;
+ }
+
+ // ========= INITIALIZER & PROXY FUNCTIONALITY ========== //
+
+ /// @notice The initializer for the BoltManager contract.
+ /// @param _validators The address of the validators registry.
+ function initialize(address _owner, address _parameters, address _validators) public initializer {
+ __Ownable_init(_owner);
+
+ parameters = IBoltParametersV1(_parameters);
+ validators = IBoltValidatorsV1(_validators);
+
+ START_TIMESTAMP = Time.timestamp();
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= VIEW FUNCTIONS =========
+
+ function getEpochStartTs(
+ uint48 epoch
+ ) public view returns (uint48 timestamp) {
+ return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the epoch at a given timestamp.
+ function getEpochAtTs(
+ uint48 timestamp
+ ) public view returns (uint48 epoch) {
+ return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the current epoch.
+ function getCurrentEpoch() public view returns (uint48 epoch) {
+ return getEpochAtTs(Time.timestamp());
+ }
+
+ /// @notice Check if an operator address is authorized to work for a validator,
+ /// given the validator's pubkey hash. This function performs a lookup in the
+ /// validators registry to check if they explicitly authorized the operator.
+ /// @param operator The operator address to check the authorization for.
+ /// @param pubkeyHash The pubkey hash of the validator to check the authorization for.
+ /// @return True if the operator is authorized, false otherwise.
+ function isOperatorAuthorizedForValidator(address operator, bytes32 pubkeyHash) public view returns (bool) {
+ if (operator == address(0) || pubkeyHash == bytes32(0)) {
+ revert InvalidQuery();
+ }
+
+ return validators.getValidatorByPubkeyHash(pubkeyHash).authorizedOperator == operator;
+ }
+
+ /// @notice Returns the addresses of the middleware contracts of restaking protocols supported by Bolt.
+ function getSupportedRestakingProtocols() public view returns (address[] memory middlewares) {
+ return restakingProtocols.values();
+ }
+
+ /// @notice Returns whether an operator is registered with Bolt.
+ function isOperator(
+ address operator
+ ) public view returns (bool) {
+ return operators.contains(operator);
+ }
+
+ /// @notice Get the status of multiple proposers, given their pubkey hashes.
+ /// @param pubkeyHashes The pubkey hashes of the proposers to get the status for.
+ /// @return statuses The statuses of the proposers, including their operator and active stake.
+ function getProposerStatuses(
+ bytes32[] calldata pubkeyHashes
+ ) public view returns (IBoltValidatorsV1.ProposerStatus[] memory statuses) {
+ statuses = new IBoltValidatorsV1.ProposerStatus[](pubkeyHashes.length);
+ for (uint256 i = 0; i < pubkeyHashes.length; ++i) {
+ statuses[i] = getProposerStatus(pubkeyHashes[i]);
+ }
+ }
+
+ /// @notice Get the status of a proposer, given their pubkey hash.
+ /// @param pubkeyHash The pubkey hash of the proposer to get the status for.
+ /// @return status The status of the proposer, including their operator and active stake.
+ function getProposerStatus(
+ bytes32 pubkeyHash
+ ) public view returns (IBoltValidatorsV1.ProposerStatus memory status) {
+ if (pubkeyHash == bytes32(0)) {
+ revert InvalidQuery();
+ }
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp()));
+ // NOTE: this will revert when the proposer does not exist.
+ IBoltValidatorsV1.Validator memory validator = validators.getValidatorByPubkeyHash(pubkeyHash);
+
+ Operator memory operator = operators.get(validator.authorizedOperator);
+
+ status.pubkeyHash = pubkeyHash;
+ status.operator = validator.authorizedOperator;
+ status.operatorRPC = operator.rpc;
+
+ (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(validator.authorizedOperator);
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ return status;
+ }
+
+ (status.collaterals, status.amounts) =
+ IBoltMiddlewareV1(operator.middleware).getOperatorCollaterals(validator.authorizedOperator);
+
+ // NOTE: check if the sum of the collaterals covers the minimum operator stake required.
+
+ uint256 totalOperatorStake = 0;
+ for (uint256 i = 0; i < status.amounts.length; ++i) {
+ totalOperatorStake += status.amounts[i];
+ }
+
+ if (totalOperatorStake < parameters.MINIMUM_OPERATOR_STAKE()) {
+ status.active = false;
+ } else {
+ status.active = true;
+ }
+
+ return status;
+ }
+
+ /// @notice Get the amount staked by an operator for a given collateral asset.
+ function getOperatorStake(address operator, address collateral) public view returns (uint256) {
+ Operator memory operatorData = operators.get(operator);
+
+ return IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral);
+ }
+
+ /// @notice Get the total amount staked of a given collateral asset.
+ function getTotalStake(
+ address collateral
+ ) public view returns (uint256 amount) {
+ // Loop over all of the operators, get their middleware, and retrieve their staked amount.
+ for (uint256 i = 0; i < operators.length(); ++i) {
+ (address operator, IBoltManagerV1.Operator memory operatorData) = operators.at(i);
+ amount += IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral);
+ }
+
+ return amount;
+ }
+
+ // ========= OPERATOR FUNCTIONS ====== //
+
+ /// @notice Registers an operator with Bolt. Only callable by a supported middleware contract.
+ function registerOperator(address operatorAddr, string calldata rpc) external onlyMiddleware {
+ if (operators.contains(operatorAddr)) {
+ revert OperatorAlreadyRegistered();
+ }
+
+ // Create an already enabled operator
+ Operator memory operator = Operator(rpc, msg.sender, Time.timestamp());
+
+ operators.set(operatorAddr, operator);
+ }
+
+ /// @notice De-registers an operator from Bolt. Only callable by a supported middleware contract.
+ function deregisterOperator(
+ address operator
+ ) public onlyMiddleware {
+ operators.remove(operator);
+ }
+
+ /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol.
+ /// @dev Pausing activity does not prevent the operator from being slashable for
+ /// the current network epoch until the end of the slashing window.
+ function pauseOperator(
+ address operator
+ ) external onlyMiddleware {
+ // SAFETY: This will revert if the operator key is not present.
+ operators.disable(operator);
+ }
+
+ /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol.
+ function unpauseOperator(
+ address operator
+ ) external onlyMiddleware {
+ // SAFETY: This will revert if the operator key is not present.
+ operators.enable(operator);
+ }
+
+ /// @notice Check if an operator is currently enabled to work in Bolt Protocol.
+ /// @param operator The operator address to check the enabled status for.
+ /// @return True if the operator is enabled, false otherwise.
+ function isOperatorEnabled(
+ address operator
+ ) public view returns (bool) {
+ if (!operators.contains(operator)) {
+ revert OperatorNotRegistered();
+ }
+
+ (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator);
+ return enabledTime != 0 && disabledTime == 0;
+ }
+
+ // ========= ADMIN FUNCTIONS ========= //
+
+ /// @notice Add a restaking protocol into Bolt
+ /// @param protocolMiddleware The address of the restaking protocol Bolt middleware
+ function addRestakingProtocol(
+ address protocolMiddleware
+ ) public onlyOwner {
+ restakingProtocols.add(protocolMiddleware);
+ }
+
+ /// @notice Remove a restaking protocol from Bolt
+ /// @param protocolMiddleware The address of the restaking protocol Bolt middleware
+ function removeRestakingProtocol(
+ address protocolMiddleware
+ ) public onlyOwner {
+ restakingProtocols.remove(protocolMiddleware);
+ }
+
+ // ========= HELPER FUNCTIONS =========
+
+ /// @notice Check if a map entry was active at a given timestamp.
+ /// @param enabledTime The enabled time of the map entry.
+ /// @param disabledTime The disabled time of the map entry.
+ /// @param timestamp The timestamp to check the map entry status at.
+ /// @return True if the map entry was active at the given timestamp, false otherwise.
+ function _wasEnabledAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) {
+ return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp);
+ }
+}
diff --git a/bolt-contracts/src/contracts/BoltParametersV1.sol b/bolt-contracts/src/contracts/BoltParametersV1.sol
new file mode 100644
index 000000000..034b03b2e
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltParametersV1.sol
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+
+/// @title Bolt Parameters
+/// @notice The BoltParameters contract contains all the parameters for the Bolt protocol.
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltParametersV1 is OwnableUpgradeable, UUPSUpgradeable {
+ // =========== CONSTANTS ========= //
+ /// @dev See EIP-4788 for more info
+ address internal constant BEACON_ROOTS_CONTRACT = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
+
+ /// @notice The EIP-4788 time window in slots
+ uint256 internal constant EIP4788_WINDOW = 8191;
+ // =========== STORAGE =========== //
+
+ // --> Storage layout marker: 0 bits
+
+ /// @notice Duration of an epoch in seconds.
+ uint48 public EPOCH_DURATION;
+
+ /// @notice Duration of the slashing window in seconds.
+ uint48 public SLASHING_WINDOW;
+
+ /// @notice Whether to allow unsafe registration of validators
+ /// @dev Until the BLS12_381 precompile is live, we need to allow unsafe registration
+ /// which means we don't check the BLS signature of the validator pubkey.
+ bool public ALLOW_UNSAFE_REGISTRATION;
+ // --> Storage layout marker: 48 + 48 + 8 = 104 bits
+
+ /// @notice The maximum duration of a challenge before it is automatically considered valid.
+ uint48 public MAX_CHALLENGE_DURATION;
+
+ /// @notice The challenge bond required to open a challenge.
+ uint256 public CHALLENGE_BOND;
+
+ /// @notice The maximum number of blocks to look back for block hashes in the EVM.
+ uint256 public BLOCKHASH_EVM_LOOKBACK;
+
+ /// @notice The number of slots to wait before considering a block justified by LMD-GHOST.
+ uint256 public JUSTIFICATION_DELAY;
+
+ /// @notice The timestamp of the eth2 genesis block.
+ uint256 public ETH2_GENESIS_TIMESTAMP;
+
+ /// @notice The duration of a slot in seconds.
+ uint256 public SLOT_TIME;
+
+ /// @notice The minimum stake required for an operator to be considered active in wei.
+ uint256 public MINIMUM_OPERATOR_STAKE;
+ // --> Storage layout marker: 7 words
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[43] private __gap;
+
+ /// @notice Error emitted when a beacon block root is not found
+ error BeaconRootNotFound();
+
+ // ============== INITIALIZER ============== //
+
+ /// @notice The initializer for the BoltManager contract.
+ /// @param _epochDuration The epoch duration.
+ function initialize(
+ address _owner,
+ uint48 _epochDuration,
+ uint48 _slashingWindow,
+ uint48 _maxChallengeDuration,
+ bool _allowUnsafeRegistration,
+ uint256 _challengeBond,
+ uint256 _blockhashEvmLookback,
+ uint256 _justificationDelay,
+ uint256 _eth2GenesisTimestamp,
+ uint256 _slotTime,
+ uint256 _minimumOperatorStake
+ ) public initializer {
+ __Ownable_init(_owner);
+
+ EPOCH_DURATION = _epochDuration;
+ SLASHING_WINDOW = _slashingWindow;
+ ALLOW_UNSAFE_REGISTRATION = _allowUnsafeRegistration;
+ MAX_CHALLENGE_DURATION = _maxChallengeDuration;
+ CHALLENGE_BOND = _challengeBond;
+ BLOCKHASH_EVM_LOOKBACK = _blockhashEvmLookback;
+ JUSTIFICATION_DELAY = _justificationDelay;
+ ETH2_GENESIS_TIMESTAMP = _eth2GenesisTimestamp;
+ SLOT_TIME = _slotTime;
+ MINIMUM_OPERATOR_STAKE = _minimumOperatorStake;
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= ADMIN METHODS ========= //
+
+ /// @notice Enable or disable the use of the BLS precompile
+ /// @param allowUnsafeRegistration Whether to allow unsafe registration of validators
+ function setAllowUnsafeRegistration(
+ bool allowUnsafeRegistration
+ ) public onlyOwner {
+ ALLOW_UNSAFE_REGISTRATION = allowUnsafeRegistration;
+ }
+
+ /// @notice Set the max challenge duration.
+ /// @param maxChallengeDuration The maximum duration of a challenge before it is automatically considered valid.
+ function setMaxChallengeDuration(
+ uint48 maxChallengeDuration
+ ) public onlyOwner {
+ MAX_CHALLENGE_DURATION = maxChallengeDuration;
+ }
+
+ /// @notice Set the required challenge bond.
+ /// @param challengeBond The challenge bond required to open a challenge.
+ function setChallengeBond(
+ uint256 challengeBond
+ ) public onlyOwner {
+ CHALLENGE_BOND = challengeBond;
+ }
+
+ /// @notice Set the minimum operator stake.
+ /// @param minimumOperatorStake The minimum stake required for an operator to be considered active in wei.
+ function setMinimumOperatorStake(
+ uint256 minimumOperatorStake
+ ) public onlyOwner {
+ MINIMUM_OPERATOR_STAKE = minimumOperatorStake;
+ }
+
+ /// @notice Set the justification delay.
+ /// @param justificationDelay The number of slots to wait before considering a block final.
+ function setJustificationDelay(
+ uint256 justificationDelay
+ ) public onlyOwner {
+ JUSTIFICATION_DELAY = justificationDelay;
+ }
+}
diff --git a/bolt-contracts/src/contracts/BoltRegistry.sol b/bolt-contracts/src/contracts/BoltRegistry.sol
deleted file mode 100644
index c064eda8d..000000000
--- a/bolt-contracts/src/contracts/BoltRegistry.sol
+++ /dev/null
@@ -1,166 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {IBoltRegistry} from "../interfaces/IBoltRegistry.sol";
-
-contract BoltRegistry is IBoltRegistry {
- // Cooldown period after which a based proposer can complete the exit process
- uint256 public constant EXIT_COOLDOWN = 1 days;
-
- // Minimum collateral per operator
- uint256 public immutable MINIMUM_COLLATERAL;
-
- // Mapping to hold the registrants
- mapping(address => Registrant) public registrants;
-
- // Array to hold operator addresses
- address[] public operators;
-
- // Mapping that holds the relationship between validator index and operator address
- mapping(uint64 => address) public delegations;
-
- /// @notice Constructor which can set the minimum collateral required to register
- constructor(uint256 _minimumCollateral) {
- MINIMUM_COLLATERAL = _minimumCollateral;
- }
-
- /// @notice Allows a based proposer to opt-in to the protocol
- function register(
- uint64[] calldata validatorIndexes,
- string calldata rpc,
- bytes calldata extra
- ) external payable {
- if (msg.value < MINIMUM_COLLATERAL) {
- revert InsufficientCollateral();
- }
-
- if (registrants[msg.sender].operator != address(0)) {
- revert AlreadyOptedIn();
- }
-
- MetaData memory metadata = MetaData(rpc, extra);
-
- registrants[msg.sender] = Registrant(
- msg.sender,
- validatorIndexes,
- block.timestamp,
- 0,
- msg.value,
- Status.ACTIVE,
- metadata
- );
-
- operators.push(msg.sender);
-
- // Set the delegations
- for (uint256 i = 0; i < validatorIndexes.length; i++) {
- delegations[validatorIndexes[i]] = msg.sender;
- }
-
- emit Registered(msg.sender, validatorIndexes, metadata);
- }
-
- /// @notice Allows a based proposer to exit out of the protocol.
- /// @dev Requires a second transaction after the cooldown period to complete the exit process.
- function startExit() external {
- Registrant storage registrant = registrants[msg.sender];
-
- if (registrant.operator != msg.sender) {
- revert BasedProposerDoesNotExist();
- }
-
- if (registrant.status == Status.EXITING) {
- revert InvalidStatusChange();
- }
-
- registrant.exitInitiatedAt = block.timestamp;
-
- emit StatusChange(msg.sender, Status.EXITING);
- }
-
- /// @notice Completes the exit process for a based proposer
- /// and sends the funds back to the `recipient` address.
- function confirmExit(address payable recipient) external {
- Registrant storage registrant = registrants[msg.sender];
-
- if (registrant.operator != msg.sender) {
- revert BasedProposerDoesNotExist();
- }
- if (registrant.exitInitiatedAt == 0) {
- revert InvalidStatusChange();
- }
- if (registrant.status == Status.INACTIVE) {
- revert InvalidStatusChange();
- }
-
- if (block.timestamp < registrant.exitInitiatedAt + EXIT_COOLDOWN) {
- revert CooldownNotElapsed();
- }
-
- // Remove operator from the operators array
- for (uint256 i = 0; i < operators.length; i++) {
- if (operators[i] == msg.sender) {
- operators[i] = operators[operators.length - 1];
- operators.pop();
- break;
- }
- }
-
- delete registrants[msg.sender];
-
- for (uint256 i = 0; i < registrant.validatorIndexes.length; i++) {
- delete delegations[registrant.validatorIndexes[i]];
- }
-
- recipient.transfer(registrant.balance);
-
- emit StatusChange(msg.sender, Status.INACTIVE);
- }
-
- /// @notice Check if an address is a based proposer opted into the protocol
- /// @param _operator The address to check
- /// @return True if the address is an active based proposer, false otherwise
- function isActiveOperator(address _operator) public view returns (bool) {
- return registrants[_operator].status == Status.ACTIVE;
- }
-
- /// @notice Get the status of a based proposer
- /// @param _operator The address of the operator
- /// @return The status of the based proposer
- function getOperatorStatus(
- address _operator
- ) external view returns (Status) {
- // Will return INACTIVE if the operator is not registered
- return registrants[_operator].status;
- }
-
- function getOperatorForValidator(
- uint64 _validatorIndex
- ) external view returns (Registrant memory) {
- if (delegations[_validatorIndex] != address(0)) {
- return registrants[delegations[_validatorIndex]];
- }
-
- revert NotFound();
- }
-
- function getAllRegistrants() external view returns (Registrant[] memory) {
- uint256 activeCount = 0;
- for (uint256 i = 0; i < operators.length; i++) {
- if (isActiveOperator(operators[i])) {
- activeCount++;
- }
- }
-
- Registrant[] memory _registrants = new Registrant[](activeCount);
- uint256 index = 0;
- for (uint256 i = 0; i < operators.length; i++) {
- if (isActiveOperator(operators[i])) {
- _registrants[index] = registrants[operators[i]];
- index++;
- }
- }
-
- return _registrants;
- }
-}
diff --git a/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV1.sol b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV1.sol
new file mode 100644
index 000000000..a844b78c4
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV1.sol
@@ -0,0 +1,415 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
+import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
+import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+
+import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol";
+import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol";
+import {IVault} from "@symbiotic/interfaces/vault/IVault.sol";
+import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol";
+import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol";
+import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol";
+import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol";
+import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol";
+
+import {MapWithTimeData} from "../lib/MapWithTimeData.sol";
+import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol";
+import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol";
+import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol";
+import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol";
+
+/// @title Bolt Symbiotic Middleware
+/// @notice This contract is responsible for interfacing with the Symbiotic restaking protocol.
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltSymbioticMiddlewareV1 is IBoltMiddlewareV1, OwnableUpgradeable, UUPSUpgradeable {
+ using EnumerableSet for EnumerableSet.AddressSet;
+ using EnumerableMap for EnumerableMap.AddressToUintMap;
+ using MapWithTimeData for EnumerableMap.AddressToUintMap;
+ using Subnetwork for address;
+
+ // ========== CONSTANTS ============ //
+ /// @notice Slasher that can instantly slash operators without veto.
+ uint256 public INSTANT_SLASHER_TYPE = 0;
+
+ /// @notice Slasher that can request a veto before actually slashing operators.
+ uint256 public VETO_SLASHER_TYPE = 1;
+
+ // ========= STORAGE ========= //
+
+ /// @notice Start timestamp of the first epoch.
+ uint48 public START_TIMESTAMP;
+
+ /// @notice Bolt Parameters contract.
+ IBoltParametersV1 public parameters;
+
+ /// @notice Validators registry, where validators are registered via their
+ /// BLS pubkey and are assigned a sequence number.
+ IBoltManagerV1 public manager;
+
+ /// @notice Set of Symbiotic protocol vaults that are used in Bolt Protocol.
+ EnumerableMap.AddressToUintMap private vaults;
+
+ /// @notice Address of the Bolt network in Symbiotic Protocol.
+ address public BOLT_SYMBIOTIC_NETWORK;
+
+ /// @notice Address of the Symbiotic Operator Registry contract.
+ address public OPERATOR_REGISTRY;
+
+ /// @notice Address of the Symbiotic Vault Factory contract.
+ address public VAULT_FACTORY;
+
+ /// @notice Address of the Symbiotic Operator Network Opt-In contract.
+ address public OPERATOR_NET_OPTIN;
+
+ bytes32 public NAME_HASH;
+
+ // --> Storage layout marker: 12 slots
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[38] private __gap;
+
+ // ========= ERRORS =========
+
+ error NotVault();
+ error SlashAmountTooHigh();
+ error UnknownSlasherType();
+
+ // ========= CONSTRUCTOR =========
+
+ /// @notice Constructor for the BoltSymbioticMiddleware contract.
+ /// @param _parameters The address of the Bolt Parameters contract.
+ /// @param _manager The address of the Bolt Manager contract.
+ /// @param _symbioticNetwork The address of the Symbiotic network.
+ /// @param _symbioticOperatorRegistry The address of the Symbiotic operator registry.
+ /// @param _symbioticOperatorNetOptIn The address of the Symbiotic operator network opt-in contract.
+ /// @param _symbioticVaultFactory The address of the Symbiotic vault registry.
+ function initialize(
+ address _owner,
+ address _parameters,
+ address _manager,
+ address _symbioticNetwork,
+ address _symbioticOperatorRegistry,
+ address _symbioticOperatorNetOptIn,
+ address _symbioticVaultFactory
+ ) public initializer {
+ __Ownable_init(_owner);
+ parameters = IBoltParametersV1(_parameters);
+ manager = IBoltManagerV1(_manager);
+ START_TIMESTAMP = Time.timestamp();
+
+ BOLT_SYMBIOTIC_NETWORK = _symbioticNetwork;
+ OPERATOR_REGISTRY = _symbioticOperatorRegistry;
+ OPERATOR_NET_OPTIN = _symbioticOperatorNetOptIn;
+ VAULT_FACTORY = _symbioticVaultFactory;
+ NAME_HASH = keccak256("SYMBIOTIC");
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= VIEW FUNCTIONS =========
+
+ /// @notice Get the start timestamp of an epoch.
+ function getEpochStartTs(
+ uint48 epoch
+ ) public view returns (uint48 timestamp) {
+ return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the epoch at a given timestamp.
+ function getEpochAtTs(
+ uint48 timestamp
+ ) public view returns (uint48 epoch) {
+ return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION();
+ }
+
+ /// @notice Get the current epoch.
+ function getCurrentEpoch() public view returns (uint48 epoch) {
+ return getEpochAtTs(Time.timestamp());
+ }
+
+ /// @notice Get the whitelisted vaults.
+ function getWhitelistedVaults() public view returns (address[] memory) {
+ return vaults.keys();
+ }
+
+ // =========== ADMIN FUNCTIONS ============ //
+
+ /// @notice Allow a vault to signal opt-in to Bolt Protocol.
+ /// @param vault The vault address to signal opt-in for.
+ function registerVault(
+ address vault
+ ) public onlyOwner {
+ if (vaults.contains(vault)) {
+ revert AlreadyRegistered();
+ }
+
+ if (!IRegistry(VAULT_FACTORY).isEntity(vault)) {
+ revert NotVault();
+ }
+
+ // TODO: check slashing conditions and veto duration
+
+ vaults.add(vault);
+ vaults.enable(vault);
+ }
+
+ /// @notice Deregister a vault from working in Bolt Protocol.
+ /// @param vault The vault address to deregister.
+ function deregisterVault(
+ address vault
+ ) public onlyOwner {
+ if (!vaults.contains(vault)) {
+ revert NotRegistered();
+ }
+
+ vaults.remove(vault);
+ }
+
+ // ========= SYMBIOTIC MIDDLEWARE LOGIC =========
+
+ /// @notice Allow an operator to signal opt-in to Bolt Protocol.
+ /// msg.sender must be an operator in the Symbiotic network.
+ function registerOperator(
+ string calldata rpc
+ ) public {
+ if (manager.isOperator(msg.sender)) {
+ revert AlreadyRegistered();
+ }
+
+ if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) {
+ revert NotOperator();
+ }
+
+ if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(msg.sender, BOLT_SYMBIOTIC_NETWORK)) {
+ revert OperatorNotOptedIn();
+ }
+
+ manager.registerOperator(msg.sender, rpc);
+ }
+
+ /// @notice Deregister a Symbiotic operator from working in Bolt Protocol.
+ /// @dev This does NOT deregister the operator from the Symbiotic network.
+ function deregisterOperator() public {
+ if (!manager.isOperator(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ manager.deregisterOperator(msg.sender);
+ }
+
+ /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol.
+ /// @dev Pausing activity does not prevent the operator from being slashable for
+ /// the current network epoch until the end of the slashing window.
+ function pauseOperator() public {
+ manager.pauseOperator(msg.sender);
+ }
+
+ /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol.
+ function unpauseOperator() public {
+ manager.unpauseOperator(msg.sender);
+ }
+
+ /// @notice Allow a vault to signal indefinite opt-out from Bolt Protocol.
+ function pauseVault() public {
+ if (!vaults.contains(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ vaults.disable(msg.sender);
+ }
+
+ /// @notice Allow a disabled vault to signal opt-in to Bolt Protocol.
+ function unpauseVault() public {
+ if (!vaults.contains(msg.sender)) {
+ revert NotRegistered();
+ }
+
+ vaults.enable(msg.sender);
+ }
+
+ /// @notice Check if a vault is currently enabled to work in Bolt Protocol.
+ /// @param vault The vault address to check the enabled status for.
+ /// @return True if the vault is enabled, false otherwise.
+ function isVaultEnabled(
+ address vault
+ ) public view returns (bool) {
+ (uint48 enabledTime, uint48 disabledTime) = vaults.getTimes(vault);
+ return enabledTime != 0 && disabledTime == 0;
+ }
+
+ /// @notice Get the collaterals and amounts staked by an operator across the supported strategies.
+ ///
+ /// @param operator The operator address to get the collaterals and amounts staked for.
+ /// @return collaterals The collaterals staked by the operator.
+ /// @dev Assumes that the operator is registered and enabled.
+ function getOperatorCollaterals(
+ address operator
+ ) public view returns (address[] memory, uint256[] memory) {
+ address[] memory collateralTokens = new address[](vaults.length());
+ uint256[] memory amounts = new uint256[](vaults.length());
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp()));
+
+ for (uint256 i = 0; i < vaults.length(); ++i) {
+ (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i);
+
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ continue;
+ }
+
+ address collateral = IVault(vault).collateral();
+ collateralTokens[i] = collateral;
+
+ // in order to have stake in a network, the operator needs to be opted in to that vault.
+ // this authorization is fully handled in the Vault, we just need to read the stake.
+ amounts[i] = IBaseDelegator(IVault(vault).delegator()).stakeAt(
+ // The stake for each subnetwork is stored in the vault's delegator contract.
+ // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork)
+ // bytes(0) is for hints, which we don't currently use.
+ BOLT_SYMBIOTIC_NETWORK.subnetwork(0),
+ operator,
+ epochStartTs,
+ new bytes(0)
+ );
+ }
+
+ return (collateralTokens, amounts);
+ }
+
+ /// @notice Get the stake of an operator in Symbiotic protocol at the current timestamp.
+ /// @param operator The operator address to check the stake for.
+ /// @param collateral The collateral address to check the stake for.
+ /// @return amount The stake of the operator at the current timestamp, in collateral token.
+ function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) {
+ uint48 timestamp = Time.timestamp();
+ return getOperatorStakeAt(operator, collateral, timestamp);
+ }
+
+ /// @notice Get the stake of an operator in Symbiotic protocol at a given timestamp.
+ /// @param operator The operator address to check the stake for.
+ /// @param collateral The collateral address to check the stake for.
+ /// @param timestamp The timestamp to check the stake at.
+ /// @return amount The stake of the operator at the given timestamp, in collateral token.
+ function getOperatorStakeAt(
+ address operator,
+ address collateral,
+ uint48 timestamp
+ ) public view returns (uint256 amount) {
+ if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) {
+ revert InvalidQuery();
+ }
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp));
+
+ for (uint256 i = 0; i < vaults.length(); ++i) {
+ (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i);
+
+ if (collateral != IVault(vault).collateral()) {
+ continue;
+ }
+
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ continue;
+ }
+
+ // in order to have stake in a network, the operator needs to be opted in to that vault.
+ // this authorization is fully handled in the Vault, we just need to read the stake.
+ amount += IBaseDelegator(IVault(vault).delegator()).stakeAt(
+ // The stake for each subnetwork is stored in the vault's delegator contract.
+ // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork)
+ // bytes(0) is for hints, which we don't currently use.
+ BOLT_SYMBIOTIC_NETWORK.subnetwork(0),
+ operator,
+ epochStartTs,
+ new bytes(0)
+ );
+ }
+
+ return amount;
+ }
+
+ /// @notice Slash a given operator for a given amount of collateral.
+ /// @param timestamp The timestamp of the slash event.
+ /// @param operator The operator address to slash.
+ /// @param collateral The collateral address to slash.
+ /// @param amount The amount of collateral to slash.
+ function slash(uint48 timestamp, address operator, address collateral, uint256 amount) public onlyOwner {
+ // TODO: remove onlyOwner modifier and gate the slashing logic behind the BoltChallenger
+ // fault proof mechanism to allow for permissionless slashing.
+
+ uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp));
+
+ for (uint256 i = 0; i < vaults.length(); ++i) {
+ (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i);
+
+ if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) {
+ continue;
+ }
+
+ if (collateral != IVault(vault).collateral()) {
+ continue;
+ }
+
+ uint256 operatorStake = getOperatorStakeAt(operator, collateral, epochStartTs);
+
+ if (amount > operatorStake) {
+ revert SlashAmountTooHigh();
+ }
+
+ uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt(
+ BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, epochStartTs, new bytes(0)
+ );
+
+ // Slash the vault pro-rata.
+ _slashVault(epochStartTs, vault, operator, (amount * vaultStake) / operatorStake);
+ }
+ }
+
+ // ========= HELPER FUNCTIONS =========
+
+ /// @notice Check if a map entry was active at a given timestamp.
+ /// @param enabledTime The enabled time of the map entry.
+ /// @param disabledTime The disabled time of the map entry.
+ /// @param timestamp The timestamp to check the map entry status at.
+ /// @return True if the map entry was active at the given timestamp, false otherwise.
+ function _wasEnabledAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) {
+ return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp);
+ }
+
+ /// @notice Slash an operator for a given amount of collateral.
+ /// @param timestamp The timestamp of the slash event.
+ /// @param operator The operator address to slash.
+ /// @param amount The amount of collateral to slash.
+ function _slashVault(uint48 timestamp, address vault, address operator, uint256 amount) private {
+ address slasher = IVault(vault).slasher();
+ uint256 slasherType = IEntity(slasher).TYPE();
+
+ if (slasherType == INSTANT_SLASHER_TYPE) {
+ ISlasher(slasher).slash(BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0));
+ } else if (slasherType == VETO_SLASHER_TYPE) {
+ IVetoSlasher(slasher).requestSlash(
+ BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0)
+ );
+ } else {
+ revert UnknownSlasherType();
+ }
+ }
+}
diff --git a/bolt-contracts/src/contracts/BoltValidatorsV1.sol b/bolt-contracts/src/contracts/BoltValidatorsV1.sol
new file mode 100644
index 000000000..aa67dd5df
--- /dev/null
+++ b/bolt-contracts/src/contracts/BoltValidatorsV1.sol
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+
+import {BLS12381} from "../lib/bls/BLS12381.sol";
+import {BLSSignatureVerifier} from "../lib/bls/BLSSignatureVerifier.sol";
+import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol";
+import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol";
+
+/// @title Bolt Validators
+/// @notice This contract is responsible for registering validators and managing their configuration
+/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades
+/// with the use of storage gaps.
+/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit.
+/// You can also validate manually with forge: forge inspect storage-layout --pretty
+contract BoltValidatorsV1 is IBoltValidatorsV1, BLSSignatureVerifier, OwnableUpgradeable, UUPSUpgradeable {
+ using BLS12381 for BLS12381.G1Point;
+
+ // ========= STORAGE =========
+
+ /// @notice Bolt Parameters contract.
+ IBoltParametersV1 public parameters;
+
+ /// @notice Validators (aka Blockspace providers)
+ /// @dev For our purpose, validators are blockspace providers for commitments.
+ /// They are identified by their BLS pubkey hash.
+ ///
+ /// Validators can be separate from their Collateral Provider, such as in the
+ /// case of non-custodial staking pools. Validators can also delegate commitment
+ /// power to an Operator to make commitments on their behalf.
+ mapping(bytes32 => Validator) public VALIDATORS;
+
+ /// @notice Mapping from validator sequence number to validator pubkey hash
+ /// @dev This is used internally to easily query the pubkey hash of a validator.
+ mapping(uint64 => bytes32) private sequenceNumberToPubkeyHash;
+
+ /// @notice counter of the next index to be assigned to a validator.
+ /// @dev This incremental index is only used to identify validators in the registry.
+ /// It is not related to the `validatorIndex` assigned by the Beacon Chain.
+ uint64 internal nextValidatorSequenceNumber;
+
+ // --> Storage layout marker: 4 slots
+
+ /**
+ * @dev This empty reserved space is put in place to allow future versions to add new
+ * variables without shifting down storage in the inheritance chain.
+ * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+ * This can be validated with the Openzeppelin Foundry Upgrades toolkit.
+ *
+ * Total storage slots: 50
+ */
+ uint256[46] private __gap;
+
+ // ========= EVENTS =========
+
+ /// @notice Emitted when a validator is registered
+ /// @param pubkeyHash BLS public key hash of the validator
+ /// @param validator Validator struct
+ event ValidatorRegistered(bytes32 indexed pubkeyHash, Validator validator);
+
+ // ========= INITIALIZER =========
+
+ /// @notice Initializer
+ /// @param _owner Address of the owner of the contract
+ /// @param _parameters Address of the Bolt Parameters contract
+ function initialize(address _owner, address _parameters) public initializer {
+ __Ownable_init(_owner);
+
+ parameters = IBoltParametersV1(_parameters);
+ }
+
+ function _authorizeUpgrade(
+ address newImplementation
+ ) internal override onlyOwner {}
+
+ // ========= VIEW FUNCTIONS =========
+
+ /// @notice Get all validators
+ /// @dev This function should be used with caution as it can return a large amount of data.
+ /// @return Validator[] memory Array of validator structs
+ function getAllValidators() public view returns (Validator[] memory) {
+ uint256 validatorCount = nextValidatorSequenceNumber;
+ Validator[] memory validators = new Validator[](validatorCount);
+ for (uint256 i = 0; i < validatorCount; i++) {
+ bytes32 pubkeyHash = sequenceNumberToPubkeyHash[uint64(i)];
+ validators[i] = VALIDATORS[pubkeyHash];
+ }
+ return validators;
+ }
+
+ /// @notice Get a validator by its BLS public key
+ /// @param pubkey BLS public key of the validator
+ /// @return Validator memory Validator struct
+ function getValidatorByPubkey(
+ BLS12381.G1Point calldata pubkey
+ ) public view returns (Validator memory) {
+ return getValidatorByPubkeyHash(_pubkeyHash(pubkey));
+ }
+
+ /// @notice Get a validator by its BLS public key hash
+ /// @param pubkeyHash BLS public key hash of the validator
+ /// @return Validator memory Validator struct
+ function getValidatorByPubkeyHash(
+ bytes32 pubkeyHash
+ ) public view returns (Validator memory) {
+ Validator memory validator = VALIDATORS[pubkeyHash];
+ if (!validator.exists) {
+ revert ValidatorDoesNotExist();
+ }
+ return validator;
+ }
+
+ /// @notice Get a validator by its sequence number
+ /// @param sequenceNumber Sequence number of the validator
+ /// @return Validator memory Validator struct
+ function getValidatorBySequenceNumber(
+ uint64 sequenceNumber
+ ) public view returns (Validator memory) {
+ bytes32 pubkeyHash = sequenceNumberToPubkeyHash[sequenceNumber];
+ return VALIDATORS[pubkeyHash];
+ }
+
+ // ========= REGISTRATION LOGIC =========
+
+ /// @notice Register a single Validator and authorize a Collateral Provider and Operator for it
+ /// @dev This function allows anyone to register a single Validator. We do not perform any checks.
+ /// @param pubkey BLS public key for the Validator to be registered
+ /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations
+ /// @param authorizedOperator The address of the authorized operator
+ function registerValidatorUnsafe(
+ BLS12381.G1Point calldata pubkey,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) public {
+ if (!parameters.ALLOW_UNSAFE_REGISTRATION()) {
+ revert UnsafeRegistrationNotAllowed();
+ }
+
+ _registerValidator(pubkey, nextValidatorSequenceNumber, maxCommittedGasLimit, authorizedOperator);
+ }
+
+ /// @notice Register a single Validator and authorize a Collateral Provider and Operator for it
+ /// @dev This function allows anyone to register a single Validator. We perform an important check:
+ /// The owner of the Validator (controller) must have signed the message with its BLS private key.
+ ///
+ /// Message format: `chainId || controller || sequenceNumber`
+ /// @param pubkey BLS public key for the Validator to be registered
+ /// @param signature BLS signature of the registration message for the Validator
+ /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations
+ /// @param authorizedOperator The address of the authorized operator
+ function registerValidator(
+ BLS12381.G1Point calldata pubkey,
+ BLS12381.G2Point calldata signature,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) public {
+ bytes memory message = abi.encodePacked(block.chainid, msg.sender, nextValidatorSequenceNumber);
+ if (!_verifySignature(message, signature, pubkey)) {
+ revert InvalidBLSSignature();
+ }
+
+ _registerValidator(pubkey, nextValidatorSequenceNumber, maxCommittedGasLimit, authorizedOperator);
+ }
+
+ /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them
+ /// @dev This function allows anyone to register a list of Validators.
+ /// @param pubkeys List of BLS public keys for the Validators to be registered
+ /// @param signature BLS aggregated signature of the registration message for this batch of Validators
+ /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations
+ /// @param authorizedOperator The address of the authorized operator
+ function batchRegisterValidators(
+ BLS12381.G1Point[] calldata pubkeys,
+ BLS12381.G2Point calldata signature,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) public {
+ uint256 validatorsCount = pubkeys.length;
+ uint64[] memory expectedValidatorSequenceNumbers = new uint64[](validatorsCount);
+ for (uint256 i = 0; i < validatorsCount; i++) {
+ expectedValidatorSequenceNumbers[i] = nextValidatorSequenceNumber + uint64(i);
+ }
+
+ // Reconstruct the unique message for which we expect an aggregated signature.
+ // We need the msg.sender to prevent a front-running attack by an EOA that may
+ // try to register the same validators
+ bytes memory message = abi.encodePacked(block.chainid, msg.sender, expectedValidatorSequenceNumbers);
+
+ // Aggregate the pubkeys into a single pubkey to verify the aggregated signature once
+ BLS12381.G1Point memory aggPubkey = _aggregatePubkeys(pubkeys);
+
+ if (!_verifySignature(message, signature, aggPubkey)) {
+ revert InvalidBLSSignature();
+ }
+
+ // Register the validators and authorize the Collateral Provider and Operator for them
+ for (uint256 i = 0; i < validatorsCount; i++) {
+ _registerValidator(
+ pubkeys[i], expectedValidatorSequenceNumbers[i], maxCommittedGasLimit, authorizedOperator
+ );
+ }
+ }
+
+ /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them
+ /// @dev This function allows anyone to register a list of Validators.
+ /// @param pubkeys List of BLS public keys for the Validators to be registered
+ /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations
+ /// @param authorizedOperator The address of the authorized operator
+ function batchRegisterValidatorsUnsafe(
+ BLS12381.G1Point[] calldata pubkeys,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) public {
+ if (!parameters.ALLOW_UNSAFE_REGISTRATION()) {
+ revert UnsafeRegistrationNotAllowed();
+ }
+
+ uint256 validatorsCount = pubkeys.length;
+ uint64[] memory expectedValidatorSequenceNumbers = new uint64[](validatorsCount);
+ for (uint256 i = 0; i < validatorsCount; i++) {
+ expectedValidatorSequenceNumbers[i] = nextValidatorSequenceNumber + uint64(i);
+ }
+
+ // Register the validators and authorize the Collateral Provider and Operator for them
+ for (uint256 i = 0; i < validatorsCount; i++) {
+ _registerValidator(
+ pubkeys[i], expectedValidatorSequenceNumbers[i], maxCommittedGasLimit, authorizedOperator
+ );
+ }
+ }
+
+ // ========= UPDATE FUNCTIONS =========
+
+ /// @notice Update the maximum gas limit that a validator can commit for preconfirmations
+ /// @dev Only the `controller` of the validator can update this value.
+ /// @param pubkeyHash The hash of the BLS public key of the validator
+ /// @param maxCommittedGasLimit The new maximum gas limit
+ function updateMaxCommittedGasLimit(bytes32 pubkeyHash, uint128 maxCommittedGasLimit) public {
+ Validator storage validator = VALIDATORS[pubkeyHash];
+
+ if (!validator.exists) {
+ revert ValidatorDoesNotExist();
+ }
+
+ if (msg.sender != validator.controller) {
+ revert UnauthorizedCaller();
+ }
+
+ validator.maxCommittedGasLimit = maxCommittedGasLimit;
+ }
+
+ // ========= HELPERS =========
+
+ /// @notice Internal helper to add a validator to the registry
+ /// @param pubkey BLS public key of the validator
+ /// @param sequenceNumber Sequence number of the validator
+ /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations
+ /// @param authorizedOperator Address of the authorized operator
+ function _registerValidator(
+ BLS12381.G1Point calldata pubkey,
+ uint64 sequenceNumber,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) internal {
+ if (authorizedOperator == address(0)) {
+ revert InvalidAuthorizedOperator();
+ }
+
+ bytes32 pubKeyHash = _pubkeyHash(pubkey);
+
+ // check if the validator already exists
+ if (VALIDATORS[pubKeyHash].exists) {
+ revert ValidatorAlreadyExists();
+ }
+
+ Validator memory newValidator = Validator({
+ sequenceNumber: sequenceNumber,
+ maxCommittedGasLimit: maxCommittedGasLimit,
+ authorizedOperator: authorizedOperator,
+ controller: msg.sender,
+ exists: true
+ });
+
+ // register the validator
+ VALIDATORS[pubKeyHash] = newValidator;
+ emit ValidatorRegistered(pubKeyHash, newValidator);
+
+ sequenceNumberToPubkeyHash[sequenceNumber] = pubKeyHash;
+ nextValidatorSequenceNumber += 1;
+ }
+
+ /// @notice Compute the hash of a BLS public key
+ /// @param pubkey BLS public key
+ /// @return Hash of the public key in compressed form
+ function _pubkeyHash(
+ BLS12381.G1Point memory pubkey
+ ) internal pure returns (bytes32) {
+ uint256[2] memory compressedPubKey = pubkey.compress();
+ return keccak256(abi.encodePacked(compressedPubKey));
+ }
+}
diff --git a/bolt-contracts/src/interfaces/IBoltChallenger.sol b/bolt-contracts/src/interfaces/IBoltChallenger.sol
deleted file mode 100644
index aba98cc89..000000000
--- a/bolt-contracts/src/interfaces/IBoltChallenger.sol
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-interface IBoltChallenger {
- enum ChallengeStatus {
- // The challenge is open and waiting for a resolution
- Pending,
- // The challenge has been resolved
- Resolved
- }
-
- enum ChallengeResult {
- // The challenge was successful: the proposer failed to honor the preconfirmation
- Success,
- // The challenge was unsuccessful: the proposer honored the preconfirmation
- Failure
- }
-
- // Bolt challenge errors
- error ChallengeAlreadyExists();
- error InsufficientBond();
- error InvalidProposerAddress();
- error Unauthorized();
- error InvalidChallenge();
- error ChallengeNotFound();
- error ChallengeAlreadyResolved();
- error TargetSlotTooFarInThePast();
- error InvalidCommitmentSignature();
- error InvalidCommitmentSigner();
-
- // Relic related errors
- error UnexpectedFactSignature();
- error WrongBlockHeader();
-
- /// @notice Event emitted when a new challenge is opened
- event NewChallenge(address indexed basedProposer, bytes32 indexed commitmentID, uint256 targetSlot);
-
- /// @notice Event emitted when a challenge is resolved
- event ChallengeResolved(bytes32 indexed challengeID, ChallengeResult result);
-}
diff --git a/bolt-contracts/src/interfaces/IBoltChallengerV1.sol b/bolt-contracts/src/interfaces/IBoltChallengerV1.sol
new file mode 100644
index 000000000..e24f3e188
--- /dev/null
+++ b/bolt-contracts/src/interfaces/IBoltChallengerV1.sol
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+interface IBoltChallengerV1 {
+ enum ChallengeStatus {
+ Open,
+ Defended,
+ Breached
+ }
+
+ struct Challenge {
+ bytes32 id;
+ uint48 openedAt;
+ ChallengeStatus status;
+ uint256 targetSlot;
+ address challenger;
+ address commitmentSigner;
+ address commitmentReceiver;
+ TransactionData[] committedTxs;
+ }
+
+ struct SignedCommitment {
+ uint64 slot;
+ bytes signature;
+ bytes signedTx;
+ }
+
+ struct TransactionData {
+ bytes32 txHash;
+ uint256 nonce;
+ uint256 gasLimit;
+ }
+
+ struct BlockHeaderData {
+ bytes32 parentHash;
+ bytes32 stateRoot;
+ bytes32 txRoot;
+ uint256 blockNumber;
+ uint256 timestamp;
+ uint256 baseFee;
+ }
+
+ struct AccountData {
+ uint256 nonce;
+ uint256 balance;
+ }
+
+ struct Proof {
+ // 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;
+ // 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();
+ error BlockIsNotFinalized();
+ error IncorrectChallengeBond();
+ error ChallengeAlreadyExists();
+ error ChallengeAlreadyResolved();
+ error ChallengeDoesNotExist();
+ error BlockIsTooOld();
+ error InvalidBlockHash();
+ error InvalidParentBlockHash();
+ error AccountDoesNotExist();
+ error TransactionNotIncluded();
+ error WrongTransactionHashProof();
+ error InvalidBlockNumber();
+ error BondTransferFailed();
+ error ChallengeNotExpired();
+ error ChallengeExpired();
+ error EmptyCommitments();
+ error UnexpectedMixedSenders();
+ error UnexpectedMixedSlots();
+ error UnexpectedMixedSigners();
+ error UnexpectedNonceOrder();
+ error InvalidProofsLength();
+ error BeaconRootNotFound();
+
+ event ChallengeOpened(bytes32 indexed challengeId, address indexed challenger, address indexed commitmentSigner);
+ event ChallengeDefended(bytes32 indexed challengeId);
+ event ChallengeBreached(bytes32 indexed challengeId);
+
+ function getAllChallenges() external view returns (Challenge[] memory);
+
+ function getOpenChallenges() external view returns (Challenge[] memory);
+
+ function getChallengeByID(
+ bytes32 challengeID
+ ) external view returns (Challenge memory);
+
+ function openChallenge(
+ SignedCommitment[] calldata commitments
+ ) external payable;
+
+ function resolveExpiredChallenge(
+ bytes32 challengeID
+ ) external;
+
+ function resolveOpenChallenge(bytes32 challengeID, Proof calldata proof) external;
+}
diff --git a/bolt-contracts/src/interfaces/IBoltManagerV1.sol b/bolt-contracts/src/interfaces/IBoltManagerV1.sol
new file mode 100644
index 000000000..cebb303fa
--- /dev/null
+++ b/bolt-contracts/src/interfaces/IBoltManagerV1.sol
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {IBoltValidatorsV1} from "./IBoltValidatorsV1.sol";
+
+interface IBoltManagerV1 {
+ error InvalidQuery();
+ error OperatorAlreadyRegistered();
+ error OperatorNotRegistered();
+ error UnauthorizedMiddleware();
+ error InactiveOperator();
+
+ struct Operator {
+ string rpc;
+ address middleware;
+ uint256 timestamp;
+ }
+
+ function registerOperator(address operator, string calldata rpc) external;
+
+ function deregisterOperator(
+ address operator
+ ) external;
+
+ function pauseOperator(
+ address operator
+ ) external;
+
+ function unpauseOperator(
+ address operator
+ ) external;
+
+ function isOperator(
+ address operator
+ ) external view returns (bool);
+
+ function validators() external view returns (IBoltValidatorsV1);
+
+ function getProposerStatus(
+ bytes32 pubkeyHash
+ ) external view returns (IBoltValidatorsV1.ProposerStatus memory status);
+
+ function getProposerStatuses(
+ bytes32[] calldata pubkeyHashes
+ ) external view returns (IBoltValidatorsV1.ProposerStatus[] memory statuses);
+
+ function isOperatorAuthorizedForValidator(address operator, bytes32 pubkeyHash) external view returns (bool);
+
+ function getSupportedRestakingProtocols() external view returns (address[] memory middlewares);
+}
diff --git a/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol b/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol
new file mode 100644
index 000000000..f48885ffa
--- /dev/null
+++ b/bolt-contracts/src/interfaces/IBoltMiddlewareV1.sol
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {BLS12381} from "../lib/bls/BLS12381.sol";
+import {IBoltValidatorsV1} from "./IBoltValidatorsV1.sol";
+
+interface IBoltMiddlewareV1 {
+ error InvalidQuery();
+ error AlreadyRegistered();
+ error NotRegistered();
+ error OperatorNotOptedIn();
+ error NotOperator();
+ error NotAllowed();
+
+ function NAME_HASH() external view returns (bytes32);
+
+ function getEpochStartTs(
+ uint48 epoch
+ ) external view returns (uint48);
+
+ function getEpochAtTs(
+ uint48 timestamp
+ ) external view returns (uint48);
+
+ function getCurrentEpoch() external view returns (uint48);
+
+ function getOperatorStake(address operator, address collateral) external view returns (uint256);
+
+ function getOperatorCollaterals(
+ address operator
+ ) external view returns (address[] memory, uint256[] memory);
+
+ function getOperatorStakeAt(
+ address operator,
+ address collateral,
+ uint48 timestamp
+ ) external view returns (uint256);
+}
diff --git a/bolt-contracts/src/interfaces/IBoltParametersV1.sol b/bolt-contracts/src/interfaces/IBoltParametersV1.sol
new file mode 100644
index 000000000..f9a3c7a73
--- /dev/null
+++ b/bolt-contracts/src/interfaces/IBoltParametersV1.sol
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+interface IBoltParametersV1 {
+ function EPOCH_DURATION() external view returns (uint48);
+ function SLASHING_WINDOW() external view returns (uint48);
+ function ALLOW_UNSAFE_REGISTRATION() external view returns (bool);
+ function MAX_CHALLENGE_DURATION() external view returns (uint48);
+ function CHALLENGE_BOND() external view returns (uint256);
+ function BLOCKHASH_EVM_LOOKBACK() external view returns (uint256);
+ function JUSTIFICATION_DELAY() external view returns (uint256);
+ function EIP4788_WINDOW() external view returns (uint256);
+ function SLOT_TIME() external view returns (uint256);
+ function ETH2_GENESIS_TIMESTAMP() external view returns (uint256);
+ function BEACON_ROOTS_CONTRACT() external view returns (address);
+ function MINIMUM_OPERATOR_STAKE() external view returns (uint256);
+}
diff --git a/bolt-contracts/src/interfaces/IBoltRegistry.sol b/bolt-contracts/src/interfaces/IBoltRegistry.sol
deleted file mode 100644
index 283f8f505..000000000
--- a/bolt-contracts/src/interfaces/IBoltRegistry.sol
+++ /dev/null
@@ -1,69 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-interface IBoltRegistry {
- /// @notice Struct to hold opted-in proposer information
- struct Registrant {
- // The address of the operator
- address operator;
- // The validator indexes this registrant is responsible for
- uint64[] validatorIndexes;
- uint256 enteredAt;
- uint256 exitInitiatedAt;
- uint256 balance;
- Status status;
- MetaData metadata;
- }
-
- struct MetaData {
- string rpc;
- bytes extra;
- }
-
- /// @notice Enum to hold the status of the based proposers
- enum Status {
- // Default INACTIVE
- INACTIVE,
- ACTIVE,
- FROZEN,
- EXITING
- }
-
- // Error messages
- error AlreadyOptedIn();
- error InsufficientCollateral();
- error BasedProposerDoesNotExist();
- error InvalidStatusChange();
- error CooldownNotElapsed();
- error Unauthorized();
- error NotFound();
-
- /// @notice Event to log the status change of a based proposer
- event StatusChange(address indexed operator, Status status);
-
- event Registered(
- address indexed operator,
- uint64[] validatorIndexes,
- MetaData metadata
- );
-
- function register(
- uint64[] calldata validatorIndexes,
- string calldata rpc,
- bytes calldata extra
- ) external payable;
-
- function isActiveOperator(address _operator) external view returns (bool);
-
- function getOperatorStatus(
- address _operator
- ) external view returns (Status);
-
- function getOperatorForValidator(
- uint64 _validatorIndex
- ) external view returns (Registrant memory);
-
- function startExit() external;
-
- function confirmExit(address payable recipient) external;
-}
diff --git a/bolt-contracts/src/interfaces/IBoltValidatorsV1.sol b/bolt-contracts/src/interfaces/IBoltValidatorsV1.sol
new file mode 100644
index 000000000..af5e32a00
--- /dev/null
+++ b/bolt-contracts/src/interfaces/IBoltValidatorsV1.sol
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {BLS12381} from "../lib/bls/BLS12381.sol";
+
+interface IBoltValidatorsV1 {
+ /// @notice Validator info
+ struct Validator {
+ // whether the validator exists in the registry
+ bool exists;
+ // the incremental sequence number assigned to the validator
+ uint64 sequenceNumber;
+ // the maximum amount of gas that the validator can consume with preconfirmations
+ // in a single slot. Operators must respect this limit when making commitments.
+ uint128 maxCommittedGasLimit;
+ // the entity authorized to make commitments on behalf of the validator
+ address authorizedOperator;
+ // the EOA that registered the validator and can update its configuration
+ address controller;
+ }
+
+ /// @notice Proposer status info.
+ struct ProposerStatus {
+ // The pubkey hash of the validator.
+ bytes32 pubkeyHash;
+ // Whether the corresponding operator is active based on collateral requirements.
+ bool active;
+ // The operator address that is authorized to make & sign commitments on behalf of the validator.
+ address operator;
+ // The operator RPC endpoint.
+ string operatorRPC;
+ // The addresses of the collateral tokens.
+ address[] collaterals;
+ // The corresponding amounts of the collateral tokens.
+ uint256[] amounts;
+ }
+
+ error InvalidBLSSignature();
+ error InvalidAuthorizedCollateralProvider();
+ error InvalidAuthorizedOperator();
+ error ValidatorAlreadyExists();
+ error ValidatorDoesNotExist();
+ error UnsafeRegistrationNotAllowed();
+ error UnauthorizedCaller();
+
+ function getAllValidators() external view returns (Validator[] memory);
+
+ function getValidatorByPubkey(
+ BLS12381.G1Point calldata pubkey
+ ) external view returns (Validator memory);
+
+ function getValidatorByPubkeyHash(
+ bytes32 pubkeyHash
+ ) external view returns (Validator memory);
+
+ function getValidatorBySequenceNumber(
+ uint64 sequenceNumber
+ ) external view returns (Validator memory);
+
+ function registerValidatorUnsafe(
+ BLS12381.G1Point calldata pubkey,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) external;
+
+ function registerValidator(
+ BLS12381.G1Point calldata pubkey,
+ BLS12381.G2Point calldata signature,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) external;
+
+ function batchRegisterValidators(
+ BLS12381.G1Point[] calldata pubkeys,
+ BLS12381.G2Point calldata signature,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) external;
+
+ function batchRegisterValidatorsUnsafe(
+ BLS12381.G1Point[] calldata pubkeys,
+ uint128 maxCommittedGasLimit,
+ address authorizedOperator
+ ) external;
+
+ function updateMaxCommittedGasLimit(bytes32 pubkeyHash, uint128 maxCommittedGasLimit) external;
+}
diff --git a/bolt-contracts/src/lib/BeaconChainUtils.sol b/bolt-contracts/src/lib/BeaconChainUtils.sol
deleted file mode 100644
index 3664e3ce5..000000000
--- a/bolt-contracts/src/lib/BeaconChainUtils.sol
+++ /dev/null
@@ -1,46 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-library BeaconChainUtils {
- /// @notice The address of the BeaconRoots contract
- /// @dev See EIP-4788 for more info
- address internal constant BEACON_ROOTS_CONTRACT = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
-
- /// @notice The duration of a slot in seconds
- uint256 internal constant SLOT_TIME = 12;
-
- /// @notice The timestamp of the genesis of the eth2 chain
- uint256 internal constant ETH2_GENESIS_TIMESTAMP = 1606824023;
-
- /// @notice Error emitted when a beacon block root is not found
- error BeaconRootNotFound();
-
- /// @notice Get the slot number from a given timestamp
- /// @param _timestamp The timestamp
- /// @return The slot number
- function _getSlotFromTimestamp(uint256 _timestamp) internal pure returns (uint256) {
- return (_timestamp - ETH2_GENESIS_TIMESTAMP) / SLOT_TIME;
- }
-
- /// @notice Get the timestamp from a given slot
- /// @param _slot The slot number
- /// @return The timestamp
- function _getTimestampFromSlot(uint256 _slot) internal pure returns (uint256) {
- return ETH2_GENESIS_TIMESTAMP + _slot * SLOT_TIME;
- }
-
- /// @notice Get the beacon block root for a given slot
- /// @param _slot The slot number
- /// @return The beacon block root
- function _getBeaconBlockRoot(uint256 _slot) internal view returns (bytes32) {
- uint256 slotTimestamp = ETH2_GENESIS_TIMESTAMP + _slot * SLOT_TIME;
-
- (bool success, bytes memory data) = BEACON_ROOTS_CONTRACT.staticcall(abi.encode(slotTimestamp));
-
- if (!success || data.length == 0) {
- revert BeaconRootNotFound();
- }
-
- return abi.decode(data, (bytes32));
- }
-}
diff --git a/bolt-contracts/src/lib/BytesUtils.sol b/bolt-contracts/src/lib/BytesUtils.sol
new file mode 100644
index 000000000..036640ad7
--- /dev/null
+++ b/bolt-contracts/src/lib/BytesUtils.sol
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+// Credits: Optimism contributors.
+// Ref: https://github.com/ethereum-optimism/optimism/blob/05deae54595b0e6bdd33580de81cb9ad194898bc/packages/contracts-bedrock/src/libraries/Bytes.sol
+
+/**
+ * @title BytesUtils
+ */
+library BytesUtils {
+ /**
+ *
+ * Internal Functions *
+ *
+ */
+ function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) {
+ unchecked {
+ require(_length + 31 >= _length, "slice_overflow");
+ require(_start + _length >= _start, "slice_overflow");
+ require(_bytes.length >= _start + _length, "slice_outOfBounds");
+
+ bytes memory tempBytes;
+
+ assembly {
+ switch iszero(_length)
+ case 0 {
+ // Get a location of some free memory and store it in tempBytes as
+ // Solidity does for memory variables.
+ tempBytes := mload(0x40)
+
+ // The first word of the slice result is potentially a partial
+ // word read from the original array. To read it, we calculate
+ // the length of that partial word and start copying that many
+ // bytes into the array. The first word we copy will start with
+ // data we don't care about, but the last `lengthmod` bytes will
+ // land at the beginning of the contents of the new array. When
+ // we're done copying, we overwrite the full first word with
+ // the actual length of the slice.
+ let lengthmod := and(_length, 31)
+
+ // The multiplication in the next line is necessary
+ // because when slicing multiples of 32 bytes (lengthmod == 0)
+ // the following copy loop was copying the origin's length
+ // and then ending prematurely not copying everything it should.
+ let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
+ let end := add(mc, _length)
+
+ for {
+ // The multiplication in the next line has the same exact purpose
+ // as the one above.
+ let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
+ } lt(mc, end) {
+ mc := add(mc, 0x20)
+ cc := add(cc, 0x20)
+ } { mstore(mc, mload(cc)) }
+
+ mstore(tempBytes, _length)
+
+ //update free-memory pointer
+ //allocating the array padded to 32 bytes like the compiler does now
+ mstore(0x40, and(add(mc, 31), not(31)))
+ }
+ //if we want a zero-length slice let's just return a zero-length array
+ default {
+ tempBytes := mload(0x40)
+
+ //zero out the 32 bytes slice we are about to return
+ //we need to do it because Solidity does not garbage collect
+ mstore(tempBytes, 0)
+
+ mstore(0x40, add(tempBytes, 0x20))
+ }
+ }
+
+ return tempBytes;
+ }
+ }
+
+ function slice(bytes memory _bytes, uint256 _start) internal pure returns (bytes memory) {
+ unchecked {
+ if (_bytes.length - _start == 0) {
+ return bytes("");
+ }
+
+ return slice(_bytes, _start, _bytes.length - _start);
+ }
+ }
+
+ function toBytes32PadLeft(
+ bytes memory _bytes
+ ) internal pure returns (bytes32) {
+ unchecked {
+ bytes32 ret;
+ uint256 len = _bytes.length <= 32 ? _bytes.length : 32;
+ assembly {
+ ret := shr(mul(sub(32, len), 8), mload(add(_bytes, 32)))
+ }
+ return ret;
+ }
+ }
+
+ function toBytes32(
+ bytes memory _bytes
+ ) internal pure returns (bytes32) {
+ unchecked {
+ if (_bytes.length < 32) {
+ bytes32 ret;
+ assembly {
+ ret := mload(add(_bytes, 32))
+ }
+ return ret;
+ }
+
+ return abi.decode(_bytes, (bytes32)); // will truncate if input length > 32 bytes
+ }
+ }
+
+ function toUint256(
+ bytes memory _bytes
+ ) internal pure returns (uint256) {
+ return uint256(toBytes32(_bytes));
+ }
+
+ function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
+ require(_start + 3 >= _start, "toUint24_overflow");
+ require(_bytes.length >= _start + 3, "toUint24_outOfBounds");
+ uint24 tempUint;
+
+ assembly {
+ tempUint := mload(add(add(_bytes, 0x3), _start))
+ }
+
+ return tempUint;
+ }
+
+ function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
+ require(_start + 1 >= _start, "toUint8_overflow");
+ require(_bytes.length >= _start + 1, "toUint8_outOfBounds");
+ uint8 tempUint;
+
+ assembly {
+ tempUint := mload(add(add(_bytes, 0x1), _start))
+ }
+
+ return tempUint;
+ }
+
+ function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
+ require(_start + 20 >= _start, "toAddress_overflow");
+ require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
+ address tempAddress;
+
+ assembly {
+ tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
+ }
+
+ return tempAddress;
+ }
+
+ function toNibbles(
+ bytes memory _bytes
+ ) internal pure returns (bytes memory) {
+ unchecked {
+ bytes memory nibbles = new bytes(_bytes.length * 2);
+
+ for (uint256 i = 0; i < _bytes.length; i++) {
+ nibbles[i * 2] = _bytes[i] >> 4;
+ nibbles[i * 2 + 1] = bytes1(uint8(_bytes[i]) % 16);
+ }
+
+ return nibbles;
+ }
+ }
+
+ function fromNibbles(
+ bytes memory _bytes
+ ) internal pure returns (bytes memory) {
+ unchecked {
+ bytes memory ret = new bytes(_bytes.length / 2);
+
+ for (uint256 i = 0; i < ret.length; i++) {
+ ret[i] = (_bytes[i * 2] << 4) | (_bytes[i * 2 + 1]);
+ }
+
+ return ret;
+ }
+ }
+
+ function equal(bytes memory _bytes, bytes memory _other) internal pure returns (bool) {
+ return keccak256(_bytes) == keccak256(_other);
+ }
+}
diff --git a/bolt-contracts/src/lib/Config.sol b/bolt-contracts/src/lib/Config.sol
new file mode 100644
index 000000000..a65c80e61
--- /dev/null
+++ b/bolt-contracts/src/lib/Config.sol
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+library BoltConfig {
+ struct Parameters {
+ uint48 epochDuration;
+ uint48 slashingWindow;
+ uint48 maxChallengeDuration;
+ uint256 challengeBond;
+ uint256 blockhashEvmLookback;
+ uint256 justificationDelay;
+ uint256 eth2GenesisTimestamp;
+ uint256 slotTime;
+ bool allowUnsafeRegistration;
+ uint256 minimumOperatorStake;
+ }
+
+ struct Deployments {
+ address symbioticNetwork;
+ address symbioticOperatorRegistry;
+ address symbioticOperatorNetOptIn;
+ address symbioticVaultFactory;
+ address[] supportedVaults;
+ address eigenLayerAVSDirectory;
+ address eigenLayerDelegationManager;
+ address eigenLayerStrategyManager;
+ address[] supportedStrategies;
+ }
+
+ struct SymbioticDeployments {
+ address symbioticNetworkRegistry;
+ address symbioticNetworkMiddlewareService;
+ }
+}
diff --git a/bolt-contracts/src/lib/EnumerableMap.sol b/bolt-contracts/src/lib/EnumerableMap.sol
new file mode 100644
index 000000000..af366efe5
--- /dev/null
+++ b/bolt-contracts/src/lib/EnumerableMap.sol
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+
+import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol";
+
+library EnumerableMap {
+ using EnumerableSet for EnumerableSet.Bytes32Set;
+
+ error KeyNotFound();
+
+ struct OperatorMap {
+ // Storage of keys
+ EnumerableSet.Bytes32Set _keys;
+ mapping(bytes32 key => IBoltManagerV1.Operator) _values;
+ }
+
+ function set(OperatorMap storage self, address key, IBoltManagerV1.Operator memory value) internal returns (bool) {
+ bytes32 keyBytes = bytes32(uint256(uint160(key)));
+ self._values[keyBytes] = value;
+ return self._keys.add(keyBytes);
+ }
+
+ function remove(OperatorMap storage self, address key) internal returns (bool) {
+ bytes32 keyBytes = bytes32(uint256(uint160(key)));
+ delete self._values[keyBytes];
+ return self._keys.remove(keyBytes);
+ }
+
+ function contains(OperatorMap storage self, address key) internal view returns (bool) {
+ return self._keys.contains(bytes32(uint256(uint160(key))));
+ }
+
+ function length(
+ OperatorMap storage self
+ ) internal view returns (uint256) {
+ return self._keys.length();
+ }
+
+ function at(
+ OperatorMap storage self,
+ uint256 index
+ ) internal view returns (address, IBoltManagerV1.Operator memory) {
+ bytes32 key = self._keys.at(index);
+ return (address(uint160(uint256(key))), self._values[key]);
+ }
+
+ function get(OperatorMap storage self, address key) internal view returns (IBoltManagerV1.Operator memory) {
+ if (!contains(self, key)) {
+ revert KeyNotFound();
+ }
+
+ return self._values[bytes32(uint256(uint160(key)))];
+ }
+
+ function keys(
+ OperatorMap storage self
+ ) internal view returns (address[] memory) {
+ address[] memory result = new address[](self._keys.length());
+ for (uint256 i = 0; i < self._keys.length(); i++) {
+ result[i] = address(uint160(uint256(self._keys.at(i))));
+ }
+
+ return result;
+ }
+}
diff --git a/bolt-contracts/src/lib/MapWithTimeData.sol b/bolt-contracts/src/lib/MapWithTimeData.sol
new file mode 100644
index 000000000..fa0a10afe
--- /dev/null
+++ b/bolt-contracts/src/lib/MapWithTimeData.sol
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+// Credits: Symbiotic contributors.
+// Ref: https://github.com/symbioticfi/cosmos-sdk/blob/c25b6d5f320eb8ea4189584fa04d28c47362c2a7/middleware/src/libraries/MapWithTimeData.sol
+
+import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
+import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
+import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
+
+library MapWithTimeData {
+ using EnumerableMap for EnumerableMap.AddressToUintMap;
+
+ error AlreadyAdded();
+ error NotEnabled();
+ error AlreadyEnabled();
+
+ uint256 private constant ENABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF;
+ uint256 private constant DISABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF << 48;
+
+ function add(EnumerableMap.AddressToUintMap storage self, address addr) internal {
+ if (!self.set(addr, uint256(0))) {
+ revert AlreadyAdded();
+ }
+ }
+
+ function disable(EnumerableMap.AddressToUintMap storage self, address addr) internal {
+ uint256 value = self.get(addr);
+
+ if (uint48(value) == 0 || uint48(value >> 48) != 0) {
+ revert NotEnabled();
+ }
+
+ value |= uint256(Time.timestamp()) << 48;
+ self.set(addr, value);
+ }
+
+ function enable(EnumerableMap.AddressToUintMap storage self, address addr) internal {
+ uint256 value = self.get(addr);
+
+ if (uint48(value) != 0 && uint48(value >> 48) == 0) {
+ revert AlreadyEnabled();
+ }
+
+ value = uint256(Time.timestamp());
+ self.set(addr, value);
+ }
+
+ function atWithTimes(
+ EnumerableMap.AddressToUintMap storage self,
+ uint256 idx
+ ) internal view returns (address key, uint48 enabledTime, uint48 disabledTime) {
+ uint256 value;
+ (key, value) = self.at(idx);
+ enabledTime = uint48(value);
+ disabledTime = uint48(value >> 48);
+ }
+
+ function getTimes(
+ EnumerableMap.AddressToUintMap storage self,
+ address addr
+ ) internal view returns (uint48 enabledTime, uint48 disabledTime) {
+ uint256 value = self.get(addr);
+ enabledTime = uint48(value);
+ disabledTime = uint48(value >> 48);
+ }
+}
diff --git a/bolt-contracts/src/lib/OperatorMapWithTime.sol b/bolt-contracts/src/lib/OperatorMapWithTime.sol
new file mode 100644
index 000000000..0b5f7ab78
--- /dev/null
+++ b/bolt-contracts/src/lib/OperatorMapWithTime.sol
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
+import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
+
+import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol";
+import {EnumerableMap} from "./EnumerableMap.sol";
+
+library OperatorMapWithTime {
+ using EnumerableMap for EnumerableMap.OperatorMap;
+
+ error AlreadyAdded();
+ error NotEnabled();
+ error AlreadyEnabled();
+
+ uint256 private constant ENABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF;
+ uint256 private constant DISABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF << 48;
+
+ function add(EnumerableMap.OperatorMap storage self, address addr) internal {
+ if (!self.set(addr, IBoltManagerV1.Operator("", address(0), 0))) {
+ revert AlreadyAdded();
+ }
+ }
+
+ function disable(EnumerableMap.OperatorMap storage self, address addr) internal {
+ IBoltManagerV1.Operator memory operator = self.get(addr);
+ uint256 value = operator.timestamp;
+
+ if (uint48(value) == 0 || uint48(value >> 48) != 0) {
+ revert NotEnabled();
+ }
+
+ value |= uint256(Time.timestamp()) << 48;
+ operator.timestamp = value;
+ self.set(addr, operator);
+ }
+
+ function enable(EnumerableMap.OperatorMap storage self, address addr) internal {
+ IBoltManagerV1.Operator memory operator = self.get(addr);
+ uint256 value = operator.timestamp;
+
+ if (uint48(value) != 0 && uint48(value >> 48) == 0) {
+ revert AlreadyEnabled();
+ }
+
+ value = uint256(Time.timestamp());
+ operator.timestamp = value;
+ self.set(addr, operator);
+ }
+
+ function atWithTimes(
+ EnumerableMap.OperatorMap storage self,
+ uint256 idx
+ ) internal view returns (address key, uint48 enabledTime, uint48 disabledTime) {
+ IBoltManagerV1.Operator memory value;
+ (key, value) = self.at(idx);
+ uint256 timestamp = value.timestamp;
+ enabledTime = uint48(timestamp);
+ disabledTime = uint48(timestamp >> 48);
+ }
+
+ function getTimes(
+ EnumerableMap.OperatorMap storage self,
+ address addr
+ ) internal view returns (uint48 enabledTime, uint48 disabledTime) {
+ IBoltManagerV1.Operator memory value = self.get(addr);
+ enabledTime = uint48(value.timestamp);
+ disabledTime = uint48(value.timestamp >> 48);
+ }
+}
diff --git a/bolt-contracts/src/lib/TransactionDecoder.sol b/bolt-contracts/src/lib/TransactionDecoder.sol
new file mode 100644
index 000000000..3bd7cfc66
--- /dev/null
+++ b/bolt-contracts/src/lib/TransactionDecoder.sol
@@ -0,0 +1,451 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
+
+import {RLPReader} from "./rlp/RLPReader.sol";
+import {RLPWriter} from "./rlp/RLPWriter.sol";
+import {BytesUtils} from "./BytesUtils.sol";
+
+/// @title TransactionDecoder
+/// @notice A library to decode Ethereum transactions in EIP-2718 format
+/// into a structured transaction object, and to recover the sender.
+library TransactionDecoder {
+ using BytesUtils for bytes;
+ using RLPReader for bytes;
+ using RLPReader for RLPReader.RLPItem;
+
+ /// @notice The type of transaction
+ enum TxType {
+ Legacy,
+ Eip2930,
+ Eip1559,
+ Eip4844
+ }
+
+ /// @notice A decoded transaction object
+ struct Transaction {
+ TxType txType;
+ uint64 chainId;
+ // As Solidity doesn't have NULL values, we use this flag to differentiate between
+ // an explicit chainId of 0 and a missing chainID. The difference is important for
+ // encoding unsigned eip-155 legacy transactions.
+ bool isChainIdSet;
+ uint256 nonce;
+ uint256 gasPrice;
+ uint256 maxPriorityFeePerGas;
+ uint256 maxFeePerGas;
+ uint256 gasLimit;
+ address to;
+ uint256 value;
+ bytes data;
+ bytes[] accessList;
+ uint256 maxFeePerBlobGas;
+ bytes32[] blobVersionedHashes;
+ bytes sig;
+ uint64 legacyV;
+ }
+
+ error NoSignature();
+ error InvalidYParity();
+ error UnsupportedTxType();
+ error InvalidFieldCount();
+ error InvalidSignatureLength();
+
+ /// @notice Decode a raw transaction into a transaction object
+ /// @param raw The raw transaction bytes
+ /// @return transaction The decoded transaction object
+ function decodeEnveloped(
+ bytes memory raw
+ ) internal pure returns (Transaction memory transaction) {
+ bytes1 prefix = raw[0];
+
+ if (prefix >= 0x7F) {
+ return _decodeLegacy(raw);
+ } else if (prefix == 0x01) {
+ return _decodeEip2930(raw);
+ } else if (prefix == 0x02) {
+ return _decodeEip1559(raw);
+ } else if (prefix == 0x03) {
+ return _decodeEip4844(raw);
+ } else {
+ revert UnsupportedTxType();
+ }
+ }
+
+ /// @notice Recover the sender of a transaction
+ /// @param transaction The transaction object
+ /// @return sender The address of the sender
+ function recoverSender(
+ Transaction memory transaction
+ ) internal pure returns (address) {
+ return ECDSA.recover(preimage(transaction), signature(transaction));
+ }
+
+ /// @notice Compute the preimage of a transaction object
+ /// @dev This is the hash of the transaction that is signed by the sender to obtain the signature
+ /// @param transaction The transaction object
+ /// @return preimg The preimage hash of the transaction
+ function preimage(
+ Transaction memory transaction
+ ) internal pure returns (bytes32 preimg) {
+ preimg = keccak256(unsigned(transaction));
+ }
+
+ /// @notice Compute the unsigned transaction object
+ /// @dev This is the transaction object without the signature
+ /// @param transaction The transaction object
+ /// @return unsignedTx The unsigned transaction object
+ function unsigned(
+ Transaction memory transaction
+ ) internal pure returns (bytes memory unsignedTx) {
+ if (transaction.txType == TxType.Legacy) {
+ unsignedTx = _unsignedLegacy(transaction);
+ } else if (transaction.txType == TxType.Eip2930) {
+ unsignedTx = _unsignedEip2930(transaction);
+ } else if (transaction.txType == TxType.Eip1559) {
+ unsignedTx = _unsignedEip1559(transaction);
+ } else if (transaction.txType == TxType.Eip4844) {
+ unsignedTx = _unsignedEip4844(transaction);
+ } else {
+ revert UnsupportedTxType();
+ }
+ }
+
+ /// @notice Return the hex-encoded signature of a transaction object
+ /// @param transaction The transaction object
+ /// @return sig The hex-encoded signature
+ function signature(
+ Transaction memory transaction
+ ) internal pure returns (bytes memory sig) {
+ if (transaction.sig.length == 0) {
+ revert NoSignature();
+ } else if (transaction.sig.length != 65) {
+ revert InvalidSignatureLength();
+ } else {
+ sig = transaction.sig;
+ }
+ }
+
+ /// @notice Helper to decode a legacy (type 0) transaction
+ /// @param raw The raw transaction bytes
+ /// @return transaction The decoded transaction object
+ function _decodeLegacy(
+ bytes memory raw
+ ) private pure returns (Transaction memory transaction) {
+ transaction.txType = TxType.Legacy;
+
+ // Legacy transactions don't have a type prefix, so we can decode directly
+ RLPReader.RLPItem[] memory fields = raw.toRLPItem().readList();
+
+ if (fields.length != 9 && fields.length != 6) {
+ revert InvalidFieldCount();
+ }
+
+ transaction.nonce = fields[0].readUint256();
+ transaction.gasPrice = fields[1].readUint256();
+ transaction.gasLimit = fields[2].readUint256();
+ transaction.to = fields[3].readAddress();
+ transaction.value = fields[4].readUint256();
+ transaction.data = fields[5].readBytes();
+
+ // Legacy unsigned transaction
+ if (fields.length == 6) {
+ return transaction;
+ }
+
+ // rlp expects signature values in (v, r, s) order
+ uint64 v = uint64(fields[6].readUint256());
+ uint256 r = fields[7].readUint256();
+ uint256 s = fields[8].readUint256();
+
+ if (r == 0 && s == 0) {
+ // EIP-155 unsigned transaction
+ transaction.chainId = v;
+ transaction.isChainIdSet = true;
+ } else {
+ if (v >= 35) {
+ // Compute the EIP-155 chain ID (or 0 for legacy)
+ transaction.chainId = (v - 35) / 2;
+ transaction.legacyV = v;
+ transaction.isChainIdSet = true;
+ }
+
+ // Compute the signature
+ uint8 parityV = uint8(((v ^ 1) % 2) + 27);
+ transaction.sig = abi.encodePacked(bytes32(r), bytes32(s), parityV);
+ }
+ }
+
+ /// @notice Helper to decode an EIP-2930 (type 1) transaction
+ /// @param raw The raw transaction bytes
+ /// @return transaction The decoded transaction object
+ function _decodeEip2930(
+ bytes memory raw
+ ) private pure returns (Transaction memory transaction) {
+ transaction.txType = TxType.Eip2930;
+
+ // Skip the first byte (transaction type)
+ bytes memory rlpData = raw.slice(1, raw.length - 1);
+ RLPReader.RLPItem[] memory fields = rlpData.toRLPItem().readList();
+
+ if (fields.length != 8 && fields.length != 11) {
+ revert InvalidFieldCount();
+ }
+
+ transaction.chainId = uint64(fields[0].readUint256());
+ transaction.nonce = fields[1].readUint256();
+ transaction.gasPrice = fields[2].readUint256();
+ transaction.gasLimit = fields[3].readUint256();
+ transaction.to = fields[4].readAddress();
+ transaction.value = fields[5].readUint256();
+ transaction.data = fields[6].readBytes();
+
+ RLPReader.RLPItem[] memory accessListItems = fields[7].readList();
+ transaction.accessList = new bytes[](accessListItems.length);
+ for (uint256 i = 0; i < accessListItems.length; i++) {
+ transaction.accessList[i] = accessListItems[i].readRawBytes();
+ }
+
+ // EIP-2930 Unsigned transaction
+ if (fields.length == 8) {
+ return transaction;
+ }
+
+ uint8 v = uint8(fields[8].readUint256()) + 27;
+ bytes32 r = fields[9].readBytes32();
+ bytes32 s = fields[10].readBytes32();
+
+ // compute the signature
+ transaction.sig = abi.encodePacked(r, s, v);
+ }
+
+ /// @notice Helper to decode an EIP-1559 (type 2) transaction
+ /// @param raw The raw transaction bytes
+ /// @return transaction The decoded transaction object
+ function _decodeEip1559(
+ bytes memory raw
+ ) private pure returns (Transaction memory transaction) {
+ transaction.txType = TxType.Eip1559;
+
+ // Skip the first byte (transaction type)
+ bytes memory rlpData = raw.slice(1, raw.length - 1);
+ RLPReader.RLPItem[] memory fields = rlpData.toRLPItem().readList();
+
+ if (fields.length != 9 && fields.length != 12) {
+ revert InvalidFieldCount();
+ }
+
+ transaction.chainId = uint64(fields[0].readUint256());
+ transaction.nonce = fields[1].readUint256();
+ transaction.maxPriorityFeePerGas = fields[2].readUint256();
+ transaction.maxFeePerGas = fields[3].readUint256();
+ transaction.gasLimit = fields[4].readUint256();
+ transaction.to = fields[5].readAddress();
+ transaction.value = fields[6].readUint256();
+ transaction.data = fields[7].readBytes();
+
+ RLPReader.RLPItem[] memory accessListItems = fields[8].readList();
+ transaction.accessList = new bytes[](accessListItems.length);
+ for (uint256 i = 0; i < accessListItems.length; i++) {
+ transaction.accessList[i] = accessListItems[i].readRawBytes();
+ }
+
+ if (fields.length == 9) {
+ // EIP-1559 Unsigned transaction
+ return transaction;
+ }
+
+ uint8 v = uint8(fields[9].readUint256()) + 27;
+ bytes32 r = fields[10].readBytes32();
+ bytes32 s = fields[11].readBytes32();
+
+ // compute the signature
+ transaction.sig = abi.encodePacked(r, s, v);
+ }
+
+ /// @notice Helper to decode an EIP-4844 (type 3) transaction
+ /// @param raw The raw transaction bytes
+ /// @return transaction The decoded transaction object
+ function _decodeEip4844(
+ bytes memory raw
+ ) private pure returns (Transaction memory transaction) {
+ transaction.txType = TxType.Eip4844;
+
+ // Skip the first byte (transaction type)
+ bytes memory rlpData = raw.slice(1, raw.length - 1);
+ RLPReader.RLPItem[] memory fields = rlpData.toRLPItem().readList();
+
+ if (fields.length != 11 && fields.length != 14) {
+ revert InvalidFieldCount();
+ }
+
+ transaction.chainId = uint64(fields[0].readUint256());
+ transaction.nonce = fields[1].readUint256();
+ transaction.maxPriorityFeePerGas = fields[2].readUint256();
+ transaction.maxFeePerGas = fields[3].readUint256();
+ transaction.gasLimit = fields[4].readUint256();
+ transaction.to = fields[5].readAddress();
+ transaction.value = fields[6].readUint256();
+ transaction.data = fields[7].readBytes();
+
+ RLPReader.RLPItem[] memory accessListItems = fields[8].readList();
+ transaction.accessList = new bytes[](accessListItems.length);
+ for (uint256 i = 0; i < accessListItems.length; i++) {
+ transaction.accessList[i] = accessListItems[i].readRawBytes();
+ }
+
+ transaction.maxFeePerBlobGas = fields[9].readUint256();
+
+ RLPReader.RLPItem[] memory blobVersionedHashesItems = fields[10].readList();
+ transaction.blobVersionedHashes = new bytes32[](blobVersionedHashesItems.length);
+ for (uint256 i = 0; i < blobVersionedHashesItems.length; i++) {
+ transaction.blobVersionedHashes[i] = blobVersionedHashesItems[i].readBytes32();
+ }
+
+ if (fields.length == 11) {
+ // Unsigned transaction
+ return transaction;
+ }
+
+ uint8 v = uint8(fields[11].readUint256()) + 27;
+ bytes32 r = fields[12].readBytes32();
+ bytes32 s = fields[13].readBytes32();
+
+ // compute the signature
+ transaction.sig = abi.encodePacked(r, s, v);
+ }
+
+ /// @notice Helper to RLP-encode an unsigned legacy transaction
+ /// @param transaction The transaction object
+ /// @return unsignedTx The unsigned transaction bytes
+ function _unsignedLegacy(
+ Transaction memory transaction
+ ) private pure returns (bytes memory unsignedTx) {
+ uint64 chainId = 0;
+ if (transaction.chainId != 0) {
+ // A chainId was provided: if non-zero, we'll use EIP-155
+ chainId = transaction.chainId;
+ } else if (transaction.sig.length != 0) {
+ // No explicit chainId, but EIP-155 have a derived implicit chainId
+ // based on the V value of the signature
+ if (transaction.legacyV >= 35) {
+ chainId = (transaction.legacyV - 35) / 2;
+ }
+ }
+
+ uint256 fieldsCount = 6 + (transaction.isChainIdSet ? 3 : 0);
+ bytes[] memory fields = new bytes[](fieldsCount);
+
+ fields[0] = RLPWriter.writeUint(transaction.nonce);
+ fields[1] = RLPWriter.writeUint(transaction.gasPrice);
+ fields[2] = RLPWriter.writeUint(transaction.gasLimit);
+ fields[3] = RLPWriter.writeAddress(transaction.to);
+ fields[4] = RLPWriter.writeUint(transaction.value);
+ fields[5] = RLPWriter.writeBytes(transaction.data);
+
+ if (transaction.isChainIdSet) {
+ if (transaction.chainId == 0) {
+ // Edge case: chainId is 0, but we still need to encode it
+ // as a single empty byte.
+ fields[6] = abi.encodePacked(bytes1(0));
+ } else {
+ fields[6] = RLPWriter.writeUint(chainId);
+ }
+
+ fields[7] = RLPWriter.writeBytes(new bytes(0));
+ fields[8] = RLPWriter.writeBytes(new bytes(0));
+ }
+
+ unsignedTx = RLPWriter.writeList(fields);
+ }
+
+ /// @notice Helper to RLP-encode an unsigned EIP-2930 transaction
+ /// @param transaction The transaction object
+ /// @return unsignedTx The unsigned transaction bytes
+ function _unsignedEip2930(
+ Transaction memory transaction
+ ) private pure returns (bytes memory unsignedTx) {
+ bytes[] memory fields = new bytes[](8);
+
+ fields[0] = RLPWriter.writeUint(transaction.chainId);
+ fields[1] = RLPWriter.writeUint(transaction.nonce);
+ fields[2] = RLPWriter.writeUint(transaction.gasPrice);
+ fields[3] = RLPWriter.writeUint(transaction.gasLimit);
+ fields[4] = RLPWriter.writeAddress(transaction.to);
+ fields[5] = RLPWriter.writeUint(transaction.value);
+ fields[6] = RLPWriter.writeBytes(transaction.data);
+
+ bytes[] memory accessList = new bytes[](transaction.accessList.length);
+ for (uint256 i = 0; i < transaction.accessList.length; i++) {
+ accessList[i] = transaction.accessList[i];
+ }
+ fields[7] = RLPWriter.writeList(accessList);
+
+ // EIP-2718 envelope
+ unsignedTx = abi.encodePacked(uint8(TxType.Eip2930), RLPWriter.writeList(fields));
+ }
+
+ /// @notice Helper to RLP-encode an unsigned EIP-1559 transaction
+ /// @param transaction The transaction object
+ /// @return unsignedTx The unsigned transaction bytes
+ function _unsignedEip1559(
+ Transaction memory transaction
+ ) private pure returns (bytes memory unsignedTx) {
+ bytes[] memory fields = new bytes[](9);
+
+ fields[0] = RLPWriter.writeUint(transaction.chainId);
+ fields[1] = RLPWriter.writeUint(transaction.nonce);
+ fields[2] = RLPWriter.writeUint(transaction.maxPriorityFeePerGas);
+ fields[3] = RLPWriter.writeUint(transaction.maxFeePerGas);
+ fields[4] = RLPWriter.writeUint(transaction.gasLimit);
+ fields[5] = RLPWriter.writeAddress(transaction.to);
+ fields[6] = RLPWriter.writeUint(transaction.value);
+ fields[7] = RLPWriter.writeBytes(transaction.data);
+
+ bytes[] memory accessList = new bytes[](transaction.accessList.length);
+ for (uint256 i = 0; i < transaction.accessList.length; i++) {
+ accessList[i] = transaction.accessList[i];
+ }
+ fields[8] = RLPWriter.writeList(accessList);
+
+ // EIP-2718 envelope
+ unsignedTx = abi.encodePacked(uint8(TxType.Eip1559), RLPWriter.writeList(fields));
+ }
+
+ /// @notice Helper to RLP-encode an unsigned EIP-4844 transaction
+ /// @param transaction The transaction object
+ /// @return unsignedTx The unsigned transaction bytes
+ function _unsignedEip4844(
+ Transaction memory transaction
+ ) private pure returns (bytes memory unsignedTx) {
+ bytes[] memory fields = new bytes[](11);
+
+ fields[0] = RLPWriter.writeUint(transaction.chainId);
+ fields[1] = RLPWriter.writeUint(transaction.nonce);
+ fields[2] = RLPWriter.writeUint(transaction.maxPriorityFeePerGas);
+ fields[3] = RLPWriter.writeUint(transaction.maxFeePerGas);
+ fields[4] = RLPWriter.writeUint(transaction.gasLimit);
+ fields[5] = RLPWriter.writeAddress(transaction.to);
+ fields[6] = RLPWriter.writeUint(transaction.value);
+ fields[7] = RLPWriter.writeBytes(transaction.data);
+
+ bytes[] memory accessList = new bytes[](transaction.accessList.length);
+ for (uint256 i = 0; i < transaction.accessList.length; i++) {
+ accessList[i] = transaction.accessList[i];
+ }
+ fields[8] = RLPWriter.writeList(accessList);
+
+ fields[9] = RLPWriter.writeUint(transaction.maxFeePerBlobGas);
+
+ bytes[] memory blobVersionedHashes = new bytes[](transaction.blobVersionedHashes.length);
+ for (uint256 i = 0; i < transaction.blobVersionedHashes.length; i++) {
+ // Decode bytes32 as uint256 (RLPWriter doesn't support bytes32 but they are equivalent)
+ blobVersionedHashes[i] = RLPWriter.writeUint(uint256(transaction.blobVersionedHashes[i]));
+ }
+ fields[10] = RLPWriter.writeList(blobVersionedHashes);
+
+ // EIP-2718 envelope
+ unsignedTx = abi.encodePacked(uint8(TxType.Eip4844), RLPWriter.writeList(fields));
+ }
+}
diff --git a/bolt-contracts/src/lib/bls/BLS12381.sol b/bolt-contracts/src/lib/bls/BLS12381.sol
new file mode 100644
index 000000000..635302213
--- /dev/null
+++ b/bolt-contracts/src/lib/bls/BLS12381.sol
@@ -0,0 +1,428 @@
+// ======================================================
+// Code below is copied from:
+// https://github.com/NethermindEth/Taiko-Preconf-AVS/blob/caf9fbbde0dd84947af5a7b26610ffd38525d932/SmartContracts/src/libraries/BLS12381.sol
+//
+// If/when a license will be added to the original code, it will be added here as well.
+// ======================================================
+
+// SPDX-License-Identifier: UNLICENSED
+// Functions in this library have been adapted from:
+// https://github.com/ethyla/bls12-381-hash-to-curve/blob/main/src/HashToCurve.sol
+pragma solidity 0.8.25;
+
+library BLS12381 {
+ using BLS12381 for *;
+
+ struct FieldPoint2 {
+ uint256[2] u;
+ uint256[2] u_I;
+ }
+
+ struct G1Point {
+ uint256[2] x;
+ uint256[2] y;
+ }
+
+ struct G2Point {
+ uint256[2] x;
+ uint256[2] x_I;
+ uint256[2] y;
+ uint256[2] y_I;
+ }
+
+ /// @dev Referenced from https://eips.ethereum.org/EIPS/eip-2537#curve-parameters
+ function baseFieldModulus() internal pure returns (uint256[2] memory) {
+ return [
+ 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7,
+ 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
+ ];
+ }
+
+ /// @dev Referenced from https://eips.ethereum.org/EIPS/eip-2537#curve-parameters
+ function generatorG1() internal pure returns (G1Point memory) {
+ return G1Point({
+ x: [
+ 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f,
+ 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb
+ ],
+ y: [
+ 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4,
+ 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1
+ ]
+ });
+ }
+
+ /**
+ * @notice Negates a G1 point, by reflecting it over the x-axis
+ * @dev Assumes that the Y coordinate is always less than the field modulus
+ * @param point The G1 point to negate
+ */
+ function negate(
+ G1Point memory point
+ ) internal pure returns (G1Point memory) {
+ uint256[2] memory fieldModulus = baseFieldModulus();
+ uint256[2] memory yNeg;
+
+ // Perform word-wise elementary subtraction
+ if (fieldModulus[1] < point.y[1]) {
+ yNeg[1] = type(uint256).max - (point.y[1] - fieldModulus[1]);
+ fieldModulus[0] -= 1; // borrow
+ } else {
+ yNeg[1] = fieldModulus[1] - point.y[1];
+ }
+ yNeg[0] = fieldModulus[0] - point.y[0];
+
+ return G1Point({x: point.x, y: yNeg});
+ }
+
+ /**
+ * @notice Transforms a sequence of bytes into a G2 point
+ * @dev Based on https://datatracker.ietf.org/doc/html/rfc9380
+ * @param message The message to hash
+ * @param dst The domain separation tag
+ */
+ function hashToCurveG2(bytes memory message, bytes memory dst) internal view returns (G2Point memory r) {
+ // 1. u = hash_to_field(msg, 2)
+ FieldPoint2[2] memory u = hashToFieldFp2(message, dst);
+ // 2. Q0 = map_to_curve(u[0])
+ G2Point memory q0 = u[0].mapToG2();
+ // 3. Q1 = map_to_curve(u[1])
+ G2Point memory q1 = u[1].mapToG2();
+ // 4. R = Q0 + Q1
+ r = q0.plus(q1);
+ // 5. P = clear_cofactor(R)
+ // Not needed as map fp to g1 already does it
+ }
+
+ /**
+ * @notice Transforms a sequence of bytes into an element in the FP2 field
+ * @dev Based on https://datatracker.ietf.org/doc/html/rfc9380
+ * @param message The message to hash
+ * @param dst The domain separation tag
+ */
+ function hashToFieldFp2(bytes memory message, bytes memory dst) internal view returns (FieldPoint2[2] memory) {
+ // 1. len_in_bytes = count * m * L
+ // so always 2 * 2 * 64 = 256
+ uint16 lenInBytes = 256;
+ // 2. uniform_bytes = expand_message(msg, DST, len_in_bytes)
+ uint256[] memory pseudoRandomBytes = _expandMsgXmd(message, dst, lenInBytes);
+ FieldPoint2[2] memory u;
+ // No loop here saves 800 gas hardcoding offset an additional 300
+ // 3. for i in (0, ..., count - 1):
+ // 4. for j in (0, ..., m - 1):
+ // 5. elm_offset = L * (j + i * m)
+ // 6. tv = substr(uniform_bytes, elm_offset, HTF_L)
+ // uint8 HTF_L = 64;
+ // bytes memory tv = new bytes(64);
+ // 7. e_j = OS2IP(tv) mod p
+ // 8. u_i = (e_0, ..., e_(m - 1))
+ // tv = bytes.concat(pseudo_random_bytes[0], pseudo_random_bytes[1]);
+ u[0].u = _modfield(pseudoRandomBytes[0], pseudoRandomBytes[1]);
+ u[0].u_I = _modfield(pseudoRandomBytes[2], pseudoRandomBytes[3]);
+ u[1].u = _modfield(pseudoRandomBytes[4], pseudoRandomBytes[5]);
+ u[1].u_I = _modfield(pseudoRandomBytes[6], pseudoRandomBytes[7]);
+ // 9. return (u_0, ..., u_(count - 1))
+ return u;
+ }
+
+ /**
+ * @notice Returns a G1Point in the compressed form
+ * @dev Based on https://github.com/zcash/librustzcash/blob/6e0364cd42a2b3d2b958a54771ef51a8db79dd29/pairing/src/bls12_381/README.md#serialization
+ * @param point The G1 point to compress
+ */
+ function compress(
+ G1Point memory point
+ ) internal pure returns (uint256[2] memory) {
+ uint256[2] memory r = point.x;
+
+ // Set the first MSB
+ r[0] = r[0] | (1 << 127);
+
+ // Second MSB is left to be 0 since we are assuming that no infinity points are involved
+
+ // Set the third MSF If point.y is lexicographically larger than the y in negated point
+ if (_greaterThan(point.y, point.negate().y)) {
+ r[0] = r[0] | (1 << 125);
+ }
+
+ return r;
+ }
+
+ //==================
+ // Precompile calls
+ //==================
+
+ /**
+ * @notice Adds two G2 points using the precompile at 0x0e
+ */
+ function plus(G2Point memory point1, G2Point memory point2) internal view returns (G2Point memory) {
+ G2Point memory r;
+
+ uint256[16] memory input = [
+ point1.x[0],
+ point1.x[1],
+ point1.x_I[0],
+ point1.x_I[1],
+ point1.y[0],
+ point1.y[1],
+ point1.y_I[0],
+ point1.y_I[1],
+ point2.x[0],
+ point2.x[1],
+ point2.x_I[0],
+ point2.x_I[1],
+ point2.y[0],
+ point2.y[1],
+ point2.y_I[0],
+ point2.y_I[1]
+ ];
+
+ // ABI for G2 addition precompile
+ // G2 addition call expects 512 bytes as an input that is interpreted as byte concatenation of two G2 points (256 bytes each). Output is an encoding of addition operation result - single G2 point (256 bytes).
+ assembly {
+ let success :=
+ staticcall(
+ sub(gas(), 2000),
+ /// gas should be 800
+ 0x0e, // address of BLS12_G2ADD
+ input, //input offset
+ 512, // input size
+ r, // output offset
+ 256 // output size
+ )
+ if iszero(success) { revert(0, 0) }
+ }
+
+ return r;
+ }
+
+ /**
+ * @notice Maps an element of the FP2 field to a G2 point using the precompile at 0x13
+ */
+ function mapToG2(
+ FieldPoint2 memory fp2
+ ) internal view returns (G2Point memory) {
+ G2Point memory r;
+
+ uint256[4] memory input = [fp2.u[0], fp2.u[1], fp2.u_I[0], fp2.u_I[1]];
+
+ // ABI for mapping Fp2 element to G2 point precompile
+ // Field-to-curve call expects 128 bytes an an input that is interpreted as a an element of the quadratic extension field. Output of this call is 256 bytes and is G2 point following respective encoding rules.
+ assembly {
+ let success :=
+ staticcall(
+ sub(gas(), 2000),
+ /// gas should be 75000
+ 0x13, // address of BLS12_MAP_FP2_TO_G2
+ input, //input offset
+ 128, // input size
+ r, // output offset
+ 256 // output size
+ )
+ if iszero(success) { revert(0, 0) }
+ }
+
+ return r;
+ }
+
+ /**
+ * @notice Pairing check using the precompile at 0x11
+ */
+ function pairing(
+ G1Point memory a1,
+ G2Point memory b1,
+ G1Point memory a2,
+ G2Point memory b2
+ ) internal view returns (bool) {
+ bool r;
+
+ uint256[24] memory input = [
+ a1.x[0],
+ a1.x[1],
+ a1.y[0],
+ a1.y[1],
+ b1.x[0],
+ b1.x[1],
+ b1.x_I[0],
+ b1.x_I[1],
+ b1.y[0],
+ b1.y[1],
+ b1.y_I[0],
+ b1.y_I[1],
+ a2.x[0],
+ a2.x[1],
+ a2.y[0],
+ a2.y[1],
+ b2.x[0],
+ b2.x[1],
+ b2.x_I[0],
+ b2.x_I[1],
+ b2.y[0],
+ b2.y[1],
+ b2.y_I[0],
+ b2.y_I[1]
+ ];
+
+ // ABI for pairing precompile
+ // Pairing expects 384 (G1Point = 128 bytes, G2Point = 256 bytes) * k bytes as input.
+ // In this case, since two pairs of points are being passed, the input size is 384 * 2 = 768 bytes.
+ assembly {
+ let success :=
+ staticcall(
+ sub(gas(), 2000),
+ /// gas should be 151000
+ 0x11, // address of BLS12_PAIRING
+ input, //input offset
+ 768, // input size
+ r, // output offset
+ 32 // output size
+ )
+ if iszero(success) { revert(0, 0) }
+ }
+
+ return r;
+ }
+
+ //=========
+ // Helpers
+ //=========
+
+ function _expandMsgXmd(
+ bytes memory message,
+ bytes memory dst,
+ uint16 lenInBytes
+ ) internal pure returns (uint256[] memory) {
+ // 1. ell = ceil(len_in_bytes / b_in_bytes)
+ // b_in_bytes seems to be 32 for sha256
+ // ceil the division
+ uint256 ell = (lenInBytes - 1) / 32 + 1;
+
+ // 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255
+ require(ell <= 255, "len_in_bytes too large for sha256");
+ // Not really needed because of parameter type
+ // require(lenInBytes <= 65535, "len_in_bytes too large");
+ // no length normalizing via hashing
+ require(dst.length <= 255, "dst too long");
+
+ bytes memory dstPrime = bytes.concat(dst, bytes1(uint8(dst.length)));
+
+ // 4. Z_pad = I2OSP(0, s_in_bytes)
+ // this should be sha256 blocksize so 64 bytes
+ bytes memory zPad =
+ hex"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+
+ // 5. l_i_b_str = I2OSP(len_in_bytes, 2)
+ // length in byte string?
+ bytes2 libStr = bytes2(lenInBytes);
+
+ // 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime
+ bytes memory msgPrime = bytes.concat(zPad, message, libStr, hex"00", dstPrime);
+
+ uint256 b_0;
+ uint256[] memory b = new uint256[](ell);
+
+ // 7. b_0 = H(msg_prime)
+ b_0 = uint256(sha256(msgPrime));
+
+ // 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime)
+ b[0] = uint256(sha256(bytes.concat(bytes32(b_0), hex"01", dstPrime)));
+
+ // 9. for i in (2, ..., ell):
+ for (uint8 i = 2; i <= ell; i++) {
+ // 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime)
+ bytes memory tmp = abi.encodePacked(b_0 ^ b[i - 2], i, dstPrime);
+ b[i - 1] = uint256(sha256(tmp));
+ }
+ // 11. uniform_bytes = b_1 || ... || b_ell
+ // 12. return substr(uniform_bytes, 0, len_in_bytes)
+ // Here we don't need the uniform_bytes because b is already properly formed
+ return b;
+ }
+
+ function _modfield(uint256 _b1, uint256 _b2) internal view returns (uint256[2] memory r) {
+ assembly {
+ let bl := 0x40
+ let ml := 0x40
+
+ let freemem := mload(0x40) // Free memory pointer is always stored at 0x40
+
+ // arg[0] = base.length @ +0
+ mstore(freemem, bl)
+ // arg[1] = exp.length @ +0x20
+ mstore(add(freemem, 0x20), 0x20)
+ // arg[2] = mod.length @ +0x40
+ mstore(add(freemem, 0x40), ml)
+
+ // arg[3] = base.bits @ + 0x60
+ // places the first 32 bytes of _b1 and the last 32 bytes of _b2
+ mstore(add(freemem, 0x60), _b1)
+ mstore(add(freemem, 0x80), _b2)
+
+ // arg[4] = exp.bits @ +0x60+base.length
+ // exponent always 1
+ mstore(add(freemem, 0xa0), 1)
+
+ // arg[5] = mod.bits @ +96+base.length+exp.length
+ // this field_modulus as hex 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787
+ // we add the 0 prefix so that the result will be exactly 64 bytes
+ // saves 300 gas per call instead of sending it along every time
+ // places the first 32 bytes and the last 32 bytes of the field modulus
+ mstore(add(freemem, 0xc0), 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7)
+ mstore(add(freemem, 0xe0), 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab)
+
+ // Invoke contract 0x5, put return value right after mod.length, @ 0x60
+ let success :=
+ staticcall(
+ sub(gas(), 1350), // gas
+ 0x5, // mpdexp precompile
+ freemem, //input offset
+ 0x100, // input size = 0x60+base.length+exp.length+mod.length
+ add(freemem, 0x60), // output offset
+ ml // output size
+ )
+ if iszero(success) { revert(0, 0) }
+
+ // point to mod length, result was placed immediately after
+ r := add(freemem, 0x60)
+ //adjust freemem pointer
+ mstore(0x40, add(add(freemem, 0x60), ml))
+ }
+ }
+
+ /**
+ * @notice Returns true if `a` is lexicographically greater than `b`
+ * @dev It makes the comparison bit-wise.
+ * This functions also assumes that the passed values are 48-byte long BLS pub keys that have
+ * 16 functional bytes in the first word, and 32 bytes in the second.
+ */
+ function _greaterThan(uint256[2] memory a, uint256[2] memory b) internal pure returns (bool) {
+ uint256 wordA;
+ uint256 wordB;
+ uint256 mask;
+
+ // Only compare the unequal words
+ if (a[0] == b[0]) {
+ wordA = a[1];
+ wordB = b[1];
+ mask = 1 << 255;
+ } else {
+ wordA = a[0];
+ wordB = b[0];
+ mask = 1 << 127; // Only check for lower 16 bytes in the first word
+ }
+
+ // We may safely set the control value to be less than 256 since it is guaranteed that the
+ // the loop returns if the first words are different.
+ for (uint256 i; i < 256; ++i) {
+ uint256 x = wordA & mask;
+ uint256 y = wordB & mask;
+
+ if (x == 0 && y != 0) return false;
+ if (x != 0 && y == 0) return true;
+
+ mask = mask >> 1;
+ }
+
+ return false;
+ }
+}
diff --git a/bolt-contracts/src/lib/bls/BLSSignatureVerifier.sol b/bolt-contracts/src/lib/bls/BLSSignatureVerifier.sol
new file mode 100644
index 000000000..43344e733
--- /dev/null
+++ b/bolt-contracts/src/lib/bls/BLSSignatureVerifier.sol
@@ -0,0 +1,64 @@
+// ======================================================
+// Code below is copied from:
+// https://github.com/NethermindEth/Taiko-Preconf-AVS/blob/caf9fbbde0dd84947af5a7b26610ffd38525d932/SmartContracts/src/libraries/BLS12381.sol
+//
+// If/when a license will be added to the original code, it will be added here as well.
+// ======================================================
+
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity 0.8.25;
+
+import {BLS12381} from "./BLS12381.sol";
+
+contract BLSSignatureVerifier {
+ using BLS12381 for *;
+
+ /// @dev The domain separation tag for the BLS signature
+ function dst() internal pure returns (bytes memory) {
+ // Todo: This must be set based on the recommendations of RFC9380
+ return hex"";
+ }
+
+ /**
+ * @notice Returns `true` if the BLS signature on the message matches against the public key
+ * @param message The message bytes
+ * @param sig The BLS signature
+ * @param pubkey The BLS public key of the expected signer
+ */
+ function _verifySignature(
+ bytes memory message,
+ BLS12381.G2Point memory sig,
+ BLS12381.G1Point memory pubkey
+ ) internal view returns (bool) {
+ // Hash the message bytes into a G2 point
+ BLS12381.G2Point memory msgG2 = message.hashToCurveG2(dst());
+
+ // Return the pairing check result
+ return BLS12381.pairing(BLS12381.generatorG1().negate(), sig, pubkey, msgG2);
+ }
+
+ /**
+ * @notice Aggregate a list of BLS public keys into a single BLS public key
+ * @param pubkeys The list of BLS public keys to aggregate
+ * @return The aggregated BLS public key
+ */
+ function _aggregatePubkeys(
+ BLS12381.G1Point[] calldata pubkeys
+ ) internal pure returns (BLS12381.G1Point memory) {
+ // TODO: implement + test.
+
+ // Simply adding pubkeys will result in a rogue key vulnerability.
+ //
+ // https://xn--2-umb.com/22/bls-signatures/#rogue-key-attack
+ // https://github.com/chronicleprotocol/scribe/blob/main/docs/Schnorr.md#key-aggregation-for-multisignatures
+
+ uint256[2] memory aggPubkeyZero = [uint256(0), uint256(0)];
+ BLS12381.G1Point memory aggPubkey = BLS12381.G1Point(aggPubkeyZero, aggPubkeyZero);
+
+ // unimplemented!()
+ // silence compiler warnings
+ pubkeys;
+
+ return aggPubkey;
+ }
+}
diff --git a/bolt-contracts/src/lib/rlp/RLPReader.sol b/bolt-contracts/src/lib/rlp/RLPReader.sol
new file mode 100644
index 000000000..a8620e488
--- /dev/null
+++ b/bolt-contracts/src/lib/rlp/RLPReader.sol
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+// Credits: Optimism contributors.
+// Ref: https://github.com/ethereum-optimism/optimism/blob/05deae54595b0e6bdd33580de81cb9ad194898bc/packages/contracts-bedrock/src/libraries/rlp/RLPReader.sol
+
+/**
+ * @title RLPReader
+ * @dev Adapted from "RLPReader" by Hamdi Allam (hamdi.allam97@gmail.com).
+ */
+library RLPReader {
+ /**
+ *
+ * Constants *
+ *
+ */
+ uint256 internal constant MAX_LIST_LENGTH = 32;
+
+ /**
+ *
+ * Enums *
+ *
+ */
+ enum RLPItemType {
+ DATA_ITEM,
+ LIST_ITEM
+ }
+
+ /**
+ *
+ * Structs *
+ *
+ */
+ struct RLPItem {
+ uint256 length;
+ uint256 ptr;
+ }
+
+ /**
+ *
+ * Internal Functions *
+ *
+ */
+
+ /**
+ * Converts bytes to a reference to memory position and length.
+ * @param _in Input bytes to convert.
+ * @return Output memory reference.
+ */
+ function toRLPItem(
+ bytes memory _in
+ ) internal pure returns (RLPItem memory) {
+ uint256 ptr;
+ assembly {
+ ptr := add(_in, 32)
+ }
+
+ return RLPItem({length: _in.length, ptr: ptr});
+ }
+
+ /**
+ * Reads an RLP list value into a list of RLP items.
+ * @param _in RLP list value.
+ * @return Decoded RLP list items.
+ */
+ function readList(
+ RLPItem memory _in
+ ) internal pure returns (RLPItem[] memory) {
+ (uint256 listOffset,, RLPItemType itemType) = _decodeLength(_in);
+
+ require(itemType == RLPItemType.LIST_ITEM, "Invalid RLP list value.");
+
+ // Solidity in-memory arrays can't be increased in size, but *can* be decreased in size by
+ // writing to the length. Since we can't know the number of RLP items without looping over
+ // the entire input, we'd have to loop twice to accurately size this array. It's easier to
+ // simply set a reasonable maximum list length and decrease the size before we finish.
+ RLPItem[] memory out = new RLPItem[](MAX_LIST_LENGTH);
+
+ uint256 itemCount = 0;
+ uint256 offset = listOffset;
+ while (offset < _in.length) {
+ require(itemCount < MAX_LIST_LENGTH, "Provided RLP list exceeds max list length.");
+
+ (uint256 itemOffset, uint256 itemLength,) =
+ _decodeLength(RLPItem({length: _in.length - offset, ptr: _in.ptr + offset}));
+
+ out[itemCount] = RLPItem({length: itemLength + itemOffset, ptr: _in.ptr + offset});
+
+ itemCount += 1;
+ offset += itemOffset + itemLength;
+ }
+
+ // Decrease the array size to match the actual item count.
+ assembly {
+ mstore(out, itemCount)
+ }
+
+ return out;
+ }
+
+ /**
+ * Reads an RLP list value into a list of RLP items.
+ * @param _in RLP list value.
+ * @return Decoded RLP list items.
+ */
+ function readList(
+ bytes memory _in
+ ) internal pure returns (RLPItem[] memory) {
+ return readList(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP bytes value into bytes.
+ * @param _in RLP bytes value.
+ * @return Decoded bytes.
+ */
+ function readBytes(
+ RLPItem memory _in
+ ) internal pure returns (bytes memory) {
+ (uint256 itemOffset, uint256 itemLength, RLPItemType itemType) = _decodeLength(_in);
+
+ require(itemType == RLPItemType.DATA_ITEM, "Invalid RLP bytes value.");
+
+ return _copy(_in.ptr, itemOffset, itemLength);
+ }
+
+ /**
+ * Reads an RLP bytes value into bytes.
+ * @param _in RLP bytes value.
+ * @return Decoded bytes.
+ */
+ function readBytes(
+ bytes memory _in
+ ) internal pure returns (bytes memory) {
+ return readBytes(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP string value into a string.
+ * @param _in RLP string value.
+ * @return Decoded string.
+ */
+ function readString(
+ RLPItem memory _in
+ ) internal pure returns (string memory) {
+ return string(readBytes(_in));
+ }
+
+ /**
+ * Reads an RLP string value into a string.
+ * @param _in RLP string value.
+ * @return Decoded string.
+ */
+ function readString(
+ bytes memory _in
+ ) internal pure returns (string memory) {
+ return readString(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP bytes32 value into a bytes32.
+ * @param _in RLP bytes32 value.
+ * @return Decoded bytes32.
+ */
+ function readBytes32(
+ RLPItem memory _in
+ ) internal pure returns (bytes32) {
+ require(_in.length <= 33, "Invalid RLP bytes32 value.");
+
+ (uint256 itemOffset, uint256 itemLength, RLPItemType itemType) = _decodeLength(_in);
+
+ require(itemType == RLPItemType.DATA_ITEM, "Invalid RLP bytes32 value.");
+
+ uint256 ptr = _in.ptr + itemOffset;
+ bytes32 out;
+ assembly {
+ out := mload(ptr)
+
+ // Shift the bytes over to match the item size.
+ if lt(itemLength, 32) { out := div(out, exp(256, sub(32, itemLength))) }
+ }
+
+ return out;
+ }
+
+ /**
+ * Reads an RLP bytes32 value into a bytes32.
+ * @param _in RLP bytes32 value.
+ * @return Decoded bytes32.
+ */
+ function readBytes32(
+ bytes memory _in
+ ) internal pure returns (bytes32) {
+ return readBytes32(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP uint256 value into a uint256.
+ * @param _in RLP uint256 value.
+ * @return Decoded uint256.
+ */
+ function readUint256(
+ RLPItem memory _in
+ ) internal pure returns (uint256) {
+ return uint256(readBytes32(_in));
+ }
+
+ /**
+ * Reads an RLP uint256 value into a uint256.
+ * @param _in RLP uint256 value.
+ * @return Decoded uint256.
+ */
+ function readUint256(
+ bytes memory _in
+ ) internal pure returns (uint256) {
+ return readUint256(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP bool value into a bool.
+ * @param _in RLP bool value.
+ * @return Decoded bool.
+ */
+ function readBool(
+ RLPItem memory _in
+ ) internal pure returns (bool) {
+ require(_in.length == 1, "Invalid RLP boolean value.");
+
+ uint256 ptr = _in.ptr;
+ uint256 out;
+ assembly {
+ out := byte(0, mload(ptr))
+ }
+
+ require(out == 0 || out == 1, "RLPReader: Invalid RLP boolean value, must be 0 or 1");
+
+ return out != 0;
+ }
+
+ /**
+ * Reads an RLP bool value into a bool.
+ * @param _in RLP bool value.
+ * @return Decoded bool.
+ */
+ function readBool(
+ bytes memory _in
+ ) internal pure returns (bool) {
+ return readBool(toRLPItem(_in));
+ }
+
+ /**
+ * Reads an RLP address value into a address.
+ * @param _in RLP address value.
+ * @return Decoded address.
+ */
+ function readAddress(
+ RLPItem memory _in
+ ) internal pure returns (address) {
+ if (_in.length == 1) {
+ return address(0);
+ }
+
+ require(_in.length == 21, "Invalid RLP address value.");
+
+ return address(uint160(readUint256(_in)));
+ }
+
+ /**
+ * Reads an RLP address value into a address.
+ * @param _in RLP address value.
+ * @return Decoded address.
+ */
+ function readAddress(
+ bytes memory _in
+ ) internal pure returns (address) {
+ return readAddress(toRLPItem(_in));
+ }
+
+ /**
+ * Reads the raw bytes of an RLP item.
+ * @param _in RLP item to read.
+ * @return Raw RLP bytes.
+ */
+ function readRawBytes(
+ RLPItem memory _in
+ ) internal pure returns (bytes memory) {
+ return _copy(_in);
+ }
+
+ /**
+ *
+ * Private Functions *
+ *
+ */
+
+ /**
+ * Decodes the length of an RLP item.
+ * @param _in RLP item to decode.
+ * @return Offset of the encoded data.
+ * @return Length of the encoded data.
+ * @return RLP item type (LIST_ITEM or DATA_ITEM).
+ */
+ function _decodeLength(
+ RLPItem memory _in
+ ) private pure returns (uint256, uint256, RLPItemType) {
+ unchecked {
+ require(_in.length > 0, "RLP item cannot be null.");
+
+ uint256 ptr = _in.ptr;
+ uint256 prefix;
+ assembly {
+ prefix := byte(0, mload(ptr))
+ }
+
+ if (prefix <= 0x7f) {
+ // Single byte.
+
+ return (0, 1, RLPItemType.DATA_ITEM);
+ } else if (prefix <= 0xb7) {
+ // Short string.
+
+ uint256 strLen = prefix - 0x80;
+
+ require(_in.length > strLen, "Invalid RLP short string.");
+
+ return (1, strLen, RLPItemType.DATA_ITEM);
+ } else if (prefix <= 0xbf) {
+ // Long string.
+ uint256 lenOfStrLen = prefix - 0xb7;
+
+ require(_in.length > lenOfStrLen, "Invalid RLP long string length.");
+
+ uint256 strLen;
+ assembly {
+ // Pick out the string length.
+ strLen := div(mload(add(ptr, 1)), exp(256, sub(32, lenOfStrLen)))
+ }
+
+ require(_in.length > lenOfStrLen + strLen, "Invalid RLP long string.");
+
+ return (1 + lenOfStrLen, strLen, RLPItemType.DATA_ITEM);
+ } else if (prefix <= 0xf7) {
+ // Short list.
+ uint256 listLen = prefix - 0xc0;
+
+ require(_in.length > listLen, "Invalid RLP short list.");
+
+ return (1, listLen, RLPItemType.LIST_ITEM);
+ } else {
+ // Long list.
+ uint256 lenOfListLen = prefix - 0xf7;
+
+ require(_in.length > lenOfListLen, "Invalid RLP long list length.");
+
+ uint256 listLen;
+ assembly {
+ // Pick out the list length.
+ listLen := div(mload(add(ptr, 1)), exp(256, sub(32, lenOfListLen)))
+ }
+
+ require(_in.length > lenOfListLen + listLen, "Invalid RLP long list.");
+
+ return (1 + lenOfListLen, listLen, RLPItemType.LIST_ITEM);
+ }
+ }
+ }
+
+ /**
+ * Copies the bytes from a memory location.
+ * @param _src Pointer to the location to read from.
+ * @param _offset Offset to start reading from.
+ * @param _length Number of bytes to read.
+ * @return Copied bytes.
+ */
+ function _copy(uint256 _src, uint256 _offset, uint256 _length) private pure returns (bytes memory) {
+ unchecked {
+ bytes memory out = new bytes(_length);
+ if (out.length == 0) {
+ return out;
+ }
+
+ uint256 src = _src + _offset;
+ uint256 dest;
+ assembly {
+ dest := add(out, 32)
+ }
+
+ // Copy over as many complete words as we can.
+ for (uint256 i = 0; i < _length / 32; i++) {
+ assembly {
+ mstore(dest, mload(src))
+ }
+
+ src += 32;
+ dest += 32;
+ }
+
+ // Pick out the remaining bytes.
+ uint256 mask = 256 ** (32 - (_length % 32)) - 1;
+ assembly {
+ mstore(dest, or(and(mload(src), not(mask)), and(mload(dest), mask)))
+ }
+
+ return out;
+ }
+ }
+
+ /**
+ * Copies an RLP item into bytes.
+ * @param _in RLP item to copy.
+ * @return Copied bytes.
+ */
+ function _copy(
+ RLPItem memory _in
+ ) private pure returns (bytes memory) {
+ return _copy(_in.ptr, 0, _in.length);
+ }
+}
diff --git a/bolt-contracts/src/lib/rlp/RLPWriter.sol b/bolt-contracts/src/lib/rlp/RLPWriter.sol
new file mode 100644
index 000000000..fad2365a9
--- /dev/null
+++ b/bolt-contracts/src/lib/rlp/RLPWriter.sol
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+pragma experimental ABIEncoderV2;
+
+// Credits: Optimism contributors.
+// Ref: https://github.com/ethereum-optimism/optimism/blob/05deae54595b0e6bdd33580de81cb9ad194898bc/packages/contracts-bedrock/src/libraries/rlp/RLPWriter.sol
+
+/**
+ * @title RLPWriter
+ * @author Bakaoh (with modifications)
+ */
+library RLPWriter {
+ /**
+ *
+ * Internal Functions *
+ *
+ */
+
+ /**
+ * RLP encodes a byte string.
+ * @param _in The byte string to encode.
+ * @return _out The RLP encoded string in bytes.
+ */
+ function writeBytes(
+ bytes memory _in
+ ) internal pure returns (bytes memory _out) {
+ bytes memory encoded;
+
+ if (_in.length == 1 && uint8(_in[0]) < 128) {
+ encoded = _in;
+ } else {
+ encoded = abi.encodePacked(_writeLength(_in.length, 128), _in);
+ }
+
+ return encoded;
+ }
+
+ /**
+ * RLP encodes a list of RLP encoded byte byte strings.
+ * @param _in The list of RLP encoded byte strings.
+ * @return _out The RLP encoded list of items in bytes.
+ */
+ function writeList(
+ bytes[] memory _in
+ ) internal pure returns (bytes memory _out) {
+ bytes memory list = _flatten(_in);
+ return abi.encodePacked(_writeLength(list.length, 192), list);
+ }
+
+ /**
+ * RLP encodes a string.
+ * @param _in The string to encode.
+ * @return _out The RLP encoded string in bytes.
+ */
+ function writeString(
+ string memory _in
+ ) internal pure returns (bytes memory _out) {
+ return writeBytes(bytes(_in));
+ }
+
+ /**
+ * RLP encodes an address.
+ * @param _in The address to encode.
+ * @return _out The RLP encoded address in bytes.
+ */
+ function writeAddress(
+ address _in
+ ) internal pure returns (bytes memory _out) {
+ return writeBytes(abi.encodePacked(_in));
+ }
+
+ /**
+ * RLP encodes a uint.
+ * @param _in The uint256 to encode.
+ * @return _out The RLP encoded uint256 in bytes.
+ */
+ function writeUint(
+ uint256 _in
+ ) internal pure returns (bytes memory _out) {
+ return writeBytes(_toBinary(_in));
+ }
+
+ /**
+ * RLP encodes a bool.
+ * @param _in The bool to encode.
+ * @return _out The RLP encoded bool in bytes.
+ */
+ function writeBool(
+ bool _in
+ ) internal pure returns (bytes memory _out) {
+ bytes memory encoded = new bytes(1);
+ encoded[0] = (_in ? bytes1(0x01) : bytes1(0x80));
+ return encoded;
+ }
+
+ /**
+ *
+ * Private Functions *
+ *
+ */
+
+ /**
+ * Encode the first byte, followed by the `len` in binary form if `length` is more than 55.
+ * @param _len The length of the string or the payload.
+ * @param _offset 128 if item is string, 192 if item is list.
+ * @return _encoded RLP encoded bytes.
+ */
+ function _writeLength(uint256 _len, uint256 _offset) private pure returns (bytes memory _encoded) {
+ bytes memory encoded;
+
+ if (_len < 56) {
+ encoded = new bytes(1);
+ encoded[0] = bytes1(uint8(_len) + uint8(_offset));
+ } else {
+ uint256 lenLen;
+ uint256 i = 1;
+ while (_len / i != 0) {
+ lenLen++;
+ i *= 256;
+ }
+
+ encoded = new bytes(lenLen + 1);
+ encoded[0] = bytes1(uint8(lenLen) + uint8(_offset) + 55);
+ for (i = 1; i <= lenLen; i++) {
+ encoded[i] = bytes1(uint8((_len / (256 ** (lenLen - i))) % 256));
+ }
+ }
+
+ return encoded;
+ }
+
+ /**
+ * Encode integer in big endian binary form with no leading zeroes.
+ * @notice TODO: This should be optimized with assembly to save gas costs.
+ * @param _x The integer to encode.
+ * @return _binary RLP encoded bytes.
+ */
+ function _toBinary(
+ uint256 _x
+ ) private pure returns (bytes memory _binary) {
+ bytes memory b = abi.encodePacked(_x);
+
+ uint256 i = 0;
+ for (; i < 32; i++) {
+ if (b[i] != 0) {
+ break;
+ }
+ }
+
+ bytes memory res = new bytes(32 - i);
+ for (uint256 j = 0; j < res.length; j++) {
+ res[j] = b[i++];
+ }
+
+ return res;
+ }
+
+ /**
+ * Copies a piece of memory to another location.
+ * @notice From: https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol.
+ * @param _dest Destination location.
+ * @param _src Source location.
+ * @param _len Length of memory to copy.
+ */
+ function _memcpy(uint256 _dest, uint256 _src, uint256 _len) private pure {
+ uint256 dest = _dest;
+ uint256 src = _src;
+ uint256 len = _len;
+
+ for (; len >= 32; len -= 32) {
+ assembly {
+ mstore(dest, mload(src))
+ }
+ dest += 32;
+ src += 32;
+ }
+
+ uint256 mask = 256 ** (32 - len) - 1;
+ assembly {
+ let srcpart := and(mload(src), not(mask))
+ let destpart := and(mload(dest), mask)
+ mstore(dest, or(destpart, srcpart))
+ }
+ }
+
+ /**
+ * Flattens a list of byte strings into one byte string.
+ * @notice From: https://github.com/sammayo/solidity-rlp-encoder/blob/master/RLPEncode.sol.
+ * @param _list List of byte strings to flatten.
+ * @return _flattened The flattened byte string.
+ */
+ function _flatten(
+ bytes[] memory _list
+ ) private pure returns (bytes memory _flattened) {
+ if (_list.length == 0) {
+ return new bytes(0);
+ }
+
+ uint256 len;
+ uint256 i = 0;
+ for (; i < _list.length; i++) {
+ len += _list[i].length;
+ }
+
+ bytes memory flattened = new bytes(len);
+ uint256 flattenedPtr;
+ assembly {
+ flattenedPtr := add(flattened, 0x20)
+ }
+
+ for (i = 0; i < _list.length; i++) {
+ bytes memory item = _list[i];
+
+ uint256 listPtr;
+ assembly {
+ listPtr := add(item, 0x20)
+ }
+
+ _memcpy(flattenedPtr, listPtr, item.length);
+ flattenedPtr += _list[i].length;
+ }
+
+ return flattened;
+ }
+}
diff --git a/bolt-contracts/src/lib/SSZ.sol b/bolt-contracts/src/lib/ssz/SSZ.sol
similarity index 93%
rename from bolt-contracts/src/lib/SSZ.sol
rename to bolt-contracts/src/lib/ssz/SSZ.sol
index 057fbc4f9..754864277 100644
--- a/bolt-contracts/src/lib/SSZ.sol
+++ b/bolt-contracts/src/lib/ssz/SSZ.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
+pragma solidity 0.8.25;
/// @title SSZ library
/// @dev inspired by https://github.com/succinctlabs/telepathy-contracts/blob/main/src/libraries/SimpleSerialize.sol
@@ -15,11 +15,12 @@ library SSZ {
/// @notice Modified version of `verify` from `MerkleProofLib` to support generalized indices and sha256 precompile.
/// @dev Returns whether `leaf` exists in the Merkle tree with `root`, given `proof`.
/// @dev from https://github.com/madlabman/eip-4788-proof/blob/master/src/SSZ.sol
- function _verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, uint256 index)
- internal
- view
- returns (bool isValid)
- {
+ function _verifyProof(
+ bytes32[] calldata proof,
+ bytes32 root,
+ bytes32 leaf,
+ uint256 index
+ ) internal view returns (bool isValid) {
/// @solidity memory-safe-assembly
assembly {
if proof.length {
@@ -107,7 +108,9 @@ library SSZ {
/// @notice Converts a value to a little-endian byte array
/// @dev from https://github.com/succinctlabs/telepathy-contracts/blob/main/src/libraries/SimpleSerialize.sol
- function _toLittleEndian(uint256 v) internal pure returns (bytes32) {
+ function _toLittleEndian(
+ uint256 v
+ ) internal pure returns (bytes32) {
v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8)
| ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8);
v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16)
@@ -121,14 +124,18 @@ library SSZ {
}
/// @notice Converts a boolean to a little-endian byte array
- function _toLittleEndian(bool v) internal pure returns (bytes32) {
+ function _toLittleEndian(
+ bool v
+ ) internal pure returns (bytes32) {
return bytes32(v ? 1 << 248 : 0);
}
/// @notice Log base 2 of a number
/// @dev From solady FixedPointMath
/// @dev Equivalent to computing the index of the most significant bit (MSB) of `x`.
- function _log2(uint256 x) internal pure returns (uint256 r) {
+ function _log2(
+ uint256 x
+ ) internal pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
if iszero(x) {
diff --git a/bolt-contracts/src/lib/SSZContainers.sol b/bolt-contracts/src/lib/ssz/SSZContainers.sol
similarity index 90%
rename from bolt-contracts/src/lib/SSZContainers.sol
rename to bolt-contracts/src/lib/ssz/SSZContainers.sol
index d87742965..7d4302c7f 100644
--- a/bolt-contracts/src/lib/SSZContainers.sol
+++ b/bolt-contracts/src/lib/ssz/SSZContainers.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
+pragma solidity 0.8.25;
import {SSZ} from "./SSZ.sol";
@@ -28,7 +28,9 @@ library SSZContainers {
}
/// @notice Computes the hash tree root of a validator SSZ container
- function _validatorHashTreeRoot(Validator memory validator) internal view returns (bytes32) {
+ function _validatorHashTreeRoot(
+ Validator memory validator
+ ) internal view returns (bytes32) {
bytes32 pubkeyRoot;
uint256 _sha256 = SSZ.SHA256_PRECOMPILE;
@@ -57,7 +59,9 @@ library SSZContainers {
}
/// @notice Computes the hash tree root of a beacon block header SSZ container
- function _beaconHeaderHashTreeRoot(BeaconBlockHeader memory header) internal view returns (bytes32) {
+ function _beaconHeaderHashTreeRoot(
+ BeaconBlockHeader memory header
+ ) internal view returns (bytes32) {
bytes32[] memory nodes = new bytes32[](8);
nodes[0] = SSZ._toLittleEndian(header.slot);
nodes[1] = SSZ._toLittleEndian(header.proposerIndex);
@@ -72,7 +76,9 @@ library SSZContainers {
}
/// @notice Computes the hash tree root of an RLP-encoded signed transaction (raw bytes)
- function _transactionHashTreeRoot(bytes memory transaction) internal view returns (bytes32) {
+ function _transactionHashTreeRoot(
+ bytes memory transaction
+ ) internal view returns (bytes32) {
uint256 chunkCount = (transaction.length + 31) / 32;
bytes32[] memory nodes = new bytes32[](chunkCount);
diff --git a/bolt-contracts/src/lib/trie/MerkleTrie.sol b/bolt-contracts/src/lib/trie/MerkleTrie.sol
new file mode 100644
index 000000000..d861e5093
--- /dev/null
+++ b/bolt-contracts/src/lib/trie/MerkleTrie.sol
@@ -0,0 +1,761 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+// Credits: Optimism contributors.
+// Ref: https://github.com/ethereum-optimism/optimism/blob/05deae54595b0e6bdd33580de81cb9ad194898bc/packages/contracts-bedrock/src/libraries/trie/MerkleTrie.sol
+
+/* Library Imports */
+import {BytesUtils} from "../BytesUtils.sol";
+import {RLPReader} from "../rlp/RLPReader.sol";
+import {RLPWriter} from "../rlp/RLPWriter.sol";
+
+/**
+ * @title MerkleTrie
+ */
+library MerkleTrie {
+ /**
+ *
+ * Data Structures *
+ *
+ */
+ enum NodeType {
+ BranchNode,
+ ExtensionNode,
+ LeafNode
+ }
+
+ struct TrieNode {
+ bytes encoded;
+ RLPReader.RLPItem[] decoded;
+ }
+
+ /**
+ *
+ * Contract Constants *
+ *
+ */
+
+ // TREE_RADIX determines the number of elements per branch node.
+ uint256 constant TREE_RADIX = 16;
+ // Branch nodes have TREE_RADIX elements plus an additional `value` slot.
+ uint256 constant BRANCH_NODE_LENGTH = TREE_RADIX + 1;
+ // Leaf nodes and extension nodes always have two elements, a `path` and a `value`.
+ uint256 constant LEAF_OR_EXTENSION_NODE_LENGTH = 2;
+
+ // Prefixes are prepended to the `path` within a leaf or extension node and
+ // allow us to differentiate between the two node types. `ODD` or `EVEN` is
+ // determined by the number of nibbles within the unprefixed `path`. If the
+ // number of nibbles if even, we need to insert an extra padding nibble so
+ // the resulting prefixed `path` has an even number of nibbles.
+ uint8 constant PREFIX_EXTENSION_EVEN = 0;
+ uint8 constant PREFIX_EXTENSION_ODD = 1;
+ uint8 constant PREFIX_LEAF_EVEN = 2;
+ uint8 constant PREFIX_LEAF_ODD = 3;
+
+ // Just a utility constant. RLP represents `NULL` as 0x80.
+ bytes1 constant RLP_NULL = bytes1(0x80);
+ bytes constant RLP_NULL_BYTES = hex"80";
+ bytes32 internal constant KECCAK256_RLP_NULL_BYTES = keccak256(RLP_NULL_BYTES);
+
+ /**
+ *
+ * Internal Functions *
+ *
+ */
+
+ /**
+ * @notice Verifies a proof that a given key/value pair is present in the
+ * Merkle trie.
+ * @param _key Key of the node to search for, as a hex string.
+ * @param _value Value of the node to search for, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the desired node. Unlike
+ * traditional Merkle trees, this proof is executed top-down and consists
+ * of a list of RLP-encoded nodes that make a path down to the target node.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _verified `true` if the k/v pair exists in the trie, `false` otherwise.
+ */
+ function verifyInclusionProof(
+ bytes memory _key,
+ bytes memory _value,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _verified) {
+ (bool exists, bytes memory value) = get(_key, _proof, _root);
+
+ return (exists && BytesUtils.equal(_value, value));
+ }
+
+ /**
+ * @notice Verifies a proof that a given key is *not* present in
+ * the Merkle trie.
+ * @param _key Key of the node to search for, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the node *nearest* the
+ * target node.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _verified `true` if the key is absent in the trie, `false` otherwise.
+ */
+ function verifyExclusionProof(
+ bytes memory _key,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _verified) {
+ (bool exists,) = get(_key, _proof, _root);
+
+ return exists == false;
+ }
+
+ /**
+ * @notice Updates a Merkle trie and returns a new root hash.
+ * @param _key Key of the node to update, as a hex string.
+ * @param _value Value of the node to update, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the node *nearest* the
+ * target node. If the key exists, we can simply update the value.
+ * Otherwise, we need to modify the trie to handle the new k/v pair.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _updatedRoot Root hash of the newly constructed trie.
+ */
+ function update(
+ bytes memory _key,
+ bytes memory _value,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bytes32 _updatedRoot) {
+ // Special case when inserting the very first node.
+ if (_root == KECCAK256_RLP_NULL_BYTES) {
+ return getSingleNodeRootHash(_key, _value);
+ }
+
+ TrieNode[] memory proof = _parseProof(_proof);
+ (uint256 pathLength, bytes memory keyRemainder,) = _walkNodePath(proof, _key, _root);
+ TrieNode[] memory newPath = _getNewPath(proof, pathLength, keyRemainder, _value);
+
+ return _getUpdatedTrieRoot(newPath, _key);
+ }
+
+ /**
+ * @notice Retrieves the value associated with a given key.
+ * @param _key Key to search for, as hex bytes.
+ * @param _proof Merkle trie inclusion proof for the key.
+ * @param _root Known root of the Merkle trie.
+ * @return _exists Whether or not the key exists.
+ * @return _value Value of the key if it exists.
+ */
+ function get(
+ bytes memory _key,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _exists, bytes memory _value) {
+ TrieNode[] memory proof = _parseProof(_proof);
+ (uint256 pathLength, bytes memory keyRemainder, bool isFinalNode) = _walkNodePath(proof, _key, _root);
+
+ bool exists = keyRemainder.length == 0;
+
+ require(exists || isFinalNode, "Provided proof is invalid.");
+
+ bytes memory value = exists ? _getNodeValue(proof[pathLength - 1]) : bytes("");
+
+ return (exists, value);
+ }
+
+ /**
+ * Computes the root hash for a trie with a single node.
+ * @param _key Key for the single node.
+ * @param _value Value for the single node.
+ * @return _updatedRoot Hash of the trie.
+ */
+ function getSingleNodeRootHash(
+ bytes memory _key,
+ bytes memory _value
+ ) internal pure returns (bytes32 _updatedRoot) {
+ return keccak256(_makeLeafNode(BytesUtils.toNibbles(_key), _value).encoded);
+ }
+
+ /**
+ *
+ * Private Functions *
+ *
+ */
+
+ /**
+ * @notice Walks through a proof using a provided key.
+ * @param _proof Inclusion proof to walk through.
+ * @param _key Key to use for the walk.
+ * @param _root Known root of the trie.
+ * @return _pathLength Length of the final path
+ * @return _keyRemainder Portion of the key remaining after the walk.
+ * @return _isFinalNode Whether or not we've hit a dead end.
+ */
+ function _walkNodePath(
+ TrieNode[] memory _proof,
+ bytes memory _key,
+ bytes32 _root
+ ) private pure returns (uint256 _pathLength, bytes memory _keyRemainder, bool _isFinalNode) {
+ uint256 pathLength = 0;
+ bytes memory key = BytesUtils.toNibbles(_key);
+
+ bytes32 currentNodeID = _root;
+ uint256 currentKeyIndex = 0;
+ uint256 currentKeyIncrement = 0;
+ TrieNode memory currentNode;
+
+ // Proof is top-down, so we start at the first element (root).
+ for (uint256 i = 0; i < _proof.length; i++) {
+ currentNode = _proof[i];
+ currentKeyIndex += currentKeyIncrement;
+
+ // Keep track of the proof elements we actually need.
+ // It's expensive to resize arrays, so this simply reduces gas costs.
+ pathLength += 1;
+
+ if (currentKeyIndex == 0) {
+ // First proof element is always the root node.
+ require(keccak256(currentNode.encoded) == currentNodeID, "Invalid root hash");
+ } else if (currentNode.encoded.length >= 32) {
+ // Nodes 32 bytes or larger are hashed inside branch nodes.
+ require(keccak256(currentNode.encoded) == currentNodeID, "Invalid large internal hash");
+ } else {
+ // Nodes smaller than 31 bytes aren't hashed.
+ require(BytesUtils.toBytes32(currentNode.encoded) == currentNodeID, "Invalid internal node hash");
+ }
+
+ if (currentNode.decoded.length == BRANCH_NODE_LENGTH) {
+ if (currentKeyIndex == key.length) {
+ // We've hit the end of the key, meaning the value should be within this branch node.
+ break;
+ } else {
+ // We're not at the end of the key yet.
+ // Figure out what the next node ID should be and continue.
+ uint8 branchKey = uint8(key[currentKeyIndex]);
+ RLPReader.RLPItem memory nextNode = currentNode.decoded[branchKey];
+ currentNodeID = _getNodeID(nextNode);
+ currentKeyIncrement = 1;
+ continue;
+ }
+ } else if (currentNode.decoded.length == LEAF_OR_EXTENSION_NODE_LENGTH) {
+ bytes memory path = _getNodePath(currentNode);
+ uint8 prefix = uint8(path[0]);
+ uint8 offset = 2 - prefix % 2;
+ bytes memory pathRemainder = BytesUtils.slice(path, offset);
+ bytes memory keyRemainder = BytesUtils.slice(key, currentKeyIndex);
+ uint256 sharedNibbleLength = _getSharedNibbleLength(pathRemainder, keyRemainder);
+
+ if (prefix == PREFIX_LEAF_EVEN || prefix == PREFIX_LEAF_ODD) {
+ if (pathRemainder.length == sharedNibbleLength && keyRemainder.length == sharedNibbleLength) {
+ // The key within this leaf matches our key exactly.
+ // Increment the key index to reflect that we have no remainder.
+ currentKeyIndex += sharedNibbleLength;
+ }
+
+ // We've hit a leaf node, so our next node should be NULL.
+ currentNodeID = bytes32(RLP_NULL);
+ break;
+ } else if (prefix == PREFIX_EXTENSION_EVEN || prefix == PREFIX_EXTENSION_ODD) {
+ if (sharedNibbleLength == 0) {
+ // Our extension node doesn't share any part of our key.
+ // We've hit the end of this path, updates will need to modify this extension.
+ currentNodeID = bytes32(RLP_NULL);
+ break;
+ } else {
+ // Our extension shares some nibbles.
+ // Carry on to the next node.
+ currentNodeID = _getNodeID(currentNode.decoded[1]);
+ currentKeyIncrement = sharedNibbleLength;
+ continue;
+ }
+ } else {
+ revert("Received a node with an unknown prefix");
+ }
+ } else {
+ revert("Received an unparseable node.");
+ }
+ }
+
+ // If our node ID is NULL, then we're at a dead end.
+ bool isFinalNode = currentNodeID == bytes32(RLP_NULL);
+ return (pathLength, BytesUtils.slice(key, currentKeyIndex), isFinalNode);
+ }
+
+ /**
+ * @notice Creates new nodes to support a k/v pair insertion into a given
+ * Merkle trie path.
+ * @param _path Path to the node nearest the k/v pair.
+ * @param _pathLength Length of the path. Necessary because the provided
+ * path may include additional nodes (e.g., it comes directly from a proof)
+ * and we can't resize in-memory arrays without costly duplication.
+ * @param _keyRemainder Portion of the initial key that must be inserted
+ * into the trie.
+ * @param _value Value to insert at the given key.
+ * @return _newPath A new path with the inserted k/v pair and extra supporting nodes.
+ */
+ function _getNewPath(
+ TrieNode[] memory _path,
+ uint256 _pathLength,
+ bytes memory _keyRemainder,
+ bytes memory _value
+ ) private pure returns (TrieNode[] memory _newPath) {
+ bytes memory keyRemainder = _keyRemainder;
+
+ // Most of our logic depends on the status of the last node in the path.
+ TrieNode memory lastNode = _path[_pathLength - 1];
+ NodeType lastNodeType = _getNodeType(lastNode);
+
+ // Create an array for newly created nodes.
+ // We need up to three new nodes, depending on the contents of the last node.
+ // Since array resizing is expensive, we'll keep track of the size manually.
+ // We're using an explicit `totalNewNodes += 1` after insertions for clarity.
+ TrieNode[] memory newNodes = new TrieNode[](3);
+ uint256 totalNewNodes = 0;
+
+ if (keyRemainder.length == 0 && lastNodeType == NodeType.LeafNode) {
+ // We've found a leaf node with the given key.
+ // Simply need to update the value of the node to match.
+ newNodes[totalNewNodes] = _makeLeafNode(_getNodeKey(lastNode), _value);
+ totalNewNodes += 1;
+ } else if (lastNodeType == NodeType.BranchNode) {
+ if (keyRemainder.length == 0) {
+ // We've found a branch node with the given key.
+ // Simply need to update the value of the node to match.
+ newNodes[totalNewNodes] = _editBranchValue(lastNode, _value);
+ totalNewNodes += 1;
+ } else {
+ // We've found a branch node, but it doesn't contain our key.
+ // Reinsert the old branch for now.
+ newNodes[totalNewNodes] = lastNode;
+ totalNewNodes += 1;
+ // Create a new leaf node, slicing our remainder since the first byte points
+ // to our branch node.
+ newNodes[totalNewNodes] = _makeLeafNode(BytesUtils.slice(keyRemainder, 1), _value);
+ totalNewNodes += 1;
+ }
+ } else {
+ // Our last node is either an extension node or a leaf node with a different key.
+ bytes memory lastNodeKey = _getNodeKey(lastNode);
+ uint256 sharedNibbleLength = _getSharedNibbleLength(lastNodeKey, keyRemainder);
+
+ if (sharedNibbleLength != 0) {
+ // We've got some shared nibbles between the last node and our key remainder.
+ // We'll need to insert an extension node that covers these shared nibbles.
+ bytes memory nextNodeKey = BytesUtils.slice(lastNodeKey, 0, sharedNibbleLength);
+ newNodes[totalNewNodes] = _makeExtensionNode(nextNodeKey, _getNodeHash(_value));
+ totalNewNodes += 1;
+
+ // Cut down the keys since we've just covered these shared nibbles.
+ lastNodeKey = BytesUtils.slice(lastNodeKey, sharedNibbleLength);
+ keyRemainder = BytesUtils.slice(keyRemainder, sharedNibbleLength);
+ }
+
+ // Create an empty branch to fill in.
+ TrieNode memory newBranch = _makeEmptyBranchNode();
+
+ if (lastNodeKey.length == 0) {
+ // Key remainder was larger than the key for our last node.
+ // The value within our last node is therefore going to be shifted into
+ // a branch value slot.
+ newBranch = _editBranchValue(newBranch, _getNodeValue(lastNode));
+ } else {
+ // Last node key was larger than the key remainder.
+ // We're going to modify some index of our branch.
+ uint8 branchKey = uint8(lastNodeKey[0]);
+ // Move on to the next nibble.
+ lastNodeKey = BytesUtils.slice(lastNodeKey, 1);
+
+ if (lastNodeType == NodeType.LeafNode) {
+ // We're dealing with a leaf node.
+ // We'll modify the key and insert the old leaf node into the branch index.
+ TrieNode memory modifiedLastNode = _makeLeafNode(lastNodeKey, _getNodeValue(lastNode));
+ newBranch = _editBranchIndex(newBranch, branchKey, _getNodeHash(modifiedLastNode.encoded));
+ } else if (lastNodeKey.length != 0) {
+ // We're dealing with a shrinking extension node.
+ // We need to modify the node to decrease the size of the key.
+ TrieNode memory modifiedLastNode = _makeExtensionNode(lastNodeKey, _getNodeValue(lastNode));
+ newBranch = _editBranchIndex(newBranch, branchKey, _getNodeHash(modifiedLastNode.encoded));
+ } else {
+ // We're dealing with an unnecessary extension node.
+ // We're going to delete the node entirely.
+ // Simply insert its current value into the branch index.
+ newBranch = _editBranchIndex(newBranch, branchKey, _getNodeValue(lastNode));
+ }
+ }
+
+ if (keyRemainder.length == 0) {
+ // We've got nothing left in the key remainder.
+ // Simply insert the value into the branch value slot.
+ newBranch = _editBranchValue(newBranch, _value);
+ // Push the branch into the list of new nodes.
+ newNodes[totalNewNodes] = newBranch;
+ totalNewNodes += 1;
+ } else {
+ // We've got some key remainder to work with.
+ // We'll be inserting a leaf node into the trie.
+ // First, move on to the next nibble.
+ keyRemainder = BytesUtils.slice(keyRemainder, 1);
+ // Push the branch into the list of new nodes.
+ newNodes[totalNewNodes] = newBranch;
+ totalNewNodes += 1;
+ // Push a new leaf node for our k/v pair.
+ newNodes[totalNewNodes] = _makeLeafNode(keyRemainder, _value);
+ totalNewNodes += 1;
+ }
+ }
+
+ // Finally, join the old path with our newly created nodes.
+ // Since we're overwriting the last node in the path, we use `_pathLength - 1`.
+ return _joinNodeArrays(_path, _pathLength - 1, newNodes, totalNewNodes);
+ }
+
+ /**
+ * @notice Computes the trie root from a given path.
+ * @param _nodes Path to some k/v pair.
+ * @param _key Key for the k/v pair.
+ * @return _updatedRoot Root hash for the updated trie.
+ */
+ function _getUpdatedTrieRoot(
+ TrieNode[] memory _nodes,
+ bytes memory _key
+ ) private pure returns (bytes32 _updatedRoot) {
+ bytes memory key = BytesUtils.toNibbles(_key);
+
+ // Some variables to keep track of during iteration.
+ TrieNode memory currentNode;
+ NodeType currentNodeType;
+ bytes memory previousNodeHash;
+
+ // Run through the path backwards to rebuild our root hash.
+ for (uint256 i = _nodes.length; i > 0; i--) {
+ // Pick out the current node.
+ currentNode = _nodes[i - 1];
+ currentNodeType = _getNodeType(currentNode);
+
+ if (currentNodeType == NodeType.LeafNode) {
+ // Leaf nodes are already correctly encoded.
+ // Shift the key over to account for the nodes key.
+ bytes memory nodeKey = _getNodeKey(currentNode);
+ key = BytesUtils.slice(key, 0, key.length - nodeKey.length);
+ } else if (currentNodeType == NodeType.ExtensionNode) {
+ // Shift the key over to account for the nodes key.
+ bytes memory nodeKey = _getNodeKey(currentNode);
+ key = BytesUtils.slice(key, 0, key.length - nodeKey.length);
+
+ // If this node is the last element in the path, it'll be correctly encoded
+ // and we can skip this part.
+ if (previousNodeHash.length > 0) {
+ // Re-encode the node based on the previous node.
+ currentNode = _makeExtensionNode(nodeKey, previousNodeHash);
+ }
+ } else if (currentNodeType == NodeType.BranchNode) {
+ // If this node is the last element in the path, it'll be correctly encoded
+ // and we can skip this part.
+ if (previousNodeHash.length > 0) {
+ // Re-encode the node based on the previous node.
+ uint8 branchKey = uint8(key[key.length - 1]);
+ key = BytesUtils.slice(key, 0, key.length - 1);
+ currentNode = _editBranchIndex(currentNode, branchKey, previousNodeHash);
+ }
+ }
+
+ // Compute the node hash for the next iteration.
+ previousNodeHash = _getNodeHash(currentNode.encoded);
+ }
+
+ // Current node should be the root at this point.
+ // Simply return the hash of its encoding.
+ return keccak256(currentNode.encoded);
+ }
+
+ /**
+ * @notice Parses an RLP-encoded proof into something more useful.
+ * @param _proof RLP-encoded proof to parse.
+ * @return _parsed Proof parsed into easily accessible structs.
+ */
+ function _parseProof(
+ bytes memory _proof
+ ) private pure returns (TrieNode[] memory _parsed) {
+ RLPReader.RLPItem[] memory nodes = RLPReader.readList(_proof);
+ TrieNode[] memory proof = new TrieNode[](nodes.length);
+
+ for (uint256 i = 0; i < nodes.length; i++) {
+ bytes memory encoded = RLPReader.readBytes(nodes[i]);
+ proof[i] = TrieNode({encoded: encoded, decoded: RLPReader.readList(encoded)});
+ }
+
+ return proof;
+ }
+
+ /**
+ * @notice Picks out the ID for a node. Node ID is referred to as the
+ * "hash" within the specification, but nodes < 32 bytes are not actually
+ * hashed.
+ * @param _node Node to pull an ID for.
+ * @return _nodeID ID for the node, depending on the size of its contents.
+ */
+ function _getNodeID(
+ RLPReader.RLPItem memory _node
+ ) private pure returns (bytes32 _nodeID) {
+ bytes memory nodeID;
+
+ if (_node.length < 32) {
+ // Nodes smaller than 32 bytes are RLP encoded.
+ nodeID = RLPReader.readRawBytes(_node);
+ } else {
+ // Nodes 32 bytes or larger are hashed.
+ nodeID = RLPReader.readBytes(_node);
+ }
+
+ return BytesUtils.toBytes32(nodeID);
+ }
+
+ /**
+ * @notice Gets the path for a leaf or extension node.
+ * @param _node Node to get a path for.
+ * @return _path Node path, converted to an array of nibbles.
+ */
+ function _getNodePath(
+ TrieNode memory _node
+ ) private pure returns (bytes memory _path) {
+ return BytesUtils.toNibbles(RLPReader.readBytes(_node.decoded[0]));
+ }
+
+ /**
+ * @notice Gets the key for a leaf or extension node. Keys are essentially
+ * just paths without any prefix.
+ * @param _node Node to get a key for.
+ * @return _key Node key, converted to an array of nibbles.
+ */
+ function _getNodeKey(
+ TrieNode memory _node
+ ) private pure returns (bytes memory _key) {
+ return _removeHexPrefix(_getNodePath(_node));
+ }
+
+ /**
+ * @notice Gets the path for a node.
+ * @param _node Node to get a value for.
+ * @return _value Node value, as hex bytes.
+ */
+ function _getNodeValue(
+ TrieNode memory _node
+ ) private pure returns (bytes memory _value) {
+ return RLPReader.readBytes(_node.decoded[_node.decoded.length - 1]);
+ }
+
+ /**
+ * @notice Computes the node hash for an encoded node. Nodes < 32 bytes
+ * are not hashed, all others are keccak256 hashed.
+ * @param _encoded Encoded node to hash.
+ * @return _hash Hash of the encoded node. Simply the input if < 32 bytes.
+ */
+ function _getNodeHash(
+ bytes memory _encoded
+ ) private pure returns (bytes memory _hash) {
+ if (_encoded.length < 32) {
+ return _encoded;
+ } else {
+ return abi.encodePacked(keccak256(_encoded));
+ }
+ }
+
+ /**
+ * @notice Determines the type for a given node.
+ * @param _node Node to determine a type for.
+ * @return _type Type of the node; BranchNode/ExtensionNode/LeafNode.
+ */
+ function _getNodeType(
+ TrieNode memory _node
+ ) private pure returns (NodeType _type) {
+ if (_node.decoded.length == BRANCH_NODE_LENGTH) {
+ return NodeType.BranchNode;
+ } else if (_node.decoded.length == LEAF_OR_EXTENSION_NODE_LENGTH) {
+ bytes memory path = _getNodePath(_node);
+ uint8 prefix = uint8(path[0]);
+
+ if (prefix == PREFIX_LEAF_EVEN || prefix == PREFIX_LEAF_ODD) {
+ return NodeType.LeafNode;
+ } else if (prefix == PREFIX_EXTENSION_EVEN || prefix == PREFIX_EXTENSION_ODD) {
+ return NodeType.ExtensionNode;
+ }
+ }
+
+ revert("Invalid node type");
+ }
+
+ /**
+ * @notice Utility; determines the number of nibbles shared between two
+ * nibble arrays.
+ * @param _a First nibble array.
+ * @param _b Second nibble array.
+ * @return _shared Number of shared nibbles.
+ */
+ function _getSharedNibbleLength(bytes memory _a, bytes memory _b) private pure returns (uint256 _shared) {
+ uint256 i = 0;
+ while (_a.length > i && _b.length > i && _a[i] == _b[i]) {
+ i++;
+ }
+ return i;
+ }
+
+ /**
+ * @notice Utility; converts an RLP-encoded node into our nice struct.
+ * @param _raw RLP-encoded node to convert.
+ * @return _node Node as a TrieNode struct.
+ */
+ function _makeNode(
+ bytes[] memory _raw
+ ) private pure returns (TrieNode memory _node) {
+ bytes memory encoded = RLPWriter.writeList(_raw);
+
+ return TrieNode({encoded: encoded, decoded: RLPReader.readList(encoded)});
+ }
+
+ /**
+ * @notice Utility; converts an RLP-decoded node into our nice struct.
+ * @param _items RLP-decoded node to convert.
+ * @return _node Node as a TrieNode struct.
+ */
+ function _makeNode(
+ RLPReader.RLPItem[] memory _items
+ ) private pure returns (TrieNode memory _node) {
+ bytes[] memory raw = new bytes[](_items.length);
+ for (uint256 i = 0; i < _items.length; i++) {
+ raw[i] = RLPReader.readRawBytes(_items[i]);
+ }
+ return _makeNode(raw);
+ }
+
+ /**
+ * @notice Creates a new extension node.
+ * @param _key Key for the extension node, unprefixed.
+ * @param _value Value for the extension node.
+ * @return _node New extension node with the given k/v pair.
+ */
+ function _makeExtensionNode(bytes memory _key, bytes memory _value) private pure returns (TrieNode memory _node) {
+ bytes[] memory raw = new bytes[](2);
+ bytes memory key = _addHexPrefix(_key, false);
+ raw[0] = RLPWriter.writeBytes(BytesUtils.fromNibbles(key));
+ raw[1] = RLPWriter.writeBytes(_value);
+ return _makeNode(raw);
+ }
+
+ /**
+ * @notice Creates a new leaf node.
+ * @dev This function is essentially identical to `_makeExtensionNode`.
+ * Although we could route both to a single method with a flag, it's
+ * more gas efficient to keep them separate and duplicate the logic.
+ * @param _key Key for the leaf node, unprefixed.
+ * @param _value Value for the leaf node.
+ * @return _node New leaf node with the given k/v pair.
+ */
+ function _makeLeafNode(bytes memory _key, bytes memory _value) private pure returns (TrieNode memory _node) {
+ bytes[] memory raw = new bytes[](2);
+ bytes memory key = _addHexPrefix(_key, true);
+ raw[0] = RLPWriter.writeBytes(BytesUtils.fromNibbles(key));
+ raw[1] = RLPWriter.writeBytes(_value);
+ return _makeNode(raw);
+ }
+
+ /**
+ * @notice Creates an empty branch node.
+ * @return _node Empty branch node as a TrieNode struct.
+ */
+ function _makeEmptyBranchNode() private pure returns (TrieNode memory _node) {
+ bytes[] memory raw = new bytes[](BRANCH_NODE_LENGTH);
+ for (uint256 i = 0; i < raw.length; i++) {
+ raw[i] = RLP_NULL_BYTES;
+ }
+ return _makeNode(raw);
+ }
+
+ /**
+ * @notice Modifies the value slot for a given branch.
+ * @param _branch Branch node to modify.
+ * @param _value Value to insert into the branch.
+ * @return _updatedNode Modified branch node.
+ */
+ function _editBranchValue(
+ TrieNode memory _branch,
+ bytes memory _value
+ ) private pure returns (TrieNode memory _updatedNode) {
+ bytes memory encoded = RLPWriter.writeBytes(_value);
+ _branch.decoded[_branch.decoded.length - 1] = RLPReader.toRLPItem(encoded);
+ return _makeNode(_branch.decoded);
+ }
+
+ /**
+ * @notice Modifies a slot at an index for a given branch.
+ * @param _branch Branch node to modify.
+ * @param _index Slot index to modify.
+ * @param _value Value to insert into the slot.
+ * @return _updatedNode Modified branch node.
+ */
+ function _editBranchIndex(
+ TrieNode memory _branch,
+ uint8 _index,
+ bytes memory _value
+ ) private pure returns (TrieNode memory _updatedNode) {
+ bytes memory encoded = _value.length < 32 ? _value : RLPWriter.writeBytes(_value);
+ _branch.decoded[_index] = RLPReader.toRLPItem(encoded);
+ return _makeNode(_branch.decoded);
+ }
+
+ /**
+ * @notice Utility; adds a prefix to a key.
+ * @param _key Key to prefix.
+ * @param _isLeaf Whether or not the key belongs to a leaf.
+ * @return _prefixedKey Prefixed key.
+ */
+ function _addHexPrefix(bytes memory _key, bool _isLeaf) private pure returns (bytes memory _prefixedKey) {
+ uint8 prefix = _isLeaf ? uint8(0x02) : uint8(0x00);
+ uint8 offset = uint8(_key.length % 2);
+ bytes memory prefixed = new bytes(2 - offset);
+ prefixed[0] = bytes1(prefix + offset);
+ return abi.encodePacked(prefixed, _key);
+ }
+
+ /**
+ * @notice Utility; removes a prefix from a path.
+ * @param _path Path to remove the prefix from.
+ * @return _unprefixedKey Unprefixed key.
+ */
+ function _removeHexPrefix(
+ bytes memory _path
+ ) private pure returns (bytes memory _unprefixedKey) {
+ if (uint8(_path[0]) % 2 == 0) {
+ return BytesUtils.slice(_path, 2);
+ } else {
+ return BytesUtils.slice(_path, 1);
+ }
+ }
+
+ /**
+ * @notice Utility; combines two node arrays. Array lengths are required
+ * because the actual lengths may be longer than the filled lengths.
+ * Array resizing is extremely costly and should be avoided.
+ * @param _a First array to join.
+ * @param _aLength Length of the first array.
+ * @param _b Second array to join.
+ * @param _bLength Length of the second array.
+ * @return _joined Combined node array.
+ */
+ function _joinNodeArrays(
+ TrieNode[] memory _a,
+ uint256 _aLength,
+ TrieNode[] memory _b,
+ uint256 _bLength
+ ) private pure returns (TrieNode[] memory _joined) {
+ TrieNode[] memory ret = new TrieNode[](_aLength + _bLength);
+
+ // Copy elements from the first array.
+ for (uint256 i = 0; i < _aLength; i++) {
+ ret[i] = _a[i];
+ }
+
+ // Copy elements from the second array.
+ for (uint256 i = 0; i < _bLength; i++) {
+ ret[i + _aLength] = _b[i];
+ }
+
+ return ret;
+ }
+}
diff --git a/bolt-contracts/src/lib/trie/SecureMerkleTrie.sol b/bolt-contracts/src/lib/trie/SecureMerkleTrie.sol
new file mode 100644
index 000000000..d25b90aa9
--- /dev/null
+++ b/bolt-contracts/src/lib/trie/SecureMerkleTrie.sol
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+pragma experimental ABIEncoderV2;
+
+// Credits: Optimism contributors.
+// Ref: https://github.com/ethereum-optimism/optimism/blob/05deae54595b0e6bdd33580de81cb9ad194898bc/packages/contracts-bedrock/src/libraries/trie/SecureMerkleTrie.sol
+
+/* Library Imports */
+import {MerkleTrie} from "./MerkleTrie.sol";
+
+/**
+ * @title SecureMerkleTrie
+ */
+library SecureMerkleTrie {
+ /**
+ *
+ * Internal Functions *
+ *
+ */
+
+ /**
+ * @notice Verifies a proof that a given key/value pair is present in the
+ * Merkle trie.
+ * @param _key Key of the node to search for, as a hex string.
+ * @param _value Value of the node to search for, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the desired node. Unlike
+ * traditional Merkle trees, this proof is executed top-down and consists
+ * of a list of RLP-encoded nodes that make a path down to the target node.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _verified `true` if the k/v pair exists in the trie, `false` otherwise.
+ */
+ function verifyInclusionProof(
+ bytes memory _key,
+ bytes memory _value,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _verified) {
+ bytes memory key = _getSecureKey(_key);
+ return MerkleTrie.verifyInclusionProof(key, _value, _proof, _root);
+ }
+
+ /**
+ * @notice Verifies a proof that a given key is *not* present in
+ * the Merkle trie.
+ * @param _key Key of the node to search for, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the node *nearest* the
+ * target node.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _verified `true` if the key is not present in the trie, `false` otherwise.
+ */
+ function verifyExclusionProof(
+ bytes memory _key,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _verified) {
+ bytes memory key = _getSecureKey(_key);
+ return MerkleTrie.verifyExclusionProof(key, _proof, _root);
+ }
+
+ /**
+ * @notice Updates a Merkle trie and returns a new root hash.
+ * @param _key Key of the node to update, as a hex string.
+ * @param _value Value of the node to update, as a hex string.
+ * @param _proof Merkle trie inclusion proof for the node *nearest* the
+ * target node. If the key exists, we can simply update the value.
+ * Otherwise, we need to modify the trie to handle the new k/v pair.
+ * @param _root Known root of the Merkle trie. Used to verify that the
+ * included proof is correctly constructed.
+ * @return _updatedRoot Root hash of the newly constructed trie.
+ */
+ function update(
+ bytes memory _key,
+ bytes memory _value,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bytes32 _updatedRoot) {
+ bytes memory key = _getSecureKey(_key);
+ return MerkleTrie.update(key, _value, _proof, _root);
+ }
+
+ /**
+ * @notice Retrieves the value associated with a given key.
+ * @param _key Key to search for, as hex bytes.
+ * @param _proof Merkle trie inclusion proof for the key.
+ * @param _root Known root of the Merkle trie.
+ * @return _exists Whether or not the key exists.
+ * @return _value Value of the key if it exists.
+ */
+ function get(
+ bytes memory _key,
+ bytes memory _proof,
+ bytes32 _root
+ ) internal pure returns (bool _exists, bytes memory _value) {
+ bytes memory key = _getSecureKey(_key);
+ return MerkleTrie.get(key, _proof, _root);
+ }
+
+ /**
+ * Computes the root hash for a trie with a single node.
+ * @param _key Key for the single node.
+ * @param _value Value for the single node.
+ * @return _updatedRoot Hash of the trie.
+ */
+ function getSingleNodeRootHash(
+ bytes memory _key,
+ bytes memory _value
+ ) internal pure returns (bytes32 _updatedRoot) {
+ bytes memory key = _getSecureKey(_key);
+ return MerkleTrie.getSingleNodeRootHash(key, _value);
+ }
+
+ /**
+ *
+ * Private Functions *
+ *
+ */
+
+ /**
+ * Computes the secure counterpart to a key.
+ * @param _key Key to get a secure key from.
+ * @return _secureKey Secure version of the key.
+ */
+ function _getSecureKey(
+ bytes memory _key
+ ) private pure returns (bytes memory _secureKey) {
+ return abi.encodePacked(keccak256(_key));
+ }
+}
diff --git a/bolt-contracts/test/BoltChallenger.t.sol b/bolt-contracts/test/BoltChallenger.t.sol
index 6a8718eef..b8dd716bc 100644
--- a/bolt-contracts/test/BoltChallenger.t.sol
+++ b/bolt-contracts/test/BoltChallenger.t.sol
@@ -1,114 +1,600 @@
-// // SPDX-License-Identifier: MIT
-// pragma solidity ^0.8.13;
-
-// import {Test, console} from "forge-std/Test.sol";
-// import {BoltRegistry} from "../src/contracts/BoltRegistry.sol";
-// import {BoltChallenger} from "../src/contracts/BoltChallenger.sol";
-// import {IBoltChallenger} from "../src/interfaces/IBoltChallenger.sol";
-// import {BeaconChainUtils} from "../src/lib/BeaconChainUtils.sol";
-
-// contract BoltChallengerTest is Test {
-// BoltRegistry public registry;
-// BoltChallenger public challenger;
-
-// // Relic protocol contracts
-// address relicReliquary = 0x5E4DE6Bb8c6824f29c44Bd3473d44da120387d08;
-// address relicBlockHeaderProver = 0x9f9A1eb0CF9340538297c853915DCc06Eb6D72c4;
-// address relicAccountInfoProver = 0xf74105AE736Ca0C4B171a2EC4F1D4B0b6EBB99ae;
-// address beaconRootsContract = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
-
-// address public alice;
-// address public bob;
-
-// uint256 public alicePk;
-// uint256 public bobPk;
-
-// function setUp() public {
-// // Set up mainnet forking
-// vm.createSelectFork("https://cloudflare-eth.com", 19932764);
-// assertEq(block.number, 19932764);
-
-// (alice, alicePk) = makeAddrAndKey("alice");
-// (bob, bobPk) = makeAddrAndKey("bob");
-
-// registry = new BoltRegistry();
-// challenger =
-// new BoltChallenger(address(registry), relicReliquary, relicBlockHeaderProver, relicAccountInfoProver);
-
-// vm.deal(alice, 10 ether);
-// vm.deal(bob, 10 ether);
-// }
-
-// function testOpenChallengeConditions() public {
-// uint256 latestSlot = BeaconChainUtils._getSlotFromTimestamp(block.timestamp);
-
-// BoltChallenger.SignedCommitment memory sc = BoltChallenger.SignedCommitment({
-// slot: 1,
-// nonce: 1,
-// gasUsed: 21000,
-// transactionHash: bytes32("0x123"),
-// signedRawTransaction: "0x456",
-// signature: "0x789"
-// });
-
-// // TEST: bond is less than 1 ether
-// vm.expectRevert(IBoltChallenger.InsufficientBond.selector);
-// vm.prank(alice);
-// challenger.challengeProposer{value: 0.5 ether}(bob, sc);
-
-// // TEST: challenge opened too late
-// vm.expectRevert(IBoltChallenger.TargetSlotTooFarInThePast.selector);
-// vm.prank(alice);
-// challenger.challengeProposer{value: 1 ether}(bob, sc);
-
-// sc.slot = latestSlot;
-
-// // TEST: proposer address is authorized in the registry
-// vm.expectRevert(IBoltChallenger.InvalidProposerAddress.selector);
-// vm.prank(alice);
-// challenger.challengeProposer{value: 1 ether}(bob, sc);
-
-// vm.prank(bob);
-// registry.optIn();
-
-// // TEST: mocked commitment signature
-// vm.expectRevert(IBoltChallenger.InvalidCommitmentSignature.selector);
-// vm.prank(alice);
-// challenger.challengeProposer{value: 1 ether}(bob, sc);
-// }
-
-// function testOpenChallengeSignature() public {
-// uint256 latestSlot = BeaconChainUtils._getSlotFromTimestamp(block.timestamp);
-
-// vm.prank(bob);
-// registry.optIn();
-
-// bytes32 txHash = 0xbe162ae10f376ad2bcf0934233493c7b353836fc1d27c5cb6785ce68d45914ea;
-// bytes memory signedRawTx =
-// hex"02f87101830f45b3808504acefa159825208944675c7e5baafbffbca748158becba61ef3b0a26387cb62154da95e6480c080a0101d7785433fd38e12fccd911bf9e61a941c88543f372877f07901dacf066b0aa016a75077103f7e175b61b5509e20ef5e8364d322f2ecaade5922717efeb892cd";
-
-// bytes32 commitmentDigest = keccak256(abi.encodePacked(latestSlot, txHash, signedRawTx));
-// (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPk, commitmentDigest);
-// bytes memory commitmentSignature = abi.encodePacked(r, s, v);
-
-// BoltChallenger.SignedCommitment memory sc = BoltChallenger.SignedCommitment({
-// slot: latestSlot,
-// nonce: vm.getNonce(0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5),
-// gasUsed: 21000,
-// transactionHash: txHash,
-// signedRawTransaction: signedRawTx,
-// signature: commitmentSignature
-// });
-
-// // mock the beacon root for the target slot (todo: fix this)
-// bytes32 timestampIdx = bytes32(uint256(block.timestamp % 8191));
-// vm.store(beaconRootsContract, timestampIdx, bytes32(block.timestamp));
-// vm.store(beaconRootsContract, bytes32(uint256(timestampIdx) + 8191), bytes32(uint256(123)));
-
-// // TEST: challenge opened successfully
-// // Ignore beacon root verification for now as EIP-4788 is out of scope for this test
-// vm.expectRevert(BeaconChainUtils.BeaconRootNotFound.selector);
-// vm.prank(alice);
-// challenger.challengeProposer{value: 1 ether}(bob, sc);
-// }
-// }
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+import {Utils} from "./Utils.sol";
+
+import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
+
+import {BoltParametersV1} from "../src/contracts/BoltParametersV1.sol";
+import {BoltChallengerV1} from "../src/contracts/BoltChallengerV1.sol";
+import {BoltConfig} from "../src/lib/Config.sol";
+import {IBoltChallengerV1} from "../src/interfaces/IBoltChallengerV1.sol";
+import {RLPReader} from "../src/lib/rlp/RLPReader.sol";
+import {RLPWriter} from "../src/lib/rlp/RLPWriter.sol";
+import {BytesUtils} from "../src/lib/BytesUtils.sol";
+import {MerkleTrie} from "../src/lib/trie/MerkleTrie.sol";
+import {SecureMerkleTrie} from "../src/lib/trie/SecureMerkleTrie.sol";
+import {TransactionDecoder} from "../src/lib/TransactionDecoder.sol";
+
+// re-export the internal resolver function for testing
+contract BoltChallengerExt is BoltChallengerV1 {
+ function _resolveExt(
+ bytes32 _challengeID,
+ bytes32 _trustedBlockHash,
+ IBoltChallengerV1.Proof calldata _proof
+ ) external {
+ _resolve(_challengeID, _trustedBlockHash, _proof);
+ }
+
+ function _getCurrentSlotExt() external view returns (uint256) {
+ return _getCurrentSlot();
+ }
+
+ function _decodeBlockHeaderRLPExt(
+ bytes calldata _blockHeaderRLP
+ ) external pure returns (IBoltChallengerV1.BlockHeaderData memory) {
+ return _decodeBlockHeaderRLP(_blockHeaderRLP);
+ }
+}
+
+contract BoltChallengerTest is Test {
+ using RLPReader for bytes;
+ using RLPReader for RLPReader.RLPItem;
+ using BytesUtils for bytes;
+ using TransactionDecoder for TransactionDecoder.Transaction;
+ using TransactionDecoder for bytes;
+
+ BoltChallengerExt boltChallenger;
+
+ address admin = makeAddr("admin");
+ address challenger = makeAddr("challenger");
+ address resolver = makeAddr("resolver");
+
+ address target;
+ uint256 targetPK;
+
+ function setUp() public {
+ vm.pauseGasMetering();
+ (target, targetPK) = makeAddrAndKey("target");
+
+ BoltConfig.Parameters memory config = new Utils().readParameters();
+
+ BoltParametersV1 parameters = new BoltParametersV1();
+ parameters.initialize(
+ admin,
+ config.epochDuration,
+ config.slashingWindow,
+ config.maxChallengeDuration,
+ config.allowUnsafeRegistration,
+ config.challengeBond,
+ config.blockhashEvmLookback,
+ config.justificationDelay,
+ config.eth2GenesisTimestamp,
+ config.slotTime,
+ config.minimumOperatorStake
+ );
+
+ boltChallenger = new BoltChallengerExt();
+ boltChallenger.initialize(admin, address(parameters));
+
+ vm.deal(challenger, 100 ether);
+ vm.deal(resolver, 100 ether);
+ vm.roll(12_456_789);
+ vm.warp(1_726_564_072);
+ }
+
+ // =========== Proving data inclusion on-chain ===========
+
+ function testProveHeaderData() public {
+ // Note: In prod, how we obtain the trusted block hash would depend on the context.
+ // For recent blocks, we can simply use the blockhash function in the EVM.
+ 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/header_20785012.json");
+ bytes memory headerRLP = vm.parseJsonBytes(file, ".result");
+
+ assertEq(keccak256(headerRLP), trustedBlockHash);
+
+ // RLP decode the header
+ vm.resumeGasMetering();
+ IBoltChallengerV1.BlockHeaderData memory header = boltChallenger._decodeBlockHeaderRLPExt(headerRLP);
+ vm.pauseGasMetering();
+
+ assertEq(header.stateRoot, 0x214389f55a96edbd4d5295a17ada4dbc68a3b276145bf824b060635f9905cefc);
+ assertEq(header.txRoot, 0x87bb9183296ce9e3b7a3246f6d3a778b99a5d7daaba2174750707407c7297365);
+ assertEq(header.blockNumber, 20_785_012);
+ assertEq(header.timestamp, 1_726_753_391);
+ assertEq(header.baseFee, 21_575_309_588);
+ }
+
+ function testProveAccountData() public {
+ // The account we want to prove
+ address accountToProve = 0x0D9f5045B604bA0c050b5eb06D0b25d01c525Ea5;
+
+ // Note: in prod the state root should be obtained from the block header proof.
+ // this way we can trust it comes from the right block number. This comes from Mainnet block 20_728_344.
+ bytes32 stateRootAtBlock = 0x214389f55a96edbd4d5295a17ada4dbc68a3b276145bf824b060635f9905cefc;
+
+ // 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_20785012.json");
+ bytes[] memory accountProofJson = vm.parseJsonBytesArray(file, ".result.accountProof");
+ bytes memory accountProof = _RLPEncodeList(accountProofJson);
+
+ // Perform a sanity check to see if the state root matches the expected trie node
+ RLPReader.RLPItem[] memory nodes = RLPReader.readList(accountProof);
+ MerkleTrie.TrieNode[] memory proof = new MerkleTrie.TrieNode[](nodes.length);
+ for (uint256 i = 0; i < nodes.length; i++) {
+ bytes memory encoded = RLPReader.readBytes(nodes[i]);
+ proof[i] = MerkleTrie.TrieNode({encoded: encoded, decoded: RLPReader.readList(encoded)});
+ }
+ assertEq(keccak256(proof[0].encoded), stateRootAtBlock, "Roots should match");
+
+ vm.resumeGasMetering();
+ (bool exists, bytes memory accountRLP) =
+ SecureMerkleTrie.get(abi.encodePacked(accountToProve), accountProof, stateRootAtBlock);
+ vm.pauseGasMetering();
+ assertEq(exists, true);
+
+ // decode the account RLP into nonce and balance
+ RLPReader.RLPItem[] memory accountFields = accountRLP.toRLPItem().readList();
+ uint256 nonce = accountFields[0].readUint256();
+ uint256 balance = accountFields[1].readUint256();
+
+ assertEq(nonce, 236);
+ assertEq(balance, 136_481_368_234_605_997);
+ }
+
+ function testProveTransactionInclusion() public {
+ // The transaction we want to prove inclusion of
+ bytes32 txHash = 0x9ec2c56ca36e445a46bc77ca77510f0ef21795d00834269f3752cbd29d63ba1f;
+
+ // 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);
+
+ // The transactions root and index in the block, also included in the CLI response
+ bytes32 txRootAtBlock = vm.parseJsonBytes32(file, ".root");
+ uint256 txIndexInBlock = vm.parseJsonUint(file, ".index");
+
+ bytes memory key = RLPWriter.writeUint(txIndexInBlock);
+
+ vm.resumeGasMetering();
+ // Gotcha: SecureMerkleTrie.get expects the key to be hashed with keccak256
+ // but the transaction trie skips this step and uses the raw index as the key.
+ (bool exists, bytes memory transactionRLP) = MerkleTrie.get(key, txProof, txRootAtBlock);
+ vm.pauseGasMetering();
+
+ assertEq(exists, true);
+ assertEq(keccak256(transactionRLP), txHash);
+
+ // Decode the transaction RLP into its fields
+ TransactionDecoder.Transaction memory decodedTx = transactionRLP.decodeEnveloped();
+ assertEq(uint8(decodedTx.txType), 2);
+ assertEq(decodedTx.chainId, 1);
+ assertEq(decodedTx.nonce, 0xeb);
+ assertEq(decodedTx.maxPriorityFeePerGas, 0x73a20d00);
+ assertEq(decodedTx.maxFeePerGas, 0x7e172a822);
+ assertEq(decodedTx.gasLimit, 0x5208);
+ assertEq(decodedTx.to, 0x0ff71973B5243005b192D5BCF552Fc2532b7bdEc);
+ assertEq(decodedTx.value, 0x15842095ebc4000);
+ assertEq(decodedTx.data.length, 0);
+ assertEq(decodedTx.recoverSender(), 0x0D9f5045B604bA0c050b5eb06D0b25d01c525Ea5);
+ }
+
+ // =========== 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.
+ IBoltChallengerV1.SignedCommitment memory commitment = _parseTestCommitment();
+
+ // Reconstruct the commitment digest: `keccak( keccak(signed tx) || le_bytes(slot) )`
+ bytes32 commitmentID = _computeCommitmentID(commitment.signedTx, commitment.slot);
+
+ assertEq(commitmentID, 0x52ecc7832625c3d107aaba5b55d4509b48cd9f4f7ce375d6696d09bbf3310525);
+ assertEq(commitment.signature.length, 65);
+
+ // Verify the commitment signature against the digest
+ vm.resumeGasMetering();
+ address commitmentSigner = ECDSA.recover(commitmentID, commitment.signature);
+ assertEq(commitmentSigner, 0x27083ED52464625660f3e30Aa5B9C20A30D7E110);
+ vm.pauseGasMetering();
+ }
+
+ function testCommitmentSignature() public {
+ bytes memory signedTx = vm.parseJsonBytes(vm.readFile("./test/testdata/signed_tx_20785012_1.json"), ".raw");
+ uint64 slot = 20_728_344;
+
+ // Reconstruct the commitment digest
+ bytes32 commitmentID = _computeCommitmentID(signedTx, slot);
+
+ // Sign the commitment digest with the target
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(targetPK, commitmentID);
+ bytes memory commitmentSignature = abi.encodePacked(r, s, v);
+
+ // Verify the commitment signature against the digest
+ vm.resumeGasMetering();
+ address commitmentSigner = ECDSA.recover(commitmentID, commitmentSignature);
+ assertEq(commitmentSigner, target);
+ vm.pauseGasMetering();
+ }
+
+ // =========== Opening a challenge ===========
+
+ function testOpenChallengeSingleTx() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.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}(commitments);
+ vm.pauseGasMetering();
+
+ assertEq(challenger.balance, 99 ether);
+
+ // Check the challenge was opened
+ IBoltChallengerV1.Challenge[] memory challenges = boltChallenger.getAllChallenges();
+ assertEq(challenges.length, 1);
+
+ IBoltChallengerV1.Challenge memory challenge = challenges[0];
+ assertEq(challenge.openedAt, block.timestamp);
+ assertEq(uint256(challenge.status), 0);
+ assertEq(challenge.challenger, challenger);
+ assertEq(challenge.commitmentSigner, 0x27083ED52464625660f3e30Aa5B9C20A30D7E110);
+ assertEq(challenge.targetSlot, commitments[0].slot);
+ }
+
+ function testOpenChallengeWithIncorrectBond() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _parseTestCommitment();
+
+ // Open a challenge with insufficient bond
+ vm.resumeGasMetering();
+ vm.prank(challenger);
+ vm.expectRevert(IBoltChallengerV1.IncorrectChallengeBond.selector);
+ boltChallenger.openChallenge{value: 0.1 ether}(commitments);
+ vm.pauseGasMetering();
+ }
+
+ function testOpenChallengeWithLargebond() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _parseTestCommitment();
+
+ // Open a challenge with a large bond, making sure that the rest is refunded
+ vm.resumeGasMetering();
+ vm.prank(challenger);
+ vm.expectRevert(IBoltChallengerV1.IncorrectChallengeBond.selector);
+ boltChallenger.openChallenge{value: 50 ether}(commitments);
+ vm.pauseGasMetering();
+
+ assertEq(challenger.balance, 100 ether);
+ }
+
+ function testOpenAlreadyExistingChallenge() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _parseTestCommitment();
+
+ // Open a challenge
+ vm.prank(challenger);
+ boltChallenger.openChallenge{value: 1 ether}(commitments);
+
+ // Try to open the same challenge again
+ vm.resumeGasMetering();
+ vm.prank(challenger);
+ vm.expectRevert(IBoltChallengerV1.ChallengeAlreadyExists.selector);
+ boltChallenger.openChallenge{value: 1 ether}(commitments);
+ vm.pauseGasMetering();
+ }
+
+ function testOpenChallengeWithSlotInTheFuture() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _parseTestCommitment();
+
+ commitments[0].slot = uint64(boltChallenger._getCurrentSlotExt()) + 10;
+
+ // Open a challenge with a slot in the future
+ vm.resumeGasMetering();
+ vm.prank(challenger);
+ vm.expectRevert(IBoltChallengerV1.BlockIsNotFinalized.selector);
+ boltChallenger.openChallenge{value: 1 ether}(commitments);
+ vm.pauseGasMetering();
+ }
+
+ function testOpenChallengeInvalidSignature() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _parseTestCommitment();
+
+ // Modify the signature to make it invalid
+ 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}(commitments);
+ vm.pauseGasMetering();
+ }
+
+ // =========== Resolving a challenge ===========
+
+ function testResolveChallengeFullDefenseSingleTx() 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.
+
+ uint256 inclusionBlockNumber = 20_785_012;
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.SignedCommitment[](1);
+ commitments[0] = _createRecentBoltCommitment(inclusionBlockNumber, 1);
+
+ // Open a challenge
+ vm.prank(challenger);
+ boltChallenger.openChallenge{value: 1 ether}(commitments);
+
+ // Get the challenge ID
+ IBoltChallengerV1.Challenge[] memory challenges = boltChallenger.getAllChallenges();
+ assertEq(challenges.length, 1);
+ bytes32 challengeID = challenges[0].id;
+
+ 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");
+
+ IBoltChallengerV1.Proof memory proof = IBoltChallengerV1.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(txProof, ".root"));
+
+ bytes32 trustedPreviousBlockHash = 0x6be050fe1f6c7ffe8f30a350250a9ecc08ff3c031d129f65e1c10e5119d7a28b;
+
+ // Resolve the challenge
+ vm.resumeGasMetering();
+ vm.prank(resolver);
+ vm.expectEmit();
+
+ emit IBoltChallengerV1.ChallengeDefended(challengeID);
+ boltChallenger._resolveExt(challengeID, trustedPreviousBlockHash, proof);
+ }
+
+ 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.
+ //
+ // 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;
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.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++) {
+ 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++) {
+ bytes32 cid = _computeCommitmentID(commitments[i].signedTx, commitments[i].slot);
+ address signer = ECDSA.recover(cid, commitments[i].signature);
+ assertEq(signer, target);
+ }
+
+ // Open a challenge
+ vm.prank(challenger);
+ boltChallenger.openChallenge{value: 1 ether}(commitments);
+
+ // Get the challenge ID
+ IBoltChallengerV1.Challenge[] memory challenges = boltChallenger.getAllChallenges();
+ 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[](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[](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");
+
+ IBoltChallengerV1.Proof memory proof = IBoltChallengerV1.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);
+
+ vm.expectEmit();
+ emit IBoltChallengerV1.ChallengeDefended(challengeID);
+
+ boltChallenger._resolveExt(challengeID, trustedPreviousBlockHash, proof);
+ }
+
+ function testResolveExpiredChallenge() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.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
+ IBoltChallengerV1.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);
+
+ // Get the challenge
+ IBoltChallengerV1.Challenge memory challenge = boltChallenger.getAllChallenges()[0];
+
+ vm.expectEmit();
+ emit IBoltChallengerV1.ChallengeBreached(challenge.id);
+
+ boltChallenger.resolveExpiredChallenge(challenge.id);
+ }
+
+ function testCannotResolveChallengeBeforeExpiration() public {
+ IBoltChallengerV1.SignedCommitment[] memory commitments = new IBoltChallengerV1.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
+ IBoltChallengerV1.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(IBoltChallengerV1.ChallengeNotExpired.selector);
+ boltChallenger.resolveExpiredChallenge(id);
+ vm.pauseGasMetering();
+ }
+
+ // =========== Helper functions ===========
+
+ // Helper to create a test commitment with a recent slot, valid for a recent challenge
+ function _createRecentBoltCommitment(
+ uint256 blockNumber,
+ uint256 id
+ ) internal view returns (IBoltChallengerV1.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(boltChallenger._getCurrentSlotExt() - 100);
+
+ // sign the new commitment with the target's private key
+ bytes32 commitmentID = _computeCommitmentID(commitment.signedTx, commitment.slot);
+ (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);
+
+ return commitment;
+ }
+
+ // Helper to parse the test commitment from a file
+ function _parseTestCommitment() internal view returns (IBoltChallengerV1.SignedCommitment memory) {
+ string memory file = vm.readFile("./test/testdata/bolt_commitment.json");
+ IBoltChallengerV1.SignedCommitment memory commitment = IBoltChallengerV1.SignedCommitment({
+ slot: uint64(vm.parseJsonUint(file, ".slot")),
+ signature: vm.parseJsonBytes(file, ".signature"),
+ signedTx: vm.parseJsonBytes(file, ".tx")
+ });
+
+ // Normalize v to 27 or 28
+ if (uint8(commitment.signature[64]) < 27) {
+ commitment.signature[64] = bytes1(uint8(commitment.signature[64]) + 0x1B);
+ }
+
+ return commitment;
+ }
+
+ // Helper to compute the commitment ID
+ function _computeCommitmentID(bytes memory signedTx, uint64 slot) internal pure returns (bytes32) {
+ return keccak256(abi.encodePacked(keccak256(signedTx), _toLittleEndian(slot)));
+ }
+
+ // Helper to encode a list of bytes[] into an RLP list with each item RLP-encoded
+ function _RLPEncodeList(
+ bytes[] memory _items
+ ) internal pure returns (bytes memory) {
+ bytes[] memory encodedItems = new bytes[](_items.length);
+ for (uint256 i = 0; i < _items.length; i++) {
+ encodedItems[i] = RLPWriter.writeBytes(_items[i]);
+ }
+ return RLPWriter.writeList(encodedItems);
+ }
+
+ // Helper to convert a u64 to a 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;
+ }
+}
diff --git a/bolt-contracts/test/BoltManager.EigenLayer.t.sol b/bolt-contracts/test/BoltManager.EigenLayer.t.sol
new file mode 100644
index 000000000..4a815704f
--- /dev/null
+++ b/bolt-contracts/test/BoltManager.EigenLayer.t.sol
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+
+import {BoltValidatorsV1} from "../src/contracts/BoltValidatorsV1.sol";
+import {BoltManagerV1} from "../src/contracts/BoltManagerV1.sol";
+import {BoltParametersV1} from "../src/contracts/BoltParametersV1.sol";
+import {BoltEigenLayerMiddlewareV1} from "../src/contracts/BoltEigenLayerMiddlewareV1.sol";
+import {BoltConfig} from "../src/lib/Config.sol";
+import {IBoltValidatorsV1} from "../src/interfaces/IBoltValidatorsV1.sol";
+import {IBoltManagerV1} from "../src/interfaces/IBoltManagerV1.sol";
+import {IBoltMiddlewareV1} from "../src/interfaces/IBoltMiddlewareV1.sol";
+import {Utils} from "./Utils.sol";
+
+import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol";
+import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol";
+import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol";
+import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol";
+import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol";
+import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol";
+import {EigenLayerDeployer} from "../test/fixtures/EigenLayerDeployer.f.sol";
+
+import {BLS12381} from "../src/lib/bls/BLS12381.sol";
+
+contract BoltManagerEigenLayerTest is Test {
+ using BLS12381 for BLS12381.G1Point;
+
+ uint48 public constant EPOCH_DURATION = 1 days;
+
+ BoltValidatorsV1 public validators;
+ BoltManagerV1 public manager;
+ BoltEigenLayerMiddlewareV1 public middleware;
+ EigenLayerDeployer public eigenLayerDeployer;
+
+ uint128 public constant PRECONF_MAX_GAS_LIMIT = 5_000_000;
+
+ address staker = makeAddr("staker");
+ address validator = makeAddr("validator");
+ BLS12381.G1Point validatorPubkey = BLS12381.generatorG1();
+ address operator;
+ uint256 operatorSk;
+
+ address admin = makeAddr("admin");
+
+ function setUp() public {
+ // Set-up accounts
+ (operator, operatorSk) = makeAddrAndKey("operator");
+
+ // Deploy EigenLayer contracts.
+ // This also deploy a `weth` token and `wethStrat` strategy base available as properties of the contract.
+ eigenLayerDeployer = new EigenLayerDeployer(staker);
+ eigenLayerDeployer.setUp();
+
+ BoltConfig.Parameters memory config = new Utils().readParameters();
+
+ BoltParametersV1 parameters = new BoltParametersV1();
+ parameters.initialize(
+ admin,
+ config.epochDuration,
+ config.slashingWindow,
+ config.maxChallengeDuration,
+ config.allowUnsafeRegistration,
+ config.challengeBond,
+ config.blockhashEvmLookback,
+ config.justificationDelay,
+ config.eth2GenesisTimestamp,
+ config.slotTime,
+ config.minimumOperatorStake
+ );
+
+ // Deploy Bolt contracts
+ validators = new BoltValidatorsV1();
+ validators.initialize(admin, address(parameters));
+ manager = new BoltManagerV1();
+ manager.initialize(admin, address(parameters), address(validators));
+ middleware = new BoltEigenLayerMiddlewareV1();
+
+ middleware.initialize(
+ address(admin),
+ address(parameters),
+ address(manager),
+ address(eigenLayerDeployer.avsDirectory()),
+ address(eigenLayerDeployer.delegationManager()),
+ address(eigenLayerDeployer.strategyManager())
+ );
+
+ // Register the middleware in the manager
+ vm.startPrank(admin);
+ manager.addRestakingProtocol(address(middleware));
+ vm.stopPrank();
+ }
+
+ function _adminRoutine() internal {
+ // PART 0: Admin setup -- Collateral whitelist
+ // vm.startPrank(admin);
+ // middleware.addWhitelistedCollateral(address(eigenLayerDeployer.weth()));
+ // vm.stopPrank();
+ // assertEq(middleware.getWhitelistedCollaterals().length, 1);
+ // assertEq(middleware.getWhitelistedCollaterals()[0], address(eigenLayerDeployer.weth()));
+ }
+
+ function _eigenLayerOptInRoutine() internal {
+ _adminRoutine();
+
+ // PART 1: External EigenLayer opt-in to BOLT AVS
+
+ // 1. As a staker, I deposit some LSTs into a Stategy via the StrategyManager.depositIntoStrategy function.
+ // After this, I get back some shares that I can use at a later time for withdrawal
+
+ vm.startPrank(staker);
+ eigenLayerDeployer.weth().approve(address(eigenLayerDeployer.strategyManager()), 1 ether);
+ uint256 shares = eigenLayerDeployer.strategyManager().depositIntoStrategy(
+ eigenLayerDeployer.wethStrat(), eigenLayerDeployer.weth(), 1 ether
+ );
+ vm.stopPrank();
+ assertEq(eigenLayerDeployer.wethStrat().sharesToUnderlyingView(shares), 1 ether);
+
+ // 2. As a Operator, I register myself into EigenLayer using DelegationManager.registerAsOperator.
+ // Note that this function doesn’t require specifying anything related to the service I’m going to provide.
+ // However, a parameter describes who can delegate to me whether it can be anyone or a subset of stakers.
+
+ IDelegationManager.OperatorDetails memory operatorDetails =
+ IDelegationManager.OperatorDetails(address(0), address(0), 0);
+ vm.startPrank(operator);
+ eigenLayerDeployer.delegationManager().registerAsOperator(operatorDetails, "https://boltprotocol.xyz");
+ vm.stopPrank();
+
+ // 3. As a staker, I can start delegating funds to these operators using
+ // the DelegationManager.delegateTo function and specifying to who I wish
+ // to delegate my funds
+
+ // NOTE: this signature is not used since the operator allows funds delegated from anyone
+ ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry(bytes(""), 0);
+ console.logAddress(eigenLayerDeployer.delegationManager().delegatedTo(staker));
+ vm.startPrank(staker);
+ eigenLayerDeployer.delegationManager().delegateTo(operator, signature, bytes32(0));
+ vm.stopPrank();
+ assertEq(eigenLayerDeployer.delegationManager().delegatedTo(staker), operator);
+
+ // 4. As an AVS developer I create an entrypoint contract.
+ // Upon deploying the contract it is required to make a call to EL’s
+ // AVSDirectory.updateAVSMetadataURI which takes just a string which is a URI.
+ // Note that his is not stored anywhere, just an log is emitted.
+ // Note that msg.sender which is the ServiceManager contract is used to identify the AVS itself
+
+ vm.prank(admin);
+ middleware.updateAVSMetadataURI("https://boltprotocol.xyz");
+
+ // 5. As a operator, I can now opt-in into an AVS by interacting with the ServiceManager.
+ // Two steps happen:
+ // i. I call the AVS’ ServiceManager.registerOperatorToAVS. The payload is a signature whose digest consists of:
+ // a. my operator address
+ // b. the AVS’ ServiceManager contract address
+ // c. a salt
+ // d. an expiry
+ // ii. The contract forwards the call to the AVSDirectory.registerOperatorToAVS to
+ // that msg.sender is the AVS contract. Upon successful verification of the signature,
+ // the operator is considered REGISTERED in a mapping avsOperatorStatus[msg.sender][operator].
+
+ // Calculate the digest hash
+ bytes32 operatorRegistrationDigestHash = eigenLayerDeployer.avsDirectory()
+ .calculateOperatorAVSRegistrationDigestHash({
+ operator: operator,
+ avs: address(middleware),
+ salt: bytes32(0),
+ expiry: UINT256_MAX
+ });
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(operatorSk, operatorRegistrationDigestHash);
+ bytes memory operatorRawSignature = abi.encodePacked(r, s, v);
+ ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature =
+ ISignatureUtils.SignatureWithSaltAndExpiry(operatorRawSignature, bytes32(0), UINT256_MAX);
+ vm.expectEmit(true, true, true, true);
+ emit IAVSDirectory.OperatorAVSRegistrationStatusUpdated(
+ operator, address(middleware), IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED
+ );
+
+ vm.prank(operator);
+ middleware.registerOperator("https://bolt-rpc.io", operatorSignature);
+
+ assertEq(manager.isOperatorEnabled(operator), true);
+
+ // PART 2: Validator and proposer opt into BOLT manager
+ //
+ // 1. --- Register Validator in BoltValidators ---
+
+ // pubkeys aren't checked, any point will be fine
+ validatorPubkey = BLS12381.generatorG1();
+
+ vm.prank(validator);
+ validators.registerValidatorUnsafe(validatorPubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ assertEq(validators.getValidatorByPubkey(validatorPubkey).exists, true);
+ assertEq(validators.getValidatorByPubkey(validatorPubkey).authorizedOperator, operator);
+
+ // 2. --- Operator and strategy registration into BoltManager (middleware) ---
+
+ vm.startPrank(admin);
+ middleware.registerStrategy(address(eigenLayerDeployer.wethStrat()));
+ vm.stopPrank();
+ assertEq(middleware.isStrategyEnabled(address(eigenLayerDeployer.wethStrat())), true);
+ }
+
+ function testDeregisterOperatorFromAVS() public {
+ _eigenLayerOptInRoutine();
+ vm.prank(operator);
+ middleware.deregisterOperator();
+ vm.expectRevert(IBoltManagerV1.OperatorNotRegistered.selector);
+ manager.isOperatorEnabled(operator);
+ }
+
+ function testGetOperatorStake() public {
+ _eigenLayerOptInRoutine();
+
+ uint256 amount = middleware.getOperatorStake(operator, address(eigenLayerDeployer.weth()));
+ uint256 totalStake = manager.getTotalStake(address(eigenLayerDeployer.weth()));
+ assertEq(amount, 1 ether);
+ assertEq(totalStake, 1 ether);
+ }
+
+ function testProposerStatus() public {
+ _eigenLayerOptInRoutine();
+
+ bytes32 pubkeyHash = _pubkeyHash(validatorPubkey);
+
+ IBoltValidatorsV1.ProposerStatus memory status = manager.getProposerStatus(pubkeyHash);
+ assertEq(status.pubkeyHash, pubkeyHash);
+ assertEq(status.operator, operator);
+ assertEq(status.active, true);
+ assertEq(status.collaterals.length, 1);
+ assertEq(status.amounts.length, 1);
+ assertEq(status.collaterals[0], address(eigenLayerDeployer.weth()));
+ assertEq(status.amounts[0], 1 ether);
+ }
+
+ function testProposersLookaheadStatus() public {
+ // This also opts in the operator which is needed
+ _eigenLayerOptInRoutine();
+ bytes32[] memory pubkeyHashes = new bytes32[](10);
+
+ // register 10 proposers with random pubkeys
+ for (uint256 i = 0; i < 10; i++) {
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+ pubkey.x[0] = pubkey.x[0] + i + 2;
+ pubkey.y[0] = pubkey.y[0] + i + 2;
+
+ pubkeyHashes[i] = _pubkeyHash(pubkey);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ }
+
+ IBoltValidatorsV1.ProposerStatus[] memory statuses = manager.getProposerStatuses(pubkeyHashes);
+ assertEq(statuses.length, 10);
+ }
+
+ function testNonExistentProposerStatus() public {
+ _eigenLayerOptInRoutine();
+
+ bytes32 pubkeyHash = bytes32(uint256(1));
+
+ vm.expectRevert(IBoltValidatorsV1.ValidatorDoesNotExist.selector);
+ manager.getProposerStatus(pubkeyHash);
+ }
+
+ /// @notice Compute the hash of a BLS public key
+ function _pubkeyHash(
+ BLS12381.G1Point memory _pubkey
+ ) internal pure returns (bytes32) {
+ uint256[2] memory compressedPubKey = _pubkey.compress();
+ return keccak256(abi.encodePacked(compressedPubKey));
+ }
+}
diff --git a/bolt-contracts/test/BoltManager.Symbiotic.t.sol b/bolt-contracts/test/BoltManager.Symbiotic.t.sol
new file mode 100644
index 000000000..d64492646
--- /dev/null
+++ b/bolt-contracts/test/BoltManager.Symbiotic.t.sol
@@ -0,0 +1,373 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+
+import {INetworkRegistry} from "@symbiotic/interfaces/INetworkRegistry.sol";
+import {IOperatorRegistry} from "@symbiotic/interfaces/IOperatorRegistry.sol";
+import {IVaultFactory} from "@symbiotic/interfaces/IVaultFactory.sol";
+import {IVault} from "@symbiotic/interfaces/vault/IVault.sol";
+import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol";
+import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol";
+import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol";
+import {IMetadataService} from "@symbiotic/interfaces/service/IMetadataService.sol";
+import {INetworkRestakeDelegator} from "@symbiotic/interfaces/delegator/INetworkRestakeDelegator.sol";
+import {INetworkMiddlewareService} from "@symbiotic/interfaces/service/INetworkMiddlewareService.sol";
+import {ISlasherFactory} from "@symbiotic/interfaces/ISlasherFactory.sol";
+import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol";
+import {IDelegatorFactory} from "@symbiotic/interfaces/IDelegatorFactory.sol";
+import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFactory.sol";
+import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol";
+import {SimpleCollateral} from "@symbiotic/../test/mocks/SimpleCollateral.sol";
+
+import {IBoltValidatorsV1} from "../src/interfaces/IBoltValidatorsV1.sol";
+import {IBoltMiddlewareV1} from "../src/interfaces/IBoltMiddlewareV1.sol";
+
+import {BoltParametersV1} from "../src/contracts/BoltParametersV1.sol";
+import {BoltValidatorsV1} from "../src/contracts/BoltValidatorsV1.sol";
+import {BoltManagerV1} from "../src/contracts/BoltManagerV1.sol";
+import {BoltSymbioticMiddlewareV1} from "../src/contracts/BoltSymbioticMiddlewareV1.sol";
+import {BLS12381} from "../src/lib/bls/BLS12381.sol";
+import {BoltConfig} from "../src/lib/Config.sol";
+import {Utils} from "./Utils.sol";
+
+import {SymbioticSetupFixture} from "./fixtures/SymbioticSetup.f.sol";
+
+contract BoltManagerSymbioticTest is Test {
+ using BLS12381 for BLS12381.G1Point;
+ using Subnetwork for address;
+
+ uint48 public constant EPOCH_DURATION = 1 days;
+ uint48 public constant SLASHING_WINDOW = 7 days;
+
+ uint128 public constant PRECONF_MAX_GAS_LIMIT = 5_000_000;
+
+ BoltValidatorsV1 public validators;
+ BoltManagerV1 public manager;
+ BoltSymbioticMiddlewareV1 public middleware;
+
+ IVaultFactory public vaultFactory;
+ IDelegatorFactory public delegatorFactory;
+ ISlasherFactory public slasherFactory;
+ INetworkRegistry public networkRegistry;
+ IOperatorRegistry public operatorRegistry;
+ IMetadataService public operatorMetadataService;
+ IMetadataService public networkMetadataService;
+ INetworkMiddlewareService public networkMiddlewareService;
+ IOptInService public operatorVaultOptInService;
+ IOptInService public operatorNetworkOptInService;
+ IVetoSlasher public vetoSlasher;
+ IVault public vault;
+ INetworkRestakeDelegator public networkRestakeDelegator;
+ IVaultConfigurator public vaultConfigurator;
+ SimpleCollateral public collateral;
+
+ address deployer = makeAddr("deployer");
+ address admin = makeAddr("admin");
+ address provider = makeAddr("provider");
+ address operator = makeAddr("operator");
+ address validator = makeAddr("validator");
+ address networkAdmin = makeAddr("networkAdmin");
+ address vaultAdmin = makeAddr("vaultAdmin");
+ address user = makeAddr("user");
+
+ uint96 subnetworkId = 0;
+ bytes32 subnetwork = networkAdmin.subnetwork(subnetworkId);
+
+ function setUp() public {
+ // fast forward a few days to avoid timestamp underflows
+ vm.warp(block.timestamp + SLASHING_WINDOW * 3);
+
+ // --- Deploy Symbiotic contracts ---
+ (
+ vaultFactory,
+ delegatorFactory,
+ slasherFactory,
+ networkRegistry,
+ operatorRegistry,
+ operatorMetadataService,
+ networkMetadataService,
+ networkMiddlewareService,
+ operatorVaultOptInService,
+ operatorNetworkOptInService,
+ vaultConfigurator,
+ collateral
+ ) = new SymbioticSetupFixture().setUp(deployer, admin);
+
+ // --- Create vault ---
+
+ address[] memory adminRoleHolders = new address[](1);
+ adminRoleHolders[0] = vaultAdmin;
+
+ IVaultConfigurator.InitParams memory vaultConfiguratorInitParams = IVaultConfigurator.InitParams({
+ version: IMigratablesFactory(vaultConfigurator.VAULT_FACTORY()).lastVersion(),
+ owner: vaultAdmin,
+ vaultParams: IVault.InitParams({
+ collateral: address(collateral),
+ delegator: address(0),
+ slasher: address(0),
+ burner: address(0xdead),
+ epochDuration: EPOCH_DURATION,
+ depositWhitelist: false,
+ isDepositLimit: false,
+ depositLimit: 0,
+ defaultAdminRoleHolder: vaultAdmin,
+ depositWhitelistSetRoleHolder: vaultAdmin,
+ depositorWhitelistRoleHolder: vaultAdmin,
+ isDepositLimitSetRoleHolder: vaultAdmin,
+ depositLimitSetRoleHolder: vaultAdmin
+ }),
+ delegatorIndex: 0, // Use NetworkRestakeDelegator
+ delegatorParams: abi.encode(
+ INetworkRestakeDelegator.InitParams({
+ baseParams: IBaseDelegator.BaseParams({
+ defaultAdminRoleHolder: vaultAdmin,
+ hook: address(0), // we don't need a hook
+ hookSetRoleHolder: vaultAdmin
+ }),
+ networkLimitSetRoleHolders: adminRoleHolders,
+ operatorNetworkSharesSetRoleHolders: adminRoleHolders
+ })
+ ),
+ withSlasher: true,
+ slasherIndex: 1, // Use VetoSlasher
+ slasherParams: abi.encode(
+ IVetoSlasher.InitParams({
+ // veto duration must be smaller than epoch duration
+ vetoDuration: uint48(12 hours),
+ resolverSetEpochsDelay: 3
+ })
+ )
+ });
+
+ (address vault_, address networkRestakeDelegator_, address vetoSlasher_) =
+ vaultConfigurator.create(vaultConfiguratorInitParams);
+ vault = IVault(vault_);
+ networkRestakeDelegator = INetworkRestakeDelegator(networkRestakeDelegator_);
+ vetoSlasher = IVetoSlasher(vetoSlasher_);
+
+ assertEq(address(networkRestakeDelegator), address(vault.delegator()));
+ assertEq(address(vetoSlasher), address(vault.slasher()));
+ assertEq(address(vault.collateral()), address(collateral));
+ assertEq(vault.epochDuration(), EPOCH_DURATION);
+
+ // --- Deploy Bolt contracts ---
+
+ BoltConfig.Parameters memory config = new Utils().readParameters();
+
+ BoltParametersV1 parameters = new BoltParametersV1();
+ parameters.initialize(
+ admin,
+ config.epochDuration,
+ config.slashingWindow,
+ config.maxChallengeDuration,
+ config.allowUnsafeRegistration,
+ config.challengeBond,
+ config.blockhashEvmLookback,
+ config.justificationDelay,
+ config.eth2GenesisTimestamp,
+ config.slotTime,
+ config.minimumOperatorStake
+ );
+
+ validators = new BoltValidatorsV1();
+ validators.initialize(admin, address(parameters));
+ manager = new BoltManagerV1();
+ manager.initialize(admin, address(parameters), address(validators));
+
+ middleware = new BoltSymbioticMiddlewareV1();
+
+ middleware.initialize(
+ admin,
+ address(parameters),
+ address(manager),
+ networkAdmin,
+ address(operatorRegistry),
+ address(operatorNetworkOptInService),
+ address(vaultFactory)
+ );
+
+ // --- Whitelist collateral in BoltSymbioticMiddleware ---
+ vm.startPrank(admin);
+ middleware.registerVault(address(vault));
+ manager.addRestakingProtocol(address(middleware));
+ vm.stopPrank();
+ }
+
+ /// @notice Internal helper to register Symbiotic contracts and opt-in operators and vaults.
+ /// Should be called inside other tests that need a common setup beyond the default setUp().
+ function _symbioticOptInRoutine() internal {
+ // --- Register Network and Middleware in Symbiotic ---
+
+ vm.prank(networkAdmin);
+ networkRegistry.registerNetwork();
+
+ vm.prank(networkAdmin);
+ networkMiddlewareService.setMiddleware(address(middleware));
+
+ // --- Register Validator in BoltValidators ---
+
+ // pubkeys aren't checked, any point will be fine
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+
+ vm.prank(validator);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ assertEq(validators.getValidatorByPubkey(pubkey).exists, true);
+ assertEq(validators.getValidatorByPubkey(pubkey).authorizedOperator, operator);
+
+ // --- Register Operator in Symbiotic, opt-in network and vault ---
+
+ vm.prank(operator);
+ operatorRegistry.registerOperator();
+ assertEq(operatorRegistry.isEntity(operator), true);
+
+ vm.prank(operator);
+ operatorNetworkOptInService.optIn(networkAdmin);
+ assertEq(operatorNetworkOptInService.isOptedIn(operator, networkAdmin), true);
+
+ vm.prank(operator);
+ operatorVaultOptInService.optIn(address(vault));
+ assertEq(operatorVaultOptInService.isOptedIn(operator, address(vault)), true);
+
+ // --- Register Vault and Operator in BoltManager (middleware) ---
+ assertEq(middleware.isVaultEnabled(address(vault)), true);
+
+ vm.prank(operator);
+ middleware.registerOperator("https://bolt-rpc.io");
+ assertEq(manager.isOperatorEnabled(operator), true);
+
+ // --- Set the stake limit for the Vault ---
+
+ vm.prank(networkAdmin);
+ networkRestakeDelegator.setMaxNetworkLimit(subnetworkId, 10 ether);
+
+ vm.prank(vaultAdmin);
+ networkRestakeDelegator.setNetworkLimit(subnetwork, 2 ether);
+
+ // --- Add stake to the Vault ---
+
+ vm.prank(provider);
+ SimpleCollateral(collateral).mint(1 ether);
+
+ vm.prank(provider);
+ SimpleCollateral(collateral).approve(address(vault), 1 ether);
+
+ // deposit collateral from "provider" on behalf of "operator"
+ vm.prank(provider);
+ (uint256 depositedAmount, uint256 mintedShares) = vault.deposit(operator, 1 ether);
+
+ assertEq(depositedAmount, 1 ether);
+ assertEq(mintedShares, 1 ether);
+ assertEq(vault.balanceOf(operator), 1 ether);
+ assertEq(SimpleCollateral(collateral).balanceOf(address(vault)), 1 ether);
+ }
+
+ /// @notice Compute the hash of a BLS public key
+ function _pubkeyHash(
+ BLS12381.G1Point memory pubkey
+ ) internal pure returns (bytes32) {
+ uint256[2] memory compressedPubKey = pubkey.compress();
+ return keccak256(abi.encodePacked(compressedPubKey));
+ }
+
+ function testReadOperatorStake() public {
+ _symbioticOptInRoutine();
+
+ // --- Read the operator stake ---
+
+ // initial state
+ uint256 shares = networkRestakeDelegator.totalOperatorNetworkShares(subnetwork);
+ uint256 stakeFromDelegator = networkRestakeDelegator.stake(subnetwork, operator);
+ uint256 stakeFromMiddleware = middleware.getOperatorStake(operator, address(collateral));
+ assertEq(shares, 0);
+ assertEq(stakeFromMiddleware, stakeFromDelegator);
+ assertEq(stakeFromMiddleware, 0);
+
+ vm.warp(block.timestamp + EPOCH_DURATION + 1);
+ assertEq(vault.currentEpoch(), 1);
+
+ // after an epoch has passed
+ assertEq(vault.totalStake(), 1 ether);
+ assertEq(vault.activeStake(), 1 ether);
+ assertEq(vault.activeBalanceOf(operator), 1 ether);
+ assertEq(vault.activeSharesAt(uint48(0), ""), 0);
+ assertEq(vault.activeSharesAt(uint48(block.timestamp), ""), 1 ether);
+
+ // there still aren't any shares minted on the delegator
+ assertEq(networkRestakeDelegator.totalOperatorNetworkShares(subnetwork), 0);
+ assertEq(networkRestakeDelegator.operatorNetworkShares(subnetwork, operator), 0);
+
+ // we need to mint shares from the vault admin to activate stake
+ // for the operator in the subnetwork.
+ vm.prank(vaultAdmin);
+ networkRestakeDelegator.setOperatorNetworkShares(subnetwork, operator, 100);
+ assertEq(networkRestakeDelegator.totalOperatorNetworkShares(subnetwork), 100);
+ assertEq(networkRestakeDelegator.operatorNetworkShares(subnetwork, operator), 100);
+
+ vm.warp(block.timestamp + EPOCH_DURATION + 1);
+ assertEq(vault.currentEpoch(), 2);
+
+ // it takes 2 epochs to activate the stake
+ stakeFromDelegator = networkRestakeDelegator.stake(subnetwork, operator);
+ stakeFromMiddleware = middleware.getOperatorStake(operator, address(collateral));
+ assertEq(stakeFromDelegator, stakeFromMiddleware);
+ assertEq(stakeFromMiddleware, 1 ether);
+ }
+
+ function testGetProposerStatus() public {
+ _symbioticOptInRoutine();
+
+ // we need to mint shares from the vault admin to activate stake
+ // for the operator in the subnetwork.
+ vm.prank(vaultAdmin);
+ networkRestakeDelegator.setOperatorNetworkShares(subnetwork, operator, 100);
+ assertEq(networkRestakeDelegator.totalOperatorNetworkShares(subnetwork), 100);
+ assertEq(networkRestakeDelegator.operatorNetworkShares(subnetwork, operator), 100);
+
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+ bytes32 pubkeyHash = _pubkeyHash(pubkey);
+
+ vm.warp(block.timestamp + EPOCH_DURATION * 2 + 1);
+ assertEq(vault.currentEpoch(), 2);
+
+ IBoltValidatorsV1.ProposerStatus memory status = manager.getProposerStatus(pubkeyHash);
+ assertEq(status.pubkeyHash, pubkeyHash);
+ assertEq(status.operator, operator);
+ assertEq(status.active, true);
+ assertEq(status.collaterals.length, 1);
+ assertEq(status.amounts.length, 1);
+ assertEq(status.collaterals[0], address(collateral));
+ assertEq(status.amounts[0], 1 ether);
+ }
+
+ function testProposersLookaheadStatus() public {
+ _symbioticOptInRoutine();
+
+ bytes32[] memory pubkeyHashes = new bytes32[](10);
+
+ // register 10 proposers with random pubkeys
+ for (uint256 i = 0; i < 10; i++) {
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+ pubkey.x[0] = pubkey.x[0] + i + 2;
+ pubkey.y[0] = pubkey.y[0] + i + 2;
+
+ pubkeyHashes[i] = _pubkeyHash(pubkey);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ }
+
+ vm.warp(block.timestamp + EPOCH_DURATION * 2 + 1);
+ assertEq(vault.currentEpoch(), 2);
+
+ IBoltValidatorsV1.ProposerStatus[] memory statuses = manager.getProposerStatuses(pubkeyHashes);
+ assertEq(statuses.length, 10);
+ }
+
+ function testGetNonExistentProposerStatus() public {
+ _symbioticOptInRoutine();
+
+ bytes32 pubkeyHash = bytes32(uint256(1));
+
+ vm.expectRevert(IBoltValidatorsV1.ValidatorDoesNotExist.selector);
+ manager.getProposerStatus(pubkeyHash);
+ }
+}
diff --git a/bolt-contracts/test/BoltRegistry.t.sol b/bolt-contracts/test/BoltRegistry.t.sol
deleted file mode 100644
index 9280a2c67..000000000
--- a/bolt-contracts/test/BoltRegistry.t.sol
+++ /dev/null
@@ -1,91 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.13;
-
-import {Test, console} from "forge-std/Test.sol";
-import {BoltRegistry} from "../src/contracts/BoltRegistry.sol";
-import {IBoltRegistry} from "../src/interfaces/IBoltRegistry.sol";
-
-contract BoltRegistryTest is Test {
- BoltRegistry public registry;
-
- uint64[] public validatorIndexes;
-
- address alice = address(0x1);
- address bob = address(0x2);
-
- function setUp() public {
- registry = new BoltRegistry(10 ether);
- vm.deal(alice, 20 ether);
- }
-
- function testRegistration() public {
- vm.prank(alice);
-
- validatorIndexes.push(1);
- validatorIndexes.push(2);
- validatorIndexes.push(3);
-
- registry.register{value: 10 ether}(validatorIndexes, "rpc", "");
-
- assertEq(
- uint8(registry.getOperatorStatus(alice)),
- uint8(IBoltRegistry.Status.ACTIVE)
- );
-
- assertEq(registry.isActiveOperator(alice), true);
-
- assertEq(registry.getOperatorForValidator(1).operator, alice);
- assertEq(registry.getOperatorForValidator(1).metadata.rpc, "rpc");
- }
-
- // function testOptOut() public {
- // assertEq(registry.isActiveBasedProposer(alice), false);
- // vm.expectRevert(IBoltRegistry.BasedProposerDoesNotExist.selector);
- // registry.getBasedProposerStatus(alice);
-
- // vm.prank(alice);
- // registry.optIn();
- // assertEq(registry.isActiveBasedProposer(alice), true);
- // assertEq(
- // uint8(registry.getBasedProposerStatus(alice)),
- // uint8(IBoltRegistry.BoltStatus.Active)
- // );
-
- // vm.prank(alice);
- // registry.beginOptOut();
-
- // assertEq(registry.isActiveBasedProposer(alice), true);
- // assertEq(
- // uint8(registry.getBasedProposerStatus(alice)),
- // uint8(IBoltRegistry.BoltStatus.Active)
- // );
-
- // // check that confirmation can't be done immediately
- // vm.expectRevert(IBoltRegistry.CooldownNotElapsed.selector);
- // vm.prank(alice);
- // registry.confirmOptOut();
-
- // // wait 1 day
- // vm.warp(block.timestamp + 1 days);
-
- // // check that opt out can be confirmed
- // vm.prank(alice);
- // vm.expectEmit(address(registry));
- // emit IBoltRegistry.BasedProposerStatusChanged(
- // alice,
- // IBoltRegistry.BoltStatus.Inactive
- // );
- // registry.confirmOptOut();
-
- // assertEq(registry.isActiveBasedProposer(alice), false);
- // assertEq(
- // uint8(registry.getBasedProposerStatus(alice)),
- // uint8(IBoltRegistry.BoltStatus.Inactive)
- // );
-
- // // check that opt out can't be confirmed again
- // vm.expectRevert(IBoltRegistry.InvalidStatusChange.selector);
- // vm.prank(alice);
- // registry.confirmOptOut();
- // }
-}
diff --git a/bolt-contracts/test/BoltValidators.t.sol b/bolt-contracts/test/BoltValidators.t.sol
new file mode 100644
index 000000000..b8b169090
--- /dev/null
+++ b/bolt-contracts/test/BoltValidators.t.sol
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+
+import {BoltParametersV1} from "../src/contracts/BoltParametersV1.sol";
+import {BoltValidatorsV1} from "../src/contracts/BoltValidatorsV1.sol";
+import {IBoltValidatorsV1} from "../src/interfaces/IBoltValidatorsV1.sol";
+import {BLS12381} from "../src/lib/bls/BLS12381.sol";
+import {BoltConfig} from "../src/lib/Config.sol";
+import {Utils} from "./Utils.sol";
+
+contract BoltValidatorsTest is Test {
+ using BLS12381 for BLS12381.G1Point;
+
+ BoltParametersV1 public parameters;
+ BoltValidatorsV1 public validators;
+
+ uint128 public constant PRECONF_MAX_GAS_LIMIT = 5_000_000;
+
+ address admin = makeAddr("admin");
+ address provider = makeAddr("provider");
+ address operator = makeAddr("operator");
+ address validator = makeAddr("validator");
+
+ function setUp() public {
+ BoltConfig.Parameters memory config = new Utils().readParameters();
+
+ parameters = new BoltParametersV1();
+ parameters.initialize(
+ admin,
+ config.epochDuration,
+ config.slashingWindow,
+ config.maxChallengeDuration,
+ config.allowUnsafeRegistration,
+ config.challengeBond,
+ config.blockhashEvmLookback,
+ config.justificationDelay,
+ config.eth2GenesisTimestamp,
+ config.slotTime,
+ config.minimumOperatorStake
+ );
+
+ validators = new BoltValidatorsV1();
+ validators.initialize(admin, address(parameters));
+ }
+
+ function testUnsafeRegistration() public {
+ // pubkeys aren't checked, any point will be fine
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+
+ vm.prank(validator);
+ validators.registerValidatorUnsafe(pubkey, 1_000_000, operator);
+
+ BoltValidatorsV1.Validator memory registered = validators.getValidatorByPubkey(pubkey);
+ assertEq(registered.exists, true);
+ assertEq(registered.maxCommittedGasLimit, 1_000_000);
+ assertEq(registered.authorizedOperator, operator);
+ assertEq(registered.controller, validator);
+ }
+
+ function testUnsafeRegistrationFailsIfAlreadyRegistered() public {
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+
+ vm.prank(validator);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+
+ vm.prank(validator);
+ vm.expectRevert(IBoltValidatorsV1.ValidatorAlreadyExists.selector);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ }
+
+ function testUnsafeRegistrationWhenNotAllowed() public {
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+
+ vm.prank(admin);
+ parameters.setAllowUnsafeRegistration(false);
+
+ vm.prank(validator);
+ vm.expectRevert(IBoltValidatorsV1.UnsafeRegistrationNotAllowed.selector);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, operator);
+ }
+
+ function testUnsafeRegistrationInvalidOperator() public {
+ BLS12381.G1Point memory pubkey = BLS12381.generatorG1();
+
+ vm.prank(validator);
+ vm.expectRevert(IBoltValidatorsV1.InvalidAuthorizedOperator.selector);
+ validators.registerValidatorUnsafe(pubkey, PRECONF_MAX_GAS_LIMIT, address(0));
+ }
+}
diff --git a/bolt-contracts/test/StringToUintArrayLib.t.sol b/bolt-contracts/test/StringToUintArrayLib.t.sol
deleted file mode 100644
index aaae88073..000000000
--- a/bolt-contracts/test/StringToUintArrayLib.t.sol
+++ /dev/null
@@ -1,82 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.0;
-
-import "forge-std/Test.sol";
-import "../script/RegisterValidators.s.sol";
-
-contract StringToUintArrayTest is Test {
-
- function setUp() public {}
-
- function testParseValidatorIndexes1() public pure {
- uint256[] memory indexes = StringToUintArrayLib.fromStr("1,2,3,4");
- uint256[4] memory expected;
- expected[0] = 1;
- expected[1] = 2;
- expected[2] = 3;
- expected[3] = 4;
-
- assertEq(indexes.length, expected.length);
- for (uint256 i = 0; i < indexes.length; i++) {
- assertEq(indexes[i], expected[i]);
- }
- }
-
- function testParseValidatorIndexes2() public pure {
- uint256[] memory indexes = StringToUintArrayLib.fromStr("1..4");
- uint256[4] memory expected;
- expected[0] = 1;
- expected[1] = 2;
- expected[2] = 3;
- expected[3] = 4;
-
- assertEq(indexes.length, expected.length);
- for (uint256 i = 0; i < indexes.length; i++) {
- assertEq(indexes[i], expected[i]);
- }
- }
-
- function testParseValidatorIndexes3() public pure {
- uint256[] memory indexes = StringToUintArrayLib.fromStr("1..4,6..8");
- uint256[7] memory expected;
- expected[0] = 1;
- expected[1] = 2;
- expected[2] = 3;
- expected[3] = 4;
- expected[4] = 6;
- expected[5] = 7;
- expected[6] = 8;
-
- assertEq(indexes.length, expected.length);
- for (uint256 i = 0; i < indexes.length; i++) {
- assertEq(indexes[i], expected[i]);
- }
- }
-
- function testParseValidatorIndexes4() public pure {
- uint256[] memory indexes = StringToUintArrayLib.fromStr("1,2..4,6..8");
- uint256[7] memory expected;
- expected[0] = 1;
- expected[1] = 2;
- expected[2] = 3;
- expected[3] = 4;
- expected[4] = 6;
- expected[5] = 7;
- expected[6] = 8;
-
- assertEq(indexes.length, expected.length);
- for (uint256 i = 0; i < indexes.length; i++) {
- assertEq(indexes[i], expected[i]);
- }
- }
-
- function testParse100Indexes() public pure {
- string memory input = "1..100";
-
- uint256[] memory indexes = StringToUintArrayLib.fromStr(input);
- assertEq(indexes.length, 100);
- for (uint256 i = 0; i < indexes.length; i++) {
- assertEq(indexes[i], i + 1);
- }
- }
-}
\ No newline at end of file
diff --git a/bolt-contracts/test/TransactionDecoder.t.sol b/bolt-contracts/test/TransactionDecoder.t.sol
new file mode 100644
index 000000000..3f911f85b
--- /dev/null
+++ b/bolt-contracts/test/TransactionDecoder.t.sol
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+
+import {TransactionDecoder} from "../src/lib/TransactionDecoder.sol";
+import {BytesUtils} from "../src/lib/BytesUtils.sol";
+
+// We use a contract to expose internal library functions
+contract DecoderImpl {
+ function decodeEnveloped(
+ bytes memory raw
+ ) public pure returns (TransactionDecoder.Transaction memory) {
+ return TransactionDecoder.decodeEnveloped(raw);
+ }
+
+ function preimage(
+ TransactionDecoder.Transaction memory transaction
+ ) public pure returns (bytes32) {
+ return TransactionDecoder.preimage(transaction);
+ }
+
+ function signature(
+ TransactionDecoder.Transaction memory transaction
+ ) public pure returns (bytes memory) {
+ return TransactionDecoder.signature(transaction);
+ }
+}
+
+contract TransactionDecoderTest is Test {
+ using TransactionDecoder for TransactionDecoder.Transaction;
+
+ DecoderImpl decoder;
+
+ struct TestCase {
+ string name;
+ uint256 privateKey;
+ bytes unsignedLegacy;
+ bytes unsignedEip155;
+ bytes unsignedBerlin;
+ bytes unsignedLondon;
+ bytes unsignedCancun;
+ bytes signedLegacy;
+ bytes signedEip155;
+ bytes signedBerlin;
+ bytes signedLondon;
+ bytes signedCancun;
+ TransactionDecoder.Transaction transaction;
+ }
+
+ function setUp() public {
+ vm.pauseGasMetering();
+ decoder = new DecoderImpl();
+ }
+
+ function testDecodeAllTestCases() public {
+ uint256 i = 0;
+ while (true) {
+ string memory path = _getTestCasePath(i);
+ if (!vm.isFile(path)) break;
+
+ // Cycle through all test cases and run them one by one
+ _decodeTestCase(i);
+ i++;
+ }
+ }
+
+ function testDecodeGasUsage() public {
+ // Decode a single transaction to measure gas usage
+ bytes memory encoded = _readTestCase(0).signedLondon;
+
+ vm.resumeGasMetering();
+ decoder.decodeEnveloped(encoded);
+ vm.pauseGasMetering();
+ }
+
+ function _decodeTestCase(
+ uint256 id
+ ) internal view {
+ TestCase memory testCase = _readTestCase(id);
+
+ // Type 0 pre eip-155 (with chainId = 0)
+ TransactionDecoder.Transaction memory decodedSignedLegacy = decoder.decodeEnveloped(testCase.signedLegacy);
+ _assertTransaction(TransactionDecoder.TxType.Legacy, decodedSignedLegacy, testCase.transaction, false);
+ assertEq(decodedSignedLegacy.unsigned(), testCase.unsignedLegacy);
+ assertEq(decodedSignedLegacy.recoverSender(), vm.addr(testCase.privateKey));
+
+ // Type 0 post eip-155 (with optional legacy chainId)
+ TransactionDecoder.Transaction memory decodedSignedEip155 = decoder.decodeEnveloped(testCase.signedEip155);
+ _assertTransaction(TransactionDecoder.TxType.Legacy, decodedSignedEip155, testCase.transaction, true);
+ assertEq(decodedSignedEip155.unsigned(), testCase.unsignedEip155);
+ assertEq(decodedSignedEip155.recoverSender(), vm.addr(testCase.privateKey));
+
+ // Type 1 with optional access list
+ TransactionDecoder.Transaction memory decodedSignedBerlin = decoder.decodeEnveloped(testCase.signedBerlin);
+ _assertTransaction(TransactionDecoder.TxType.Eip2930, decodedSignedBerlin, testCase.transaction, true);
+ assertEq(decodedSignedBerlin.unsigned(), testCase.unsignedBerlin);
+ assertEq(decodedSignedBerlin.recoverSender(), vm.addr(testCase.privateKey));
+
+ // Type 2 with fee market fields
+ TransactionDecoder.Transaction memory decodedSignedLondon = decoder.decodeEnveloped(testCase.signedLondon);
+ _assertTransaction(TransactionDecoder.TxType.Eip1559, decodedSignedLondon, testCase.transaction, true);
+ assertEq(decodedSignedLondon.unsigned(), testCase.unsignedLondon);
+ assertEq(decodedSignedLondon.recoverSender(), vm.addr(testCase.privateKey));
+
+ // Type 3 with blob fields
+ TransactionDecoder.Transaction memory decodedSignedCancun = decoder.decodeEnveloped(testCase.signedCancun);
+ _assertTransaction(TransactionDecoder.TxType.Eip4844, decodedSignedCancun, testCase.transaction, true);
+ assertEq(decodedSignedCancun.unsigned(), testCase.unsignedCancun);
+ assertEq(decodedSignedCancun.recoverSender(), vm.addr(testCase.privateKey));
+ }
+
+ // Helper to get the path of a test case file based on its index
+ function _getTestCasePath(
+ uint256 id
+ ) internal pure returns (string memory) {
+ // Location of the test cases on disk (relative to the project root)
+ // Example: ./test/testdata/transactions/random_10.json
+ return string.concat("./test/testdata/transactions/random_", vm.toString(id), ".json");
+ }
+
+ function _assertTransaction(
+ TransactionDecoder.TxType txType,
+ TransactionDecoder.Transaction memory decoded,
+ TransactionDecoder.Transaction memory expected,
+ bool isEip155
+ ) internal pure {
+ assertEq(uint8(decoded.txType), uint8(txType));
+
+ if (!isEip155) {
+ // Pre-EIP-155 transactions have a chainId of 0
+ assertEq(decoded.chainId, 0);
+ } else {
+ assertEq(decoded.chainId, expected.chainId);
+ }
+
+ assertEq(decoded.data, expected.data);
+ assertEq(decoded.gasLimit, expected.gasLimit);
+ assertEq(decoded.nonce, expected.nonce);
+ assertEq(decoded.to, expected.to);
+ assertEq(decoded.value, expected.value);
+
+ if (uint8(txType) < 2) {
+ assertEq(decoded.gasPrice, expected.gasPrice);
+ }
+
+ if (uint8(txType) >= 1) {
+ // We keep access lists as opaque bytes for now, because we simply re-encode
+ // them to obtain the unsigned transaction. So we can't compare them directly.
+ // assertEq(decoded.accessList, expected.accessList);
+ }
+
+ if (uint8(txType) >= 2) {
+ assertEq(decoded.maxFeePerGas, expected.maxFeePerGas);
+ assertEq(decoded.maxPriorityFeePerGas, expected.maxPriorityFeePerGas);
+ }
+
+ if (uint8(txType) == 3) {
+ assertEq(decoded.maxFeePerBlobGas, expected.maxFeePerBlobGas);
+ assertEq(decoded.blobVersionedHashes.length, expected.blobVersionedHashes.length);
+ for (uint256 i = 0; i < decoded.blobVersionedHashes.length; i++) {
+ assertEq(decoded.blobVersionedHashes[i], expected.blobVersionedHashes[i]);
+ }
+ }
+ }
+
+ function _readTestCase(
+ uint256 id
+ ) public view returns (TestCase memory) {
+ string memory file = vm.readFile(_getTestCasePath(id));
+
+ TransactionDecoder.Transaction memory transaction = TransactionDecoder.Transaction({
+ chainId: uint64(_parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.chainId"))),
+ data: vm.parseJsonBytes(file, ".transaction.data"),
+ gasLimit: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.gasLimit")),
+ gasPrice: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.gasPrice")),
+ maxFeePerGas: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.maxFeePerGas")),
+ maxPriorityFeePerGas: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.maxPriorityFeePerGas")),
+ nonce: vm.parseJsonUint(file, ".transaction.nonce"),
+ to: vm.parseJsonAddress(file, ".transaction.to"),
+ value: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.value")),
+ maxFeePerBlobGas: _parseUintFromBytes(vm.parseJsonBytes(file, ".transaction.maxFeePerBlobGas")),
+ blobVersionedHashes: vm.parseJsonBytes32Array(file, ".transaction.blobVersionedHashes"),
+ // Note: These fields aren't present in the test cases so they can be skipped.
+ // These are tested indirectly by the signature and preimage checks.
+ txType: TransactionDecoder.TxType.Legacy,
+ accessList: new bytes[](0),
+ // Signature is checked by recovering the sender and comparing it to the pubkey
+ // derived from the private key in the test case.
+ sig: "",
+ // Note: these fields are just internal helpers for the decoder library.
+ legacyV: 0,
+ isChainIdSet: false
+ });
+
+ return TestCase({
+ name: vm.parseJsonString(file, ".name"),
+ privateKey: uint256(vm.parseJsonBytes32(file, ".privateKey")),
+ unsignedLegacy: vm.parseJsonBytes(file, ".unsignedLegacy"),
+ unsignedEip155: vm.parseJsonBytes(file, ".unsignedEip155"),
+ unsignedBerlin: vm.parseJsonBytes(file, ".unsignedBerlin"),
+ unsignedLondon: vm.parseJsonBytes(file, ".unsignedLondon"),
+ unsignedCancun: vm.parseJsonBytes(file, ".unsignedCancun"),
+ signedLegacy: vm.parseJsonBytes(file, ".signedLegacy"),
+ signedEip155: vm.parseJsonBytes(file, ".signedEip155"),
+ signedBerlin: vm.parseJsonBytes(file, ".signedBerlin"),
+ signedLondon: vm.parseJsonBytes(file, ".signedLondon"),
+ signedCancun: vm.parseJsonBytes(file, ".signedCancun"),
+ transaction: transaction
+ });
+ }
+
+ // Helper to parse an uint from bytes padded to the left
+ function _parseUintFromBytes(
+ bytes memory data
+ ) internal pure returns (uint256) {
+ return uint256(BytesUtils.toBytes32PadLeft(data));
+ }
+
+ function _parseOpaqueAccessList(
+ bytes memory data
+ ) internal pure returns (bytes[] memory) {}
+}
diff --git a/bolt-contracts/test/Utils.sol b/bolt-contracts/test/Utils.sol
new file mode 100644
index 000000000..731d1de18
--- /dev/null
+++ b/bolt-contracts/test/Utils.sol
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test, console} from "forge-std/Test.sol";
+
+import {BoltConfig} from "../src/lib/Config.sol";
+
+contract Utils is Test {
+ function readParameters() public view returns (BoltConfig.Parameters memory) {
+ string memory root = vm.projectRoot();
+ string memory path = string.concat(root, "/config/test/parameters.json");
+ string memory json = vm.readFile(path);
+
+ uint48 epochDuration = uint48(vm.parseJsonUint(json, ".epochDuration"));
+ uint48 slashingWindow = uint48(vm.parseJsonUint(json, ".slashingWindow"));
+ uint48 maxChallengeDuration = uint48(vm.parseJsonUint(json, ".maxChallengeDuration"));
+ bool allowUnsafeRegistration = vm.parseJsonBool(json, ".allowUnsafeRegistration");
+ uint256 challengeBond = vm.parseJsonUint(json, ".challengeBond");
+ uint256 blockhashEvmLookback = vm.parseJsonUint(json, ".blockhashEvmLookback");
+ uint256 justificationDelay = vm.parseJsonUint(json, ".justificationDelay");
+ uint256 eth2GenesisTimestamp = vm.parseJsonUint(json, ".eth2GenesisTimestamp");
+ uint256 slotTime = vm.parseJsonUint(json, ".slotTime");
+ uint256 minimumOperatorStake = vm.parseJsonUint(json, ".minimumOperatorStake");
+
+ return BoltConfig.Parameters({
+ epochDuration: epochDuration,
+ slashingWindow: slashingWindow,
+ maxChallengeDuration: maxChallengeDuration,
+ challengeBond: challengeBond,
+ blockhashEvmLookback: blockhashEvmLookback,
+ justificationDelay: justificationDelay,
+ eth2GenesisTimestamp: eth2GenesisTimestamp,
+ slotTime: slotTime,
+ allowUnsafeRegistration: allowUnsafeRegistration,
+ minimumOperatorStake: minimumOperatorStake
+ });
+ }
+}
diff --git a/bolt-contracts/test/fixtures/EigenLayerDeployer.f.sol b/bolt-contracts/test/fixtures/EigenLayerDeployer.f.sol
new file mode 100644
index 000000000..61e6133e9
--- /dev/null
+++ b/bolt-contracts/test/fixtures/EigenLayerDeployer.f.sol
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: BUSL-1.1
+pragma solidity ^0.8.12;
+
+import "@eigenlayer/lib/openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
+import "@eigenlayer/lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol";
+import "@eigenlayer/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
+import "@eigenlayer/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol";
+import "@eigenlayer/lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol";
+
+import "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol";
+import "@eigenlayer/src/contracts/core/DelegationManager.sol";
+import "@eigenlayer/src/contracts/core/AVSDirectory.sol";
+
+import "@eigenlayer/src/contracts/interfaces/IETHPOSDeposit.sol";
+
+import "@eigenlayer/src/contracts/core/StrategyManager.sol";
+import "@eigenlayer/src/contracts/strategies/StrategyBase.sol";
+import "@eigenlayer/src/contracts/core/Slasher.sol";
+
+import "@eigenlayer/src/contracts/pods/EigenPod.sol";
+import "@eigenlayer/src/contracts/pods/EigenPodManager.sol";
+
+import "@eigenlayer/src/contracts/permissions/PauserRegistry.sol";
+
+import "@eigenlayer/src/test/mocks/LiquidStakingToken.sol";
+import "@eigenlayer/src/test/mocks/EmptyContract.sol";
+import "@eigenlayer/src/test/mocks/ETHDepositMock.sol";
+
+import "forge-std/Test.sol";
+import "forge-std/Script.sol";
+import "forge-std/StdJson.sol";
+
+address constant HEVM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));
+
+contract Operators is Test {
+ string internal operatorConfigJson;
+
+ constructor() {
+ operatorConfigJson = vm.readFile("./test/test_data_eigenlayer/operators.json");
+ }
+
+ function operatorPrefix(
+ uint256 index
+ ) public pure returns (string memory) {
+ return string.concat(".operators[", string.concat(vm.toString(index), "]."));
+ }
+
+ function getNumOperators() public view returns (uint256) {
+ return stdJson.readUint(operatorConfigJson, ".numOperators");
+ }
+
+ function getOperatorAddress(
+ uint256 index
+ ) public view returns (address) {
+ return stdJson.readAddress(operatorConfigJson, string.concat(operatorPrefix(index), "Address"));
+ }
+
+ function getOperatorSecretKey(
+ uint256 index
+ ) public view returns (uint256) {
+ return readUint(operatorConfigJson, index, "SecretKey");
+ }
+
+ function readUint(string memory json, uint256 index, string memory key) public pure returns (uint256) {
+ return stringToUint(stdJson.readString(json, string.concat(operatorPrefix(index), key)));
+ }
+
+ function stringToUint(
+ string memory s
+ ) public pure returns (uint256) {
+ bytes memory b = bytes(s);
+ uint256 result = 0;
+ for (uint256 i = 0; i < b.length; i++) {
+ if (uint256(uint8(b[i])) >= 48 && uint256(uint8(b[i])) <= 57) {
+ result = result * 10 + (uint256(uint8(b[i])) - 48);
+ }
+ }
+ return result;
+ }
+
+ function setOperatorJsonFilePath(
+ string memory filepath
+ ) public {
+ operatorConfigJson = vm.readFile(filepath);
+ }
+}
+
+contract EigenLayerDeployer is Operators {
+ Vm cheats = Vm(HEVM_ADDRESS);
+
+ // EigenLayer contracts
+ ProxyAdmin public eigenLayerProxyAdmin;
+ PauserRegistry public eigenLayerPauserReg;
+
+ Slasher public slasher;
+ DelegationManager public delegationManager;
+ StrategyManager public strategyManager;
+ EigenPodManager public eigenPodManager;
+ AVSDirectory public avsDirectory;
+ IEigenPod public pod;
+ IETHPOSDeposit public ethPOSDeposit;
+ IBeacon public eigenPodBeacon;
+
+ // testing/mock contracts
+ IERC20 public eigenToken;
+ IERC20 public weth;
+ StrategyBase public wethStrat;
+ StrategyBase public eigenStrat;
+ StrategyBase public baseStrategyImplementation;
+ EmptyContract public emptyContract;
+
+ mapping(uint256 => IStrategy) public strategies;
+
+ //from testing seed phrase
+ bytes32 priv_key_0 = 0x1234567812345678123456781234567812345678123456781234567812345678;
+ bytes32 priv_key_1 = 0x1234567812345678123456781234567812345698123456781234567812348976;
+
+ //strategy indexes for undelegation (see commitUndelegation function)
+ uint256[] public strategyIndexes;
+ address sample_registrant = cheats.addr(436_364_636);
+
+ address[] public slashingContracts;
+
+ uint256 wethInitialSupply = 10e50;
+ address staker;
+ uint256 public constant eigenTotalSupply = 1000e18;
+ uint256 nonce = 69;
+ uint256 public gasLimit = 750_000;
+ IStrategy[] public initializeStrategiesToSetDelayBlocks;
+ uint256[] public initializeWithdrawalDelayBlocks;
+ uint256 minWithdrawalDelayBlocks = 0;
+ uint32 PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS = 7 days / 12 seconds;
+ uint256 REQUIRED_BALANCE_WEI = 32 ether;
+ uint64 MAX_PARTIAL_WTIHDRAWAL_AMOUNT_GWEI = 1 ether / 1e9;
+ uint64 GOERLI_GENESIS_TIME = 1_616_508_000;
+
+ address pauser;
+ address unpauser;
+ address theMultiSig = address(420);
+ address operator = address(0x4206904396bF2f8b173350ADdEc5007A52664293); //sk: e88d9d864d5d731226020c5d2f02b62a4ce2a4534a39c225d32d3db795f83319
+ address acct_0 = cheats.addr(uint256(priv_key_0));
+ address acct_1 = cheats.addr(uint256(priv_key_1));
+ address _challenger = address(0x6966904396bF2f8b173350bCcec5007A52669873);
+ address public eigenLayerReputedMultisig = address(this);
+
+ address eigenLayerProxyAdminAddress;
+ address eigenLayerPauserRegAddress;
+ address slasherAddress;
+ address delegationAddress;
+ address strategyManagerAddress;
+ address eigenPodManagerAddress;
+ address podAddress;
+ address eigenPodBeaconAddress;
+ address emptyContractAddress;
+ address operationsMultisig;
+ address executorMultisig;
+
+ // addresses excluded from fuzzing due to abnormal behavior. TODO: @Sidu28 define this better and give it a clearer name
+ mapping(address => bool) fuzzedAddressMapping;
+
+ //ensures that a passed in address is not set to true in the fuzzedAddressMapping
+ modifier fuzzedAddress(
+ address addr
+ ) virtual {
+ cheats.assume(fuzzedAddressMapping[addr] == false);
+ _;
+ }
+
+ modifier cannotReinit() {
+ cheats.expectRevert(bytes("Initializable: contract is already initialized"));
+ _;
+ }
+
+ constructor(
+ address _staker
+ ) {
+ staker = _staker;
+ }
+
+ //performs basic deployment before each test
+ function setUp() public virtual {
+ try vm.envUint("CHAIN_ID") returns (uint256 chainId) {
+ if (chainId == 31_337) {
+ _deployEigenLayerContractsLocal();
+ }
+ // If CHAIN_ID ENV is not set, assume local deployment on 31337
+ } catch {
+ _deployEigenLayerContractsLocal();
+ }
+
+ fuzzedAddressMapping[address(0)] = true;
+ fuzzedAddressMapping[address(eigenLayerProxyAdmin)] = true;
+ fuzzedAddressMapping[address(strategyManager)] = true;
+ fuzzedAddressMapping[address(eigenPodManager)] = true;
+ fuzzedAddressMapping[address(delegationManager)] = true;
+ fuzzedAddressMapping[address(slasher)] = true;
+ }
+
+ function _deployEigenLayerContractsLocal() internal {
+ pauser = address(69);
+ unpauser = address(489);
+ // deploy proxy admin for ability to upgrade proxy contracts
+ eigenLayerProxyAdmin = new ProxyAdmin();
+
+ //deploy pauser registry
+ address[] memory pausers = new address[](1);
+ pausers[0] = pauser;
+ eigenLayerPauserReg = new PauserRegistry(pausers, unpauser);
+
+ /**
+ * First, deploy upgradeable proxy contracts that **will point** to the implementations. Since the implementation contracts are
+ * not yet deployed, we give these proxies an empty contract as the initial implementation, to act as if they have no code.
+ */
+ emptyContract = new EmptyContract();
+ delegationManager = DelegationManager(
+ address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
+ );
+ strategyManager = StrategyManager(
+ address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
+ );
+ slasher =
+ Slasher(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
+ eigenPodManager = EigenPodManager(
+ address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
+ );
+ avsDirectory = AVSDirectory(
+ address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
+ );
+ ethPOSDeposit = new ETHPOSDepositMock();
+ pod = new EigenPod(ethPOSDeposit, eigenPodManager, GOERLI_GENESIS_TIME);
+
+ eigenPodBeacon = new UpgradeableBeacon(address(pod));
+
+ // Second, deploy the *implementation* contracts, using the *proxy contracts* as inputs
+ DelegationManager delegationImplementation = new DelegationManager(strategyManager, slasher, eigenPodManager);
+ StrategyManager strategyManagerImplementation = new StrategyManager(delegationManager, eigenPodManager, slasher);
+ Slasher slasherImplementation = new Slasher(strategyManager, delegationManager);
+ EigenPodManager eigenPodManagerImplementation =
+ new EigenPodManager(ethPOSDeposit, eigenPodBeacon, strategyManager, slasher, delegationManager);
+ AVSDirectory avsDirectoryImplementation = new AVSDirectory(delegationManager);
+
+ // Third, upgrade the proxy contracts to use the correct implementation contracts and initialize them.
+ eigenLayerProxyAdmin.upgradeAndCall(
+ TransparentUpgradeableProxy(payable(address(delegationManager))),
+ address(delegationImplementation),
+ abi.encodeWithSelector(
+ DelegationManager.initialize.selector,
+ eigenLayerReputedMultisig,
+ eigenLayerPauserReg,
+ 0, /*initialPausedStatus*/
+ minWithdrawalDelayBlocks,
+ initializeStrategiesToSetDelayBlocks,
+ initializeWithdrawalDelayBlocks
+ )
+ );
+ eigenLayerProxyAdmin.upgradeAndCall(
+ TransparentUpgradeableProxy(payable(address(strategyManager))),
+ address(strategyManagerImplementation),
+ abi.encodeWithSelector(
+ StrategyManager.initialize.selector,
+ eigenLayerReputedMultisig,
+ eigenLayerReputedMultisig,
+ eigenLayerPauserReg,
+ 0 /*initialPausedStatus*/
+ )
+ );
+ eigenLayerProxyAdmin.upgradeAndCall(
+ TransparentUpgradeableProxy(payable(address(slasher))),
+ address(slasherImplementation),
+ abi.encodeWithSelector(
+ Slasher.initialize.selector, eigenLayerReputedMultisig, eigenLayerPauserReg, 0 /*initialPausedStatus*/
+ )
+ );
+ eigenLayerProxyAdmin.upgradeAndCall(
+ TransparentUpgradeableProxy(payable(address(eigenPodManager))),
+ address(eigenPodManagerImplementation),
+ abi.encodeWithSelector(
+ EigenPodManager.initialize.selector,
+ eigenLayerReputedMultisig,
+ eigenLayerPauserReg,
+ 0 /*initialPausedStatus*/
+ )
+ );
+ eigenLayerProxyAdmin.upgradeAndCall(
+ TransparentUpgradeableProxy(payable(address(avsDirectory))),
+ address(avsDirectoryImplementation),
+ abi.encodeWithSelector(
+ EigenPodManager.initialize.selector,
+ eigenLayerReputedMultisig,
+ eigenLayerPauserReg,
+ 0 /*initialPausedStatus*/
+ )
+ );
+
+ //simple ERC20 (**NOT** WETH-like!), used in a test strategy
+ weth = new ERC20PresetFixedSupply("weth", "WETH", wethInitialSupply, staker);
+
+ // deploy StrategyBase contract implementation, then create upgradeable proxy that points to implementation and initialize it
+ baseStrategyImplementation = new StrategyBase(strategyManager);
+ wethStrat = StrategyBase(
+ address(
+ new TransparentUpgradeableProxy(
+ address(baseStrategyImplementation),
+ address(eigenLayerProxyAdmin),
+ abi.encodeWithSelector(StrategyBase.initialize.selector, weth, eigenLayerPauserReg)
+ )
+ )
+ );
+
+ eigenToken = new ERC20PresetFixedSupply("eigen", "EIGEN", wethInitialSupply, staker);
+
+ // deploy upgradeable proxy that points to StrategyBase implementation and initialize it
+ eigenStrat = StrategyBase(
+ address(
+ new TransparentUpgradeableProxy(
+ address(baseStrategyImplementation),
+ address(eigenLayerProxyAdmin),
+ abi.encodeWithSelector(StrategyBase.initialize.selector, eigenToken, eigenLayerPauserReg)
+ )
+ )
+ );
+
+ // Whitelist strategies for deposit
+ IStrategy[] memory strategiesToWhitelist = new IStrategy[](2);
+ strategiesToWhitelist[0] = wethStrat;
+ strategiesToWhitelist[1] = eigenStrat;
+
+ bool[] memory thirdPartyTransfersForbidden = new bool[](2);
+
+ strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist, thirdPartyTransfersForbidden);
+ }
+
+ function _setAddresses(
+ string memory config
+ ) internal {
+ eigenLayerProxyAdminAddress = stdJson.readAddress(config, ".addresses.eigenLayerProxyAdmin");
+ eigenLayerPauserRegAddress = stdJson.readAddress(config, ".addresses.eigenLayerPauserReg");
+ delegationAddress = stdJson.readAddress(config, ".addresses.delegation");
+ strategyManagerAddress = stdJson.readAddress(config, ".addresses.strategyManager");
+ slasherAddress = stdJson.readAddress(config, ".addresses.slasher");
+ eigenPodManagerAddress = stdJson.readAddress(config, ".addresses.eigenPodManager");
+ emptyContractAddress = stdJson.readAddress(config, ".addresses.emptyContract");
+ operationsMultisig = stdJson.readAddress(config, ".parameters.operationsMultisig");
+ executorMultisig = stdJson.readAddress(config, ".parameters.executorMultisig");
+ }
+}
diff --git a/bolt-contracts/test/fixtures/SymbioticSetup.f.sol b/bolt-contracts/test/fixtures/SymbioticSetup.f.sol
new file mode 100644
index 000000000..e23fdafb7
--- /dev/null
+++ b/bolt-contracts/test/fixtures/SymbioticSetup.f.sol
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {Test} from "forge-std/Test.sol";
+
+import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol";
+import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol";
+import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol";
+import {NetworkRegistry} from "@symbiotic/contracts/NetworkRegistry.sol";
+import {OperatorRegistry} from "@symbiotic/contracts/OperatorRegistry.sol";
+import {MetadataService} from "@symbiotic/contracts/service/MetadataService.sol";
+import {NetworkMiddlewareService} from "@symbiotic/contracts/service/NetworkMiddlewareService.sol";
+import {OptInService} from "@symbiotic/contracts/service/OptInService.sol";
+
+import {Vault} from "@symbiotic/contracts/vault/Vault.sol";
+import {NetworkRestakeDelegator} from "@symbiotic/contracts/delegator/NetworkRestakeDelegator.sol";
+import {FullRestakeDelegator} from "@symbiotic/contracts/delegator/FullRestakeDelegator.sol";
+import {Slasher} from "@symbiotic/contracts/slasher/Slasher.sol";
+import {VetoSlasher} from "@symbiotic/contracts/slasher/VetoSlasher.sol";
+import {VaultConfigurator} from "@symbiotic/contracts/VaultConfigurator.sol";
+
+import {SimpleCollateral} from "@symbiotic/../test/mocks/SimpleCollateral.sol";
+import {Token} from "../mocks/Token.sol";
+
+contract SymbioticSetupFixture is Test {
+ function setUp(
+ address deployer,
+ address owner
+ )
+ public
+ returns (
+ VaultFactory vaultFactory,
+ DelegatorFactory delegatorFactory,
+ SlasherFactory slasherFactory,
+ NetworkRegistry networkRegistry,
+ OperatorRegistry operatorRegistry,
+ MetadataService operatorMetadataService,
+ MetadataService networkMetadataService,
+ NetworkMiddlewareService networkMiddlewareService,
+ OptInService operatorVaultOptInService,
+ OptInService operatorNetworkOptInService,
+ VaultConfigurator vaultConfigurator,
+ SimpleCollateral collateral
+ )
+ {
+ vm.startPrank(deployer);
+
+ VaultFactory vaultFactory_ = new VaultFactory(deployer);
+ DelegatorFactory delegatorFactory_ = new DelegatorFactory(deployer);
+ SlasherFactory slasherFactory_ = new SlasherFactory(deployer);
+ NetworkRegistry networkRegistry_ = new NetworkRegistry();
+ OperatorRegistry operatorRegistry_ = new OperatorRegistry();
+ MetadataService operatorMetadataService_ = new MetadataService(address(operatorRegistry_));
+ MetadataService networkMetadataService_ = new MetadataService(address(networkRegistry_));
+ NetworkMiddlewareService networkMiddlewareService_ = new NetworkMiddlewareService(address(networkRegistry_));
+ OptInService operatorVaultOptInService_ = new OptInService(address(operatorRegistry_), address(vaultFactory_));
+ OptInService operatorNetworkOptInService_ =
+ new OptInService(address(operatorRegistry_), address(networkRegistry_));
+
+ Vault vault_ = new Vault(address(delegatorFactory_), address(slasherFactory_), address(vaultFactory_));
+ vaultFactory_.whitelist(address(vault_));
+
+ address networkRestakeDelegator_ = address(
+ new NetworkRestakeDelegator(
+ address(networkRegistry_),
+ address(vaultFactory_),
+ address(operatorVaultOptInService_),
+ address(operatorNetworkOptInService_),
+ address(delegatorFactory_),
+ delegatorFactory_.totalTypes()
+ )
+ );
+ delegatorFactory_.whitelist(networkRestakeDelegator_);
+
+ address fullRestakeDelegator_ = address(
+ new FullRestakeDelegator(
+ address(networkRegistry_),
+ address(vaultFactory_),
+ address(operatorVaultOptInService_),
+ address(operatorNetworkOptInService_),
+ address(delegatorFactory_),
+ delegatorFactory_.totalTypes()
+ )
+ );
+ delegatorFactory_.whitelist(fullRestakeDelegator_);
+
+ address slasher_ = address(
+ new Slasher(
+ address(vaultFactory_),
+ address(networkMiddlewareService_),
+ address(slasherFactory_),
+ slasherFactory_.totalTypes()
+ )
+ );
+ slasherFactory_.whitelist(slasher_);
+
+ address vetoSlasher_ = address(
+ new VetoSlasher(
+ address(vaultFactory_),
+ address(networkMiddlewareService_),
+ address(networkRegistry_),
+ address(slasherFactory_),
+ slasherFactory_.totalTypes()
+ )
+ );
+ slasherFactory_.whitelist(vetoSlasher_);
+
+ VaultConfigurator vaultConfigurator_ =
+ new VaultConfigurator(address(vaultFactory_), address(delegatorFactory_), address(slasherFactory_));
+
+ vaultFactory_.transferOwnership(owner);
+ delegatorFactory_.transferOwnership(owner);
+ slasherFactory_.transferOwnership(owner);
+
+ Token token_ = new Token("Token");
+ SimpleCollateral collateral_ = new SimpleCollateral(address(token_));
+
+ vm.stopPrank();
+
+ return (
+ vaultFactory_,
+ delegatorFactory_,
+ slasherFactory_,
+ networkRegistry_,
+ operatorRegistry_,
+ operatorMetadataService_,
+ networkMetadataService_,
+ networkMiddlewareService_,
+ operatorVaultOptInService_,
+ operatorNetworkOptInService_,
+ vaultConfigurator_,
+ collateral_
+ );
+ }
+}
diff --git a/bolt-contracts/test/mocks/Token.sol b/bolt-contracts/test/mocks/Token.sol
new file mode 100644
index 000000000..f987de68c
--- /dev/null
+++ b/bolt-contracts/test/mocks/Token.sol
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.25;
+
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+contract Token is ERC20 {
+ constructor(
+ string memory name_
+ ) ERC20(name_, "") {
+ _mint(msg.sender, 1_000_000 * 10 ** decimals());
+ }
+}
diff --git a/bolt-contracts/test/test_data_eigenlayer/operators.json b/bolt-contracts/test/test_data_eigenlayer/operators.json
new file mode 100644
index 000000000..bcf24d006
--- /dev/null
+++ b/bolt-contracts/test/test_data_eigenlayer/operators.json
@@ -0,0 +1,350 @@
+{
+ "numOperators": 15,
+ "operators": [
+ {
+ "Address": "0x364ea4241059a1880a289ccf6f3e730371e399c2",
+ "SecretKey": "26538123046512735896321680912735809321651",
+ "SField": "11819029207127053157019202363815717308230004305264870932201236865394129798528",
+ "RPoint": {
+ "X": "4151816577563894587001429602861434330710880584628236143409913584674951374486",
+ "Y": "14755168631248666267406561486877346692276498564690931377857101897349212004983"
+ },
+ "PubkeyG1": {
+ "X": "18260007818883133054078754218619977578772505796600400998181738095793040006897",
+ "Y": "3432351341799135763167709827653955074218841517684851694584291831827675065899"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "7873360006442301391509331169041141152067986239770361289399829888515098511089",
+ "A1": "4819287168576083437987399094513915926762558557241451705488044107651777333471"
+ },
+ "Y": {
+ "A0": "20134059591429338312012269666175785157377156721420899775744488735642013266015",
+ "A1": "5180163866630806529018451776671042574814236436776522234046604410442923698614"
+ }
+ }
+ },
+ {
+ "Address": "0x435a2d0799fd5b4f6576d43ddedbfb21eeffd2ed",
+ "SecretKey": "26538123046512735896321680912735809321652",
+ "SField": "6841827072565597045205937795412322299165267769171326199068981979305129605253",
+ "RPoint": {
+ "X": "12076439550529905259390228862926613756534481189623148993240145246002590976299",
+ "Y": "974155300103671376671957621510050067124810818694470470430352429136065997968"
+ },
+ "PubkeyG1": {
+ "X": "20885635058167472198503803626695880018252841373181906566024991517994827983735",
+ "Y": "11837569400448747146619236953090341530939723755586681794040848163257307322015"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "3533647070879242563419359582151690893240073815886344269869120254242419508633",
+ "A1": "9536422671122585613677052246081291259644057320757694709671580313420208694081"
+ },
+ "Y": {
+ "A0": "12166486073145345231362662138062912830359628001285426518990520911876160470477",
+ "A1": "14962020261456080798629037722818062638639517674733500632912397408761625050802"
+ }
+ }
+ },
+ {
+ "Address": "0x523e0a2d296e6bc969296a0541289077d2c43246",
+ "SecretKey": "26538123046512735896321680912735809321653",
+ "SField": "6251323578278497984001410170708026455838289583399486055694150917867937589622",
+ "RPoint": {
+ "X": "17659335174238407859449322441269022710538670095928217612186082625191038704952",
+ "Y": "4030658631342963783788534863066598739941334059525264488801360888493204491332"
+ },
+ "PubkeyG1": {
+ "X": "2470846831436625524441718675125184646728308675024947867896488186792002263384",
+ "Y": "19398539713444487906856323737585450565673486119324566923739712072838048985181"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "18996698705977687383601958641379064195173448823300612508806962257981430138470",
+ "A1": "11390594476086232146697028608851603084418668227205561974107759331535935107531"
+ },
+ "Y": {
+ "A0": "10549563568039161023384174670373065438958654243218116252293848148484855972510",
+ "A1": "8392609726364984764069457200587817879819857139319066962985233014328835122137"
+ }
+ }
+ },
+ {
+ "Address": "0x618a412f2446921d3d942c8bf073000b97728f0f",
+ "SecretKey": "26538123046512735896321680912735809321654",
+ "SField": "12061860258274605985652162007024956030804911220114181564759813442108765736942",
+ "RPoint": {
+ "X": "7145152916403894254971893757626878750640430645346094737434568772482788991869",
+ "Y": "5571984232385193687234211544970704499791824743626734155736946702800034693686"
+ },
+ "PubkeyG1": {
+ "X": "12541368988415564232975583436986866293473603658145394466756402945376729450760",
+ "Y": "8511301244073806918313939726706790376744658017957805706622009852760184010910"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "12496796444461928801423998788871427472219126045995885876471223766691196263411",
+ "A1": "20900414356950550650632823116879453189806004631615934725359633772870431548864"
+ },
+ "Y": {
+ "A0": "1850282599163866918562002476972618618179924465313771842342246796868912815924",
+ "A1": "16267494054236543151546820684638619209010244733864385924658421853804576220788"
+ }
+ }
+ },
+ {
+ "Address": "0x70130e87da895ef840a9f8c41c6ebf0e5b48e824",
+ "SecretKey": "26538123046512735896321680912735809321655",
+ "SField": "18246936953360371302513647065102080917080836724122762820676034131128284534276",
+ "RPoint": {
+ "X": "14485348008548227125800493276491803315994326328485845085024767746874209352205",
+ "Y": "8354185564983387451413630826006944059627584517454500268893837380556264839426"
+ },
+ "PubkeyG1": {
+ "X": "12250988429106632987382001675218698439738318866960678150521924430133077466824",
+ "Y": "19721708441253375174926671177088389244933199376153285627796664290007532195052"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "15382764368631216635417307340007437259725116431665265535534236291634253279005",
+ "A1": "551113638605800186370795409845956026924016364384585878450724593710757152783"
+ },
+ "Y": {
+ "A0": "15798745520528389465130300542056950632504309474787848602575280595042835040946",
+ "A1": "15121681895121404713757090493706231060528428365493560188175592305488154660061"
+ }
+ }
+ },
+ {
+ "Address": "0x7290f60d6666d7be7a41de178216502342562996",
+ "SecretKey": "26538123046512735896321680912735809321656",
+ "SField": "18341272017139583935816150534955335402336824558387991233830658246285519689014",
+ "RPoint": {
+ "X": "18174419042266772640587019436747343741444240929289718199821728520031148396624",
+ "Y": "12944489167977623909526722715453960134323253038442885147323841789720380485266"
+ },
+ "PubkeyG1": {
+ "X": "21781696011906991536752467363456044926552139424863730238878701223169542126809",
+ "Y": "12365056838489612884478362448314510672391017658276612399537174676963477032436"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "7602713822854229266858239462850154602893490834036446886716499673780465081437",
+ "A1": "6755904671274545514612841103024355730484086518739594420309025281657878144069"
+ },
+ "Y": {
+ "A0": "19169973646980957540631709981893659820049084634788201359252419558688034251538",
+ "A1": "3617096885662895416635037761438216245027220053803903379077414864234110502059"
+ }
+ }
+ },
+ {
+ "Address": "0x8d8e96b3bb658c5830aef90760f79e6a2a3bba41",
+ "SecretKey": "26538123046512735896321680912735809321657",
+ "SField": "12685816762097293544054119736761928362052971499853660676176419762479313483888",
+ "RPoint": {
+ "X": "1845886565323083117137480765392173031492903457678364169791939158881472188429",
+ "Y": "13621394807982841470261073338436747381963203491878788814653645637014444568601"
+ },
+ "PubkeyG1": {
+ "X": "3212013732044844428966711160610869391929177932647992063562413834101261373977",
+ "Y": "15448314027915072279969465354772467136408027411697025021594849451692520530947"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "19758878765410158466580323802894117259849215806855698140995410808900358199475",
+ "A1": "6427725156456990179277796366756163887223625521472017190816516318004039813324"
+ },
+ "Y": {
+ "A0": "20996191524580276458284414610581896434480007527247384059381015445157582556532",
+ "A1": "14948621227766242224388430084744009250457802204821770762677931043984889219820"
+ }
+ }
+ },
+ {
+ "Address": "0xa71e43be339f9791235641f457c1ba2da86b9eb3",
+ "SecretKey": "26538123046512735896321680912735809321658",
+ "SField": "6042114504025969519448864857463886717993192687698758790871627541311475539318",
+ "RPoint": {
+ "X": "2992138105299859744840964524791160337229571931324293630805658245249757407389",
+ "Y": "15694736019044294046047702836262370586371888280839741425891501314654607706201"
+ },
+ "PubkeyG1": {
+ "X": "3667070400945503213289817934609709506016134089772708044008927730500827726299",
+ "Y": "201965492781810222622603023497133228304861253112953784295749598350720860919"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "18480475432118436773114589869805528330114616937670136874673576408735271448220",
+ "A1": "13748787029936230407330641361791178403853002576716058275674223802004495813919"
+ },
+ "Y": {
+ "A0": "9393505688889091760306178659573951040934743290746925629508348356081460722985",
+ "A1": "8911229656394169335558484911670452902836715586054327693497766770926601776864"
+ }
+ }
+ },
+ {
+ "Address": "0xca9827a614f6c910c33f7668823f9d079515a6b2",
+ "SecretKey": "26538123046512735896321680912735809321659",
+ "SField": "4089036516179531739857184481801462737502679331172598056405360493929192933822",
+ "RPoint": {
+ "X": "11344456565890051191982811922569789285426097564838757420328232921661131972594",
+ "Y": "5214697287561608797059987511134212052053543960787141328755773567687921715591"
+ },
+ "PubkeyG1": {
+ "X": "10603532593425799461116428547718401984650331452231439217583617357908466102307",
+ "Y": "9038166977605721429955182587619307974711670991162762169137768185433368026502"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "2665943891698121544094051819956376909984132954889653928721123177413544545101",
+ "A1": "5067301718610021746809469955529953474259097833119436258748751618113087224295"
+ },
+ "Y": {
+ "A0": "14618865954382816111109898027326987227550002588645252687730089534591097604721",
+ "A1": "17668847581520475585017930293964643097348730464667967915604118496715335354459"
+ }
+ }
+ },
+ {
+ "Address": "0xdda9a89ddf845c9ee5d2810e0833b4a71d66179d",
+ "SecretKey": "265381230465127358963216809127358093216510",
+ "SField": "3494626234115151042064050025070425206800280651375775826757871600988046500493",
+ "RPoint": {
+ "X": "2823775573240572793382269280921054559755380883233834426161454792596668870337",
+ "Y": "2116482807847046775754020075448309148535809630987701014560424524130609194680"
+ },
+ "PubkeyG1": {
+ "X": "9580940219508129828900474539321198495544615416706116943528153675304087866935",
+ "Y": "19817948430483568579408524674460630359968264374260052173849319927990972424409"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "14146902041950849992065142134350889699589625292784530382058790130731064171060",
+ "A1": "9643173139567873327873151464253356980578492901466026171946322545605998333728"
+ },
+ "Y": {
+ "A0": "14418997116441129782787372775003831815699951211438552978754436263436048789176",
+ "A1": "7555011997614448462339727638657930440787456831044184868102463346992814218544"
+ }
+ }
+ },
+ {
+ "Address": "0xdde7a5dbb45d0f48d74d40772c2dda8acd89a304",
+ "SecretKey": "265381230465127358963216809127358093216511",
+ "SField": "19331236222084214974477469493395354532285940046992732403697375380156732141073",
+ "RPoint": {
+ "X": "18634736052925172105067492946541508040225080612748659668517401756117768987613",
+ "Y": "5852722109239017950047732236711408776617783536462429224588732414658414009409"
+ },
+ "PubkeyG1": {
+ "X": "3369815046108753886283376619953242018939084016540366266511463175341760902943",
+ "Y": "17472522035733702939562892574753748100276735367509783930154400656979558647072"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "12828974775248479668694052242644004695064721299497890085252948770085462880539",
+ "A1": "7272600703570139069540236030139742611695513123908572460354778844049179248665"
+ },
+ "Y": {
+ "A0": "6187997634621416381645973736825134854588734099638054268199535544372796244575",
+ "A1": "3168611351805563509285790274061051556477979031759511765104889273283967743401"
+ }
+ }
+ },
+ {
+ "Address": "0xe3fe83a83f3c7dc625382f062c159809551c580b",
+ "SecretKey": "265381230465127358963216809127358093216512",
+ "SField": "5508482612796623641475745691306343682701589066262691055533968657240534344204",
+ "RPoint": {
+ "X": "21685944819341458246191809126376074155122173268146956524878025759785902438148",
+ "Y": "14479737406316720196422920923227363552037206020376032776793814881581188057870"
+ },
+ "PubkeyG1": {
+ "X": "8070743655895930899886894874393947088184525509008178391331533769906913292208",
+ "Y": "6166041079211541340267466423045787629866667719971895616570343665150421540281"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "2678990229866359665311344452542118509908675024979420599004546926969138331011",
+ "A1": "2049191090024360768595356041854174484396277833846595743282145840232552333594"
+ },
+ "Y": {
+ "A0": "3201866794780851807317614975829248389859676087140523014123159053900573945553",
+ "A1": "6264970183638030024425633356945995283617627028846078571269515762464600802251"
+ }
+ }
+ },
+ {
+ "Address": "0x7e32bdd5ba50eb8d5e5bd7d3eef1e8bb7f0177f0",
+ "SecretKey": "265381230465127358963216809127358093216513",
+ "SField": "8232703131218555943214969386347851175399186201805966768750414520371803575150",
+ "RPoint": {
+ "X": "19242125080553338551396473264815502757575266047308561121276377861187981762008",
+ "Y": "6218373850521459529400329711936878862028424392534186108921910846882020330028"
+ },
+ "PubkeyG1": {
+ "X": "3330687773238963481566047234114671341321386178661830488834900006935732354709",
+ "Y": "1586247746639542071922023676451514656089805747478597321110273768112339015566"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "17613195447478151801754685924971955231494961653094677584006576833833181220337",
+ "A1": "8248149727711895523076824904570372119241450169122068550957488380013460422052"
+ },
+ "Y": {
+ "A0": "4921860114926042695859196707091513960894135032516354392120636579531786968732",
+ "A1": "6497674699404552189948059719808314151701318782389032881121361153353612847519"
+ }
+ }
+ },
+ {
+ "Address": "0x90a89f27673e9b99001d25c5328ea7b248341aee",
+ "SecretKey": "265381230465127358963216809127358093216514",
+ "SField": "4235478359213410732582392105113225989686712574423827420889537931357288127804",
+ "RPoint": {
+ "X": "20654778647025427886329231590146081813406215959318690452465157767589910386387",
+ "Y": "16434595237030862998502345611891947093280350242989172641995260786504200724952"
+ },
+ "PubkeyG1": {
+ "X": "20354234858398388849816932240956270026965879865370684564771582015555136553997",
+ "Y": "19793685024108212849441227006186442648468124186906966567382667532342323504884"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "2406250413451469352424770691776336657864578839352351617783440321552775726788",
+ "A1": "17191001754715829242845993906554922262830769871842065626630924785145928353563"
+ },
+ "Y": {
+ "A0": "9681366579801251558910069516165884865963240333390287489273262916032696422128",
+ "A1": "16915387601971985830991163871960501726831560513559637189932888959407309364443"
+ }
+ }
+ },
+ {
+ "Address": "0x8d26afa6847f837d05c1bad6e9f87c7ac1be1566",
+ "SecretKey": "265381230465127358963216809127358093216515",
+ "SField": "11035850218432072216042138989371348095984008884653111551948778896223502226130",
+ "RPoint": {
+ "X": "17221699912650368882632599262801462631856236460154101376095314228983311857640",
+ "Y": "16920232814901492682413294000319977483527313089896426400855455024070752739456"
+ },
+ "PubkeyG1": {
+ "X": "3168995773972892355858790410620399201792411842283506602925392425152373993765",
+ "Y": "3166501588898491583880629288124229291720446936681364855672031758818970837581"
+ },
+ "PubkeyG2": {
+ "X": {
+ "A0": "1061540865081686461127583443728867079739536618424919508446137160337220400469",
+ "A1": "14314019642545413347565949557134146221646346593211321744274793627527703796333"
+ },
+ "Y": {
+ "A0": "5285717062790019556696539193484956838225490023146378618961849297769407066983",
+ "A1": "14567857613796427090761122799009706481422522677880753883893727561643094726547"
+ }
+ }
+ }
+ ]
+}
diff --git a/bolt-contracts/test/testdata/bolt_commitment.json b/bolt-contracts/test/testdata/bolt_commitment.json
new file mode 100644
index 000000000..596ee674e
--- /dev/null
+++ b/bolt-contracts/test/testdata/bolt_commitment.json
@@ -0,0 +1,5 @@
+{
+ "slot": 633067,
+ "signature": "0xcdd20b2abbd8cdfb77ec2608e1227f8ce0f66133b9d0ec0ea68102c2152b82193e3be0d6967b7c20b83e1a2530daa3a07713556541dc2aa16a46d922e6145a2b01",
+ "tx": "0xf86b82016e84042343e0830f424094deaddeaddeaddeaddeaddeaddeaddeaddeaddead0780850344281a21a0e525fc31b5574722ff064bdd127c4441b0fc66de7dc44928e163cb68e9d807e5a00b3ec02fc1e34b0209f252369ad10b745cd5a51c88384a340f7a150d0e45e471"
+}
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_20785012.json b/bolt-contracts/test/testdata/eth_proof_20785012.json
new file mode 100644
index 000000000..25d98aa9b
--- /dev/null
+++ b/bolt-contracts/test/testdata/eth_proof_20785012.json
@@ -0,0 +1,22 @@
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "0x0d9f5045b604ba0c050b5eb06d0b25d01c525ea5",
+ "accountProof": [
+ "0xf90211a097ed06a390ed9c16ec9299e19125791653b9201b784bf9a42d84e0974b664cb3a0f3a9aa3a6f2cdc6b910d4734c0c8fec4b3bd8a676c711eb0c533295a68ce43bfa01f7ed42296ea93615febab61c43d9cc467789f55851549452858136b9efe9e42a077b8504dbe1ba2d05802efbaa36630fa5b10558c6a13de736375997b5e14c9a2a08ea3f8e0bdb950843d81846528ee71d6eca46c9a273528843aea374b5c355ccca0eeaab1a08eb462bb3efe98d852fbea54769095de57beb60de9c8c72331304401a0cd3c794240e0de405bfee95babf6f505979983a37f79590be3b862e53f960807a04dd46005f5c29ebfb69815f918853bcff3af08a2f9c3b3493033fc0dc0a773eca005ca774fdb64b89b25f07f7e960fdfc9b4149ca9c6e7751fcb55824f04eb3ff3a021c51f7e644285567f75772792cb66c39f34f0509a50af2da12af26cf1ac9b10a072ccec3415e7904da03a2bf677708cdcd8d1d29b59c9c0e408ee8608fe3b3823a010d8c53e6bf134e70476d64f41cf8d1e219aece6c969809225a269027eee6a6ca06effeb1ca18e2254df42d2d0cd42825178409d6b210127a0a6c07fc95680dcdfa01c1b854f1606365826b1153586d98342ecacdf0acebe933a00256fdee7af5a59a0306fb9c170aa24d20350f7277392adb78557f48ecb62b3d6cf23698988eac284a0daaa352a43bdd737c9524a8cb3a98802ae5185e3b71eb865c71473540edb7ef280",
+ "0xf90211a03550162fce21c8be10d224e61acebee31945d54a9d576f11758526190e5bf987a0c75ea41d0704a50f6ef436fede8ff700a2d3c0edbf5c7066f2e7e2346a1b6b15a0a99a258a63cda78ff4fcb4bcdcef99e8f8f125da55ecaba58034408a3c10c03ba028b51ffb8812262e563d86724920a7b7b8d13573ccc747e01b4cc5db45b9c2e9a0ec8f2eab33b3c5a1f2e6c1dc8984299f07a5be7f41cc82e103ec8b5aca0a9e90a0b57d1ae6538304a97d74f19dcf5b5999ce74a8b3818e4f1ba6f2a55c7d3933aca06d302c5ad7c218e7772a6fdd426b07ee6a151529f2993e07fa3fe728e03a1016a010184894262fd8c80d8596760e68e39bbe17864fca10d5caf0803c3f9c77ecf1a04a2fa9ae91770be3477bd857ca876b629e8d74f15b36a2545c888d17396bff0fa0ae4da4e3330f57b5f57956e648c7c7385a5a5d366b0463ddb0add15e47c588f6a0bfcd3d5bf7b55f88d34e1f6d7760bf18b3547ae2004a05db706c6d6d41d2ce0da07d9caa8bb424afe94efc92b3f2207b480231c6a46bd2c6e67e33b8335eabe14fa0c05d5e0666734ea57d97a9ea74f16698b05db5282f973fb6f3f5f3de1c2b458ca0ac81cc5c382467c74df9f355e220cabdba514a01b59ba5779639611e87b88b8da0219c2f1156d413196b274acb0234c2fcb41ed66aecd1e5011ad83da73b047ce7a090be7be27cf1be666f7835cd80feecb0904d50403adabb86ad1fae00a8cb29da80",
+ "0xf90211a0a85deaca1adffdb7713b02a33af6ec05b09237d060963af84d7dc1a269178845a0cfd70f233bd4eb4ac42209a0578808f9e24498297af9a3e902233c1839a3a3cda0d6975a90005a1966ddae9b0f978161fb032ea2ca2d8db2453fc6be2710d48388a0d33abb2b6de28462b483a584224b30c079986bed9599023f28c81db7ee324b29a054911488c612caf55eaa494f0cc84ba25485ee49ce40f49b434098951036517da0cc4c54ed2de6d2b8fcc3bcc6d1fc490e8d26e24d4ca1ff3d81c7492fa23e4c3ba06249e67a29f9ed1713043324faf5b666d790977fad710f1a0a9e660ccb48c97da0ed8f59ba4a2766bb4035cc489fbe5f34795c2c36e16bd2d00b7b08a5a6f42507a0e2caaa014febfa0bdc9179bf519a963a05e36c634ce616c744ae2bd0b48824c0a05c2e1286d8a66758e1c1fcb3c85f28791f95ee85615a39c1d6780f6a54e1b3a6a05fe7bca3ef5fb8a2c8be1b987976a40c0345cf92e5117b3898159e4ed5ada800a0ac9228bf16f7c15155965d5eb5d7aa2e2f31e8f937d52a7203abef348b843632a0a193e1b44b2441cbed3d8cd20862d7f11b0121b957a82d63168b67809dbaf3b2a09aba1e3d0feb612998294bd3645247790107e619bf8728819fbe463fec342f5ca09336f5d7c3d7ddb6e987ac5f7c0e78e3936e05f6da27a7c87813a6e5250e9c38a021c230f247db23bd8d948d68a24891afd58c29864f1d74551060c10067e386a380",
+ "0xf90211a09d93666285cfbada2d91c81fb96b5e30ff75a51353c69a30c0a1431f51867880a05efd44e1b30767c66f02d79e93b2c45efcd56018050605c08818645ddb7a46c5a05e5bdbdd63d64f58a34f47a42d70b83febf924c08cdfa8d5673db3c6bd71df67a05d67553cf6620c67dead6ee37a48a5dfddddd4adce333c4a7771c25140ecdd92a07d5eb9550d7f807d7e0dd86a52f3d111b4359ac2928ca3e07c34bd6551055537a0faba45751c6566255746b31a67d0cd73163a115d0aca516c0625668a20439846a03230ce2ec5fa8173fca250ac63df602e3a6d2bff691edc8552ab8bdea458bf07a01e7ad54a56fcd625d6066ec05b6efc30bf90a65b756277f79a65ceb0eb108a32a0089ff36bd32acfeef1b5faf546e54ac7646086d1c2c764239cc9b98da8c460e7a00905f00f493594ce1c13274c2bb5ad018fab388de860f1cae6af64cb3ad3b46fa0ad7d4f9200d8f0ae34bf0cbece04d61b13c6daa0b7ea0e9d732522c5d46feacda075da3fc2991d7d32cb0be70210c7cbe3bd5883cfc52f58e8e04d07b9977f816fa0c7bc6aab1b71efc9ffc459cfda04d930f1652d53179a38b43f8e0980b40ab48ca0ae012ca5c6a3363d5f850e060001c90cd3a022aa44e325d5d832021ebef4fafea06ee58a40e8abba2a3be093fdefd9d9871558f4fa11f6d6721307117a17260b51a07cc44d713e05b70e6749fedd7cb1b4b47d2102638bf93ffd0e8ebf404af6940080",
+ "0xf90211a0b1689af6f7f511cc59773684cb467f33c3dcc5dc1a08f451c851544dca6b1a3aa039a80eb019a427f8b32dfdd88a0b5d95a21521444c67fca8f1e875f1898e16c4a06652a79c42cf0d23532b84d47d58b6c898076d88147030581ff121d25407e25ba0cd2b62c08885a27becfd0077ccdfdd5d77d4cdc5a0ab1ec2ad1303c93a9aae3ba0333e03e837b7838cc45d8f30b17ec09933b77debdfceea6cc31033c34cb25b88a025c683f0ff00593e3b96caa6a45153e1907e263c2815064492ea29d2ae467d16a0b1f927c724f5f7425c5d47ff064d099a13982ddb528ac5526829ffefb2502c91a0fa6b40da08a9bebf3b0b320e4895d4bee360047a7d48efb0f8a0a6e5fcdcb338a0436081588c2e0ceec3e3b2298c97085410d971d8da9eedcdc48e62fdbd125a2ba02f24c969da88b0cfe6318383edf10cbf2174c883ecd007d6bb79562663723099a0a9cbc30a1d257e3b651b3c304be3c253948c373f6980b22898af778ef9955b0fa0a343404514e213b6e5832f4621e6a6b1f0eb977811d759f06b7ad6da445092fba02eeb6ec27797e59283781b12f56e315f35b16395560a9c34bdd21280e942cfe1a0c874d2114c306baf91f0d43c2c76e30a6ad0da45273fb6d82246565d84c2538ea071a72e0b7d80490d26f95d3714cf8dd0c8b3c7b232980607aee525a9162e6308a04b4d1cf635eab18bbbb9e64109b0d992d759df23a96aab3d4a40e39a7c3becba80",
+ "0xf90211a023c9514b2fac99cd172f93d2db4d6578ec69fc2e0ef5c79fc76499ded09130eda00c645327db795a8a9c69282d86e3acece2901bdbb8047528ee8c78681b0e14b1a01e76e1109e1936249fdf08a4f583f91c01c6a4672794657bd74cbdd6ab3c2f3aa0a6b7b10c53412201d9ca7461b7210d6457a6b275b95033e2d8ca9778e5071c05a03e6f50fedb2b9f9f963356fc2899800e46a38169a794a1e6906c02ce8e8f0d59a0d4cec6a796cbab7e9cb28063423fcf157bac02de0cf1f0931303a701006019a2a0fb7e5187937b4203cf97bce2f345c9161bfbb9eb431a105356009e6ce18a1fbca02e63f3f119a82cedbf5e64e31aefeaa62892816846065c408c13f9bf9669d3d5a01c91aad4f4d391df268007fee771caedccc43369e07d6eb22fd45dab6beb0986a0672ca6ccfd8ac83f8974afefc9c9c2b58376991580158b16fd98cb9076cf8ea9a06cb3234503ad7116bb0e66257c646b2bce7603d774474a4f100cf93dde3eb594a085d0406435a6562f3370dd4b5de47a2b7d1084b9f25f0dbf49f54d2f46a2fc32a08bba9249e073397de4c46dd27688e9467b20fb8e61e04fd2594136cae26cd5c7a0a43fd38e14c38b64b84bb579b9a3cfab05cd5cc69c3a812a68e458acf72ea96ca0d848b40a2dbd5d8a4179e89fe8533f21db91982a0b8ecc1542c08ea27aec522aa0e03bc889898881f7ecb7caf0db5fb95125e26ff9e26bb85c33cbe360b50c9f1f80",
+ "0xf90191a017e9f741d070bfe3d975719f75663dabd5acdeb3678c50bd4872b817d7713d0b80a068f1f1d4e6b9697c43cffb6125e3bd1f4f10d57cb9dbd0938388fffd555a6457a0363162c748099d6ed114ead87c481972c1b0c124730b341c4ef1f963e0835a9880a09ef90c2a9ad234a08cbf4ea4ced535f227bc48becd22c1b05f2cbd586f018b73a0ae59576b0c1089b2828db7e54602ad0fabe4eff312ee5e0c21a04ac09b0e04d180a0fdbde0b4dbc02bb2ec472517c6f543de3673d33a6246a602479b43042ae9222da0c8d33ec13b89d8fd0ba487fe9cd61d18deff49bed48dc073384dc086a7361273a021253bb1110de615cede8c708a94cd6b898a61b5b1aaef8760b3341f232d63f0a06e7fbd4a6404e4acae007bb6147c58aebf6f19b3914a8d48b831bc3e50e5c05ea0c72d502ed522a796f823edefb721e64ff9d38904c9913b5be76f1e759c899b9da0fcc3a12a432daa29cb33f4a198624f6391124579ab7cbfae1eb30de076cfdd7980a0ab5019c60fef28581a93f0707739bbb24dcfbc2ffc550fb57aea59d91c79c04780",
+ "0xf86f9d3a1698215d94a53d304dacf1ae39aec65065a3307b0d96cd2acd29de94b84ff84d81ec8801e4e114d0153dada056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
+ ],
+ "balance": "0x1e4e114d0153dad",
+ "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
+ "nonce": "0xec",
+ "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
+ "storageProof": []
+ }
+}
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_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/header_20785012.json b/bolt-contracts/test/testdata/header_20785012.json
new file mode 100644
index 000000000..49a4b9a6a
--- /dev/null
+++ b/bolt-contracts/test/testdata/header_20785012.json
@@ -0,0 +1,5 @@
+{
+ "jsonrpc": "2.0",
+ "result": "0xf9025ca06be050fe1f6c7ffe8f30a350250a9ecc08ff3c031d129f65e1c10e5119d7a28ba01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479495222290dd7278aa3ddd389cc1e1d165cc4bafe5a0214389f55a96edbd4d5295a17ada4dbc68a3b276145bf824b060635f9905cefca087bb9183296ce9e3b7a3246f6d3a778b99a5d7daaba2174750707407c7297365a077c5d55d916e8abd4ca46cc769eb4b70b2693d25c8b67f3036a4a746fc512976b90100936ff6cbcd3707ce33a3e9fcf8b4ba995f7f0d23fedf7227548fefe25c19c8a878efed486844dc1942347a33dddb3fe703b5eadc8abe3cc43d9f1c72012f6132e7b4950acc4e1bed490f5eaab2e853b504b31dfb9e7d6cf8cae89d6c9ff0c6f3af0ce24e667c26f1c5cc78b791983eef172c1f6b80de7e2a1a627836b05ffc1a35683650b3c1e9e9bddf836e46eb4efc4d2a7dcba7e3defe7dbd8aceedbb93f263bf10ef5bb0fb1cf249d0c1ba9dddf3448fc4f5a6b3f3250ab9697089ad3f78119f203f2b3e7b1983198889c4fbd9347a33f93f9463dad70cf5ff6ff6b4e1867bfcb559e1c1bc2df2c5dedbf1244bb65f7cbc816bfeaad22f6fc8217a0516478084013d27748401c9c38084012a47bd8466ec2a6f8f6265617665726275696c642e6f7267a03417ec3f517a20587f89d4802cf478b2faea06b296b1637a8b87200c26433be1880000000000000000850505fd1914a08ae76a6bfb85b813966e007e021e8b4970c8cb02f8be771f5242b45f2e3946028302000083040000a04fa17978dd1135edb87244b53dbee105502454087c389d3638a6b2ac3bc6340c",
+ "id": 1
+}
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_20785012_1.json b/bolt-contracts/test/testdata/signed_tx_20785012_1.json
new file mode 100644
index 000000000..1551308e3
--- /dev/null
+++ b/bolt-contracts/test/testdata/signed_tx_20785012_1.json
@@ -0,0 +1,6 @@
+{
+ "hash": "0x9ec2c56ca36e445a46bc77ca77510f0ef21795d00834269f3752cbd29d63ba1f",
+ "sender": "0x0D9f5045B604bA0c050b5eb06D0b25d01c525Ea5",
+ "raw": "0x02f8740181eb8473a20d008507e172a822825208940ff71973b5243005b192d5bcf552fc2532b7bdec88015842095ebc400080c080a07a955ea50e980729226f547245bcf1e0e4ab69467cf82490a4bb0dbe9f35ae84a05f17d1a8f65b60905535ef0c36a8ec6d560c355949aeee2d3ab9791e7fe18fb4",
+ "blockNumber": 20785012
+}
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/transactions/README.md b/bolt-contracts/test/testdata/transactions/README.md
new file mode 100644
index 000000000..6ef0d5f28
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/README.md
@@ -0,0 +1,4 @@
+# Ethereum transaction test cases
+
+This directory contains test cases for Ethereum transactions taken from the
+[Ethers.js test suite](https://github.com/ethers-io/ethers.js/blob/5aba4963e3e8ddfc912747076f5b7fe7a743cfe2/testcases/transactions.json.gz).
diff --git a/bolt-contracts/test/testdata/transactions/random_0.json b/bolt-contracts/test/testdata/transactions/random_0.json
new file mode 100644
index 000000000..6d588f57b
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_0.json
@@ -0,0 +1,56 @@
+{
+ "name": "random-0",
+ "transaction": {
+ "to": "0x6Eb893e3466931517a04a17D153a6330c3f2f1dD",
+ "nonce": 648,
+ "gasLimit": "0x9d",
+ "gasPrice": "0x237e",
+ "maxFeePerGas": "0x346d9246",
+ "maxPriorityFeePerGas": "0x2c7e63",
+ "data": "0x889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33c",
+ "value": "0xc854",
+ "accessList": [],
+ "chainId": "0x8404bf1f",
+ "maxFeePerBlobGas": "0x2ddc4988",
+ "blobVersionedHashes": [
+ "0x01ecbc98026bec75cc99701e9dbf9d1a7edf9db4106e01c171822da8b1f7f392",
+ "0x016b567cab2d2fa35658d3bac34b81cf3cc667b9a9fbae9ed58ac961dfc5d96b"
+ ]
+ },
+ "privateKey": "0x2bf558dce44ca98616ee629199215ae5401c97040664637c48e3b74e66bcb3ae",
+ "unsignedLegacy": "0xf85682028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33c",
+ "unsignedEip155": "0xf85d82028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33c848404bf1f8080",
+ "unsignedBerlin": "0x01f85c848404bf1f82028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc0",
+ "unsignedLondon": "0x02f862848404bf1f820288832c7e6384346d9246819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc0",
+ "unsignedCancun": "0x03f8ab848404bf1f820288832c7e6384346d9246819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc0842ddc4988f842a001ecbc98026bec75cc99701e9dbf9d1a7edf9db4106e01c171822da8b1f7f392a0016b567cab2d2fa35658d3bac34b81cf3cc667b9a9fbae9ed58ac961dfc5d96b",
+ "signedLegacy": "0xf89982028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33c1ba0ba61344dc955b2f0e5dbc3c65e023e1c718539465131acb8a51b2ef75620114aa03366e9f2294bf2eca7322f3954b9b38745c40602239e3d7fa693667206907518",
+ "signedEip155": "0xf89e82028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33c850108097e62a0f141fe1b7e2fc1ed5d2b6ea4f04f92053e18f07274e2bda1c6852438c1895229a075553a7ae158a3fd46f75b547e847b59e2876f9a42de7a26d016db33232516de",
+ "signedBerlin": "0x01f89f848404bf1f82028882237e819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc080a0775f29642af1045b40e5beae8e6bce2dc9e222023b7a50372be6824dbb7434fba05dacfff85752a0b9fd860bc751c17235a670d318a8b9494d664c1b87e33ac8dd",
+ "signedLondon": "0x02f8a5848404bf1f820288832c7e6384346d9246819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc080a0f1003f96c6c6620dd46db36d2ae9f12d363947eb0db088c678b6ad1cf494aa6fa06085b5abbf448de5d622dc820da590cfdb6bb77b41c6650962b998a941f8d701",
+ "signedCancun": "0x03f8ee848404bf1f820288832c7e6384346d9246819d946eb893e3466931517a04a17d153a6330c3f2f1dd82c854b5889e365e59664fb881554ba1175519b5195b1d20390beb806d8f2cda7893e6f79848195dba4c905db6d7257ffb5eefea35f18ae33cc0842ddc4988f842a001ecbc98026bec75cc99701e9dbf9d1a7edf9db4106e01c171822da8b1f7f392a0016b567cab2d2fa35658d3bac34b81cf3cc667b9a9fbae9ed58ac961dfc5d96b01a092109dabedb5dcd157eddaee3ce39c4c467589b338878b3434181d93ea09c099a0015f8b0ca76ad70e4a69a15078232b77871b8c59b30b2279066064dff3afef47",
+ "signatureLegacy": {
+ "r": "0xba61344dc955b2f0e5dbc3c65e023e1c718539465131acb8a51b2ef75620114a",
+ "s": "0x3366e9f2294bf2eca7322f3954b9b38745c40602239e3d7fa693667206907518",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0xf141fe1b7e2fc1ed5d2b6ea4f04f92053e18f07274e2bda1c6852438c1895229",
+ "s": "0x75553a7ae158a3fd46f75b547e847b59e2876f9a42de7a26d016db33232516de",
+ "v": "0x108097e62"
+ },
+ "signatureBerlin": {
+ "r": "0x775f29642af1045b40e5beae8e6bce2dc9e222023b7a50372be6824dbb7434fb",
+ "s": "0x5dacfff85752a0b9fd860bc751c17235a670d318a8b9494d664c1b87e33ac8dd",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xf1003f96c6c6620dd46db36d2ae9f12d363947eb0db088c678b6ad1cf494aa6f",
+ "s": "0x6085b5abbf448de5d622dc820da590cfdb6bb77b41c6650962b998a941f8d701",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0x92109dabedb5dcd157eddaee3ce39c4c467589b338878b3434181d93ea09c099",
+ "s": "0x015f8b0ca76ad70e4a69a15078232b77871b8c59b30b2279066064dff3afef47",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_1.json b/bolt-contracts/test/testdata/transactions/random_1.json
new file mode 100644
index 000000000..2fa991680
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_1.json
@@ -0,0 +1,55 @@
+{
+ "name": "random-1",
+ "transaction": {
+ "to": "0x2E806e1F4B2C1Bc500E41b882213648Af39Dd4A2",
+ "nonce": 613,
+ "gasLimit": "0x3a",
+ "gasPrice": "0xf6",
+ "maxFeePerGas": "0x76cf803ef8c9",
+ "maxPriorityFeePerGas": "0x45",
+ "data": "0xb2f1646d",
+ "value": "0x45",
+ "accessList": [],
+ "chainId": "0xdf",
+ "maxFeePerBlobGas": "0x2e070f634f12e6",
+ "blobVersionedHashes": [
+ "0x0155aa189900c1c076d1a6ca7d1e65f09e62c5e4f0e929c0607c1ab2582b658a"
+ ]
+ },
+ "privateKey": "0xdde87f54e7dc562364d8559a1777b684a9121d132c4b4237e2534bd5a090166c",
+ "unsignedLegacy": "0xe182026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646d",
+ "unsignedEip155": "0xe582026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646d81df8080",
+ "unsignedBerlin": "0x01e481df82026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc0",
+ "unsignedLondon": "0x02ea81df820265458676cf803ef8c93a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc0",
+ "unsignedCancun": "0x03f85481df820265458676cf803ef8c93a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc0872e070f634f12e6e1a00155aa189900c1c076d1a6ca7d1e65f09e62c5e4f0e929c0607c1ab2582b658a",
+ "signedLegacy": "0xf86482026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646d1ca0c02ac8b415eefbe6ea2437788d50774aad5eda4c66a8c146b599b08ab67fac62a0491bf160ad32783a281f77c86f12483e1ce9a661680f05e54f6836f3048338f8",
+ "signedEip155": "0xf86682026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646d8201e1a0f08888fa9e155e7bbab69c3c1b30f8979de3dabcfe4ee848c99c5130a2dc6628a05e9b300a85b9ca65dabfa1b4718171abbd8393e9f9f01c6b0c628b575808bc84",
+ "signedBerlin": "0x01f86781df82026581f63a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc001a0f87200f49113aac563b703d7056f0bc21961cf72d4bd16ce814bb972182d95b8a023fca36c0c37daadeb1d067efb3e574089c77aa1832f83b6e6a5d48f7900ca33",
+ "signedLondon": "0x02f86d81df820265458676cf803ef8c93a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc001a00633d97e07870e5c1fa9e4f9f8beb169396225d5c691fddcf41ce3a3ea121e1ca0057f715c06932e29d5a4d8b70019c974a34d59e44e4ded4df7f5726ec8e87780",
+ "signedCancun": "0x03f89781df820265458676cf803ef8c93a942e806e1f4b2c1bc500e41b882213648af39dd4a24584b2f1646dc0872e070f634f12e6e1a00155aa189900c1c076d1a6ca7d1e65f09e62c5e4f0e929c0607c1ab2582b658a80a0392799f17b5f8c1cc8bfb7a29381a717c1b1298e4e3b9218be14c05ec32e50a3a03678448fa6438aee13af5e189fd4e45bd13b014882bc66fdcd9897e9732fb93b",
+ "signatureLegacy": {
+ "r": "0xc02ac8b415eefbe6ea2437788d50774aad5eda4c66a8c146b599b08ab67fac62",
+ "s": "0x491bf160ad32783a281f77c86f12483e1ce9a661680f05e54f6836f3048338f8",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0xf08888fa9e155e7bbab69c3c1b30f8979de3dabcfe4ee848c99c5130a2dc6628",
+ "s": "0x5e9b300a85b9ca65dabfa1b4718171abbd8393e9f9f01c6b0c628b575808bc84",
+ "v": "0x1e1"
+ },
+ "signatureBerlin": {
+ "r": "0xf87200f49113aac563b703d7056f0bc21961cf72d4bd16ce814bb972182d95b8",
+ "s": "0x23fca36c0c37daadeb1d067efb3e574089c77aa1832f83b6e6a5d48f7900ca33",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x0633d97e07870e5c1fa9e4f9f8beb169396225d5c691fddcf41ce3a3ea121e1c",
+ "s": "0x057f715c06932e29d5a4d8b70019c974a34d59e44e4ded4df7f5726ec8e87780",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x392799f17b5f8c1cc8bfb7a29381a717c1b1298e4e3b9218be14c05ec32e50a3",
+ "s": "0x3678448fa6438aee13af5e189fd4e45bd13b014882bc66fdcd9897e9732fb93b",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_10.json b/bolt-contracts/test/testdata/transactions/random_10.json
new file mode 100644
index 000000000..d9d5ee86d
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_10.json
@@ -0,0 +1,63 @@
+{
+ "name": "random-10",
+ "transaction": {
+ "to": "0xe80B2a2b7a84c886319faB83dF55E63C7539D2E7",
+ "nonce": 11,
+ "gasLimit": "0x66745b0460",
+ "gasPrice": "0x19b27a8a",
+ "maxFeePerGas": "0x9bc10525",
+ "maxPriorityFeePerGas": "0xde6421",
+ "data": "0xc67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184",
+ "value": "0x452f964f",
+ "accessList": [
+ {
+ "address": "0x9C1010eBD297f0af2110e4Be1356a99e088B70b0",
+ "storageKeys": [
+ "0x8f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcd",
+ "0xecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b513"
+ ]
+ }
+ ],
+ "chainId": "0x4e33268bd8",
+ "maxFeePerBlobGas": "0x5b8180c72d",
+ "blobVersionedHashes": [
+ "0x01fe39328d1867749d23972d03954f13d7025f6c7db432c678156c81b87ca044"
+ ]
+ },
+ "privateKey": "0x937f09851dd891844eee05f469dc776fe07a398b47ee3d064460f74fb00b8454",
+ "unsignedLegacy": "0xf8710b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184",
+ "unsignedEip155": "0xf8790b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184854e33268bd88080",
+ "unsignedBerlin": "0x01f8d4854e33268bd80b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b513",
+ "unsignedLondon": "0x02f8d8854e33268bd80b83de6421849bc105258566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b513",
+ "unsignedCancun": "0x03f90100854e33268bd80b83de6421849bc105258566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b513855b8180c72de1a001fe39328d1867749d23972d03954f13d7025f6c7db432c678156c81b87ca044",
+ "signedLegacy": "0xf8b40b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a9531841ba0f9c1a46fcbbeaa4915050ffc5e33043e372ab5b8e55e40c99bcea824b07ae62ba03a78b26033934d062ab52ac1b527df5b27bd08ced030d392832c89ca779dcc93",
+ "signedEip155": "0xf8b90b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184859c664d17d4a008a210072105a1ad68dc2b2811abd43dcd1dabaa2c2d2a2f669fb0dea7903535a028817f47aa0014375ed11fde0b733a5b3b648e5ffb2f66e3b4a0fc8db3000bcf",
+ "signedBerlin": "0x01f90117854e33268bd80b8419b27a8a8566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b51380a0a972d4365dac9b214ae1c1cdbdb70d5c10877eac1edaee2398012e6f084b99fca0184054b4a0b98483664b9469564eb21a28439982883f61bb1e538e968f74ce9d",
+ "signedLondon": "0x02f9011b854e33268bd80b83de6421849bc105258566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b51301a0abb1f9fd744bd588e3cc73caf61ea9a023aa8b486356f5bc97910ca01fe6453ca062ae4642f50df606c7587957e497e3867510554f88ba57a2bb93a4f8883329e5",
+ "signedCancun": "0x03f90143854e33268bd80b83de6421849bc105258566745b046094e80b2a2b7a84c886319fab83df55e63c7539d2e784452f964fb849c67a6a742250a5b9144dbfac7342e66a31df07bc2de714d5700d4a328600e6c274a8713cd12df6c64ca6f8088e1a49108dc171457cfcc9a4daf0625786bdf689e568fd61a63a953184f85bf859949c1010ebd297f0af2110e4be1356a99e088b70b0f842a08f4489956d5c84285dd2337de059733fd7caff5e3bc562d2e19f4c8416f7adcda0ecacb47c7f3b429a188ae196d5f6905999bdd4909022d3d5b3566399ef44b513855b8180c72de1a001fe39328d1867749d23972d03954f13d7025f6c7db432c678156c81b87ca04480a042db40cd1ebb1117bc52c8a9d2d467b008203571b1be9ad32db67ba24758cd27a024ac6a399e3a135a5986333a933bd431df1969ca12fdc5217aa7cc778110898b",
+ "signatureLegacy": {
+ "r": "0xf9c1a46fcbbeaa4915050ffc5e33043e372ab5b8e55e40c99bcea824b07ae62b",
+ "s": "0x3a78b26033934d062ab52ac1b527df5b27bd08ced030d392832c89ca779dcc93",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x08a210072105a1ad68dc2b2811abd43dcd1dabaa2c2d2a2f669fb0dea7903535",
+ "s": "0x28817f47aa0014375ed11fde0b733a5b3b648e5ffb2f66e3b4a0fc8db3000bcf",
+ "v": "0x9c664d17d4"
+ },
+ "signatureBerlin": {
+ "r": "0xa972d4365dac9b214ae1c1cdbdb70d5c10877eac1edaee2398012e6f084b99fc",
+ "s": "0x184054b4a0b98483664b9469564eb21a28439982883f61bb1e538e968f74ce9d",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xabb1f9fd744bd588e3cc73caf61ea9a023aa8b486356f5bc97910ca01fe6453c",
+ "s": "0x62ae4642f50df606c7587957e497e3867510554f88ba57a2bb93a4f8883329e5",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x42db40cd1ebb1117bc52c8a9d2d467b008203571b1be9ad32db67ba24758cd27",
+ "s": "0x24ac6a399e3a135a5986333a933bd431df1969ca12fdc5217aa7cc778110898b",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_11.json b/bolt-contracts/test/testdata/transactions/random_11.json
new file mode 100644
index 000000000..9af3b0e7f
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_11.json
@@ -0,0 +1,78 @@
+{
+ "name": "random-11",
+ "transaction": {
+ "to": "0x84c0Ab0b0Bb392Fc645293Fd9F88ce734F520C3d",
+ "nonce": 152,
+ "gasLimit": "0x27",
+ "gasPrice": "0x5d0f93b5b0",
+ "maxFeePerGas": "0x5966bba5",
+ "maxPriorityFeePerGas": "0x5017",
+ "data": "0x0c5c6b4f63948ee9bd3027",
+ "value": "0x6e555229e2",
+ "accessList": [
+ {
+ "address": "0x43b314A73dd9368a2C0227F3cc1315818Db914D8",
+ "storageKeys": [
+ "0xe0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef"
+ ]
+ },
+ {
+ "address": "0x4eA79A74f60B6Bf2688B46fa9Ba5db593098740b",
+ "storageKeys": [
+ "0xe0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef"
+ ]
+ },
+ {
+ "address": "0x6eB7Fd5fdfD7F48e137da1b368287f2C6Cc12b45",
+ "storageKeys": [
+ "0xe0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef"
+ ]
+ },
+ {
+ "address": "0xb8EcE452e0840B0b30D0BCB4d1400B545cfb746F",
+ "storageKeys": [
+ "0xe0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef"
+ ]
+ }
+ ],
+ "chainId": "0xdb4137d499",
+ "maxFeePerBlobGas": "0x1ba30e3498",
+ "blobVersionedHashes": []
+ },
+ "privateKey": "0x335dca53da48dba1ac89b63618acd056621774dd60a7ef7f891f64c8c659e7b3",
+ "unsignedLegacy": "0xf08198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027",
+ "unsignedEip155": "0xf8388198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd302785db4137d4998080",
+ "unsignedBerlin": "0x01f9011885db4137d4998198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef",
+ "unsignedLondon": "0x02f9011a85db4137d4998198825017845966bba5279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef",
+ "unsignedCancun": "0x03f9012185db4137d4998198825017845966bba5279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef851ba30e3498c0",
+ "signedLegacy": "0xf8738198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd30271ca07cf7847c9f6bf2ac5b12866c8977bffe203b8674bc35a0c789e60913803298c4a02388e993aa07c5f28a6e31bd8189597ccd314b6ff803b202cc700add8bb907e8",
+ "signedEip155": "0xf8798198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd30278601b6826fa956a016821219866f3c43a9d6705be5c99bed3443b663be66036ed3cd6d1b922cf6dca013496a38cc9a45ae5f04a7539adfd4961b6527d8ca203438db0538fa13ee07a3",
+ "signedBerlin": "0x01f9015b85db4137d4998198855d0f93b5b0279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef80a0c09b02589bf56592b078fa358f37f0349debe1561378ddda9c195c8cbbad6aaca02ce613405a1a0fb0cf7c11e2ae6a13d16dc05b4b54fa29fe659f0d3486b0748d",
+ "signedLondon": "0x02f9015d85db4137d4998198825017845966bba5279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef01a00291267b646d31657e80c87018fac49779ec0b4b8acfea5b5e5875b2bc574389a028c4e6e7dc6aa417199de1a0ba1deb90fbd9c42818cd926c4d50c5de61cf733a",
+ "signedCancun": "0x03f9016485db4137d4998198825017845966bba5279484c0ab0b0bb392fc645293fd9f88ce734f520c3d856e555229e28b0c5c6b4f63948ee9bd3027f8e0f79443b314a73dd9368a2c0227f3cc1315818db914d8e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7944ea79a74f60b6bf2688b46fa9ba5db593098740be1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff7946eb7fd5fdfd7f48e137da1b368287f2c6cc12b45e1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051eff794b8ece452e0840b0b30d0bcb4d1400b545cfb746fe1a0e0e82939483d467088aae991d873c23719321b29648506c612ad2a06194051ef851ba30e3498c080a0b0b4ec97964ed7aa3a66e0053ab4163c8b346b2150f581ae09ac7c62f810543ba00e27aac4faaa5fc4634ed8b64f50aaf74156b90d561db528b255242fe624ae0e",
+ "signatureLegacy": {
+ "r": "0x7cf7847c9f6bf2ac5b12866c8977bffe203b8674bc35a0c789e60913803298c4",
+ "s": "0x2388e993aa07c5f28a6e31bd8189597ccd314b6ff803b202cc700add8bb907e8",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x16821219866f3c43a9d6705be5c99bed3443b663be66036ed3cd6d1b922cf6dc",
+ "s": "0x13496a38cc9a45ae5f04a7539adfd4961b6527d8ca203438db0538fa13ee07a3",
+ "v": "0x1b6826fa956"
+ },
+ "signatureBerlin": {
+ "r": "0xc09b02589bf56592b078fa358f37f0349debe1561378ddda9c195c8cbbad6aac",
+ "s": "0x2ce613405a1a0fb0cf7c11e2ae6a13d16dc05b4b54fa29fe659f0d3486b0748d",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0x0291267b646d31657e80c87018fac49779ec0b4b8acfea5b5e5875b2bc574389",
+ "s": "0x28c4e6e7dc6aa417199de1a0ba1deb90fbd9c42818cd926c4d50c5de61cf733a",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0xb0b4ec97964ed7aa3a66e0053ab4163c8b346b2150f581ae09ac7c62f810543b",
+ "s": "0x0e27aac4faaa5fc4634ed8b64f50aaf74156b90d561db528b255242fe624ae0e",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_12.json b/bolt-contracts/test/testdata/transactions/random_12.json
new file mode 100644
index 000000000..7c104ce7a
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_12.json
@@ -0,0 +1,78 @@
+{
+ "name": "random-12",
+ "transaction": {
+ "to": "0x4F18B878B82C8931c4A6d67FC28b857AaFf6D764",
+ "nonce": 547,
+ "gasLimit": "0xfa26c05691",
+ "gasPrice": "0x892d19ea64",
+ "maxFeePerGas": "0x6bf86f84",
+ "maxPriorityFeePerGas": "0xf627",
+ "data": "0x57d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8",
+ "value": "0x64660ccd",
+ "accessList": [
+ {
+ "address": "0x275677ef49D8315ed641fe3b6519C4dC3f099112",
+ "storageKeys": [
+ "0x819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457b",
+ "0xb76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d"
+ ]
+ },
+ {
+ "address": "0xa02363D8A2921bF57F7Dc48B42Ccc04548554E81",
+ "storageKeys": [
+ "0x819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457b",
+ "0xb76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d"
+ ]
+ },
+ {
+ "address": "0x8D6796be448CEFf2333157dEE56C74AF0aFb0BD5",
+ "storageKeys": [
+ "0x819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457b",
+ "0xb76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d"
+ ]
+ }
+ ],
+ "chainId": "0x44af2c",
+ "maxFeePerBlobGas": "0x12027a6466afd6",
+ "blobVersionedHashes": [
+ "0x01c67453c9a170645c5784da796dc3aaaf4933d5e8d9c99ae1fd3d8032b0631f",
+ "0x016865cb3a8b41dabbd03465926a9bedfb91914dd973d05e325f3d77a7db1d8e"
+ ]
+ },
+ "privateKey": "0x3d04ff4d2657142a58d3cc787e089ca7f20a6b66776ed5b04dc9d4dceef253d7",
+ "unsignedLegacy": "0xf87182022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8",
+ "unsignedEip155": "0xf87782022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f88344af2c8080",
+ "unsignedBerlin": "0x01f901898344af2c82022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d",
+ "unsignedLondon": "0x02f9018b8344af2c82022382f627846bf86f8485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d",
+ "unsignedCancun": "0x03f901d78344af2c82022382f627846bf86f8485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d8712027a6466afd6f842a001c67453c9a170645c5784da796dc3aaaf4933d5e8d9c99ae1fd3d8032b0631fa0016865cb3a8b41dabbd03465926a9bedfb91914dd973d05e325f3d77a7db1d8e",
+ "signedLegacy": "0xf8b482022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f81ca01c1a043ba56d5a6edec9efd4b24695f996204caec12978d32bf086d8e21bcf96a04a5144d22ab0ddd5335bc3e1b22cd24e66bd742865f1a4447e2675ee00a2227f",
+ "signedEip155": "0xf8b782022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f883895e7ca06d75cb007c3b91b7465c095e28b841d93da110bbb66280a0f5920083ad4e80f2a026b07e9427fd6dc320c23ced73fb8aae9872af808f20db04f0bb67287da0373b",
+ "signedBerlin": "0x01f901cc8344af2c82022385892d19ea6485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d80a0f69b776f4eee48cf9a220abb4e132d2cd915b320e409fe99376535aa6984f69ea045b9071a002c820e368070525657f2cd47053ea600d24c1da75b7adbd076cfcf",
+ "signedLondon": "0x02f901ce8344af2c82022382f627846bf86f8485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d01a074ffa09bd37c038326cebdd9794d5f90fe95aa9210e903fb6a5c44a735bbafc6a01605e0f1db258100a878445341892825483f14cfa7b9bde7bd001f4f3e6eed4a",
+ "signedCancun": "0x03f9021a8344af2c82022382f627846bf86f8485fa26c05691944f18b878b82c8931c4a6d67fc28b857aaff6d7648464660ccdb84657d10e77f6d8cd44acf5914b7b73eca98b9afe235e07b9db2d1712c4adc287e3e474a377404de19203f5127b676c88c839a76e15e665f183709a3460025556e5fd5f2a2f17f8f90111f85994275677ef49d8315ed641fe3b6519c4dc3f099112f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df85994a02363d8a2921bf57f7dc48b42ccc04548554e81f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6df859948d6796be448ceff2333157dee56c74af0afb0bd5f842a0819da32335b67b104a5c786c41f2811e1167a1245b4998eb9297d0b777db457ba0b76e298e9f4a7cb7e30809076edbb99bcbc310ec27d7a563cbc24cddd0a2ae6d8712027a6466afd6f842a001c67453c9a170645c5784da796dc3aaaf4933d5e8d9c99ae1fd3d8032b0631fa0016865cb3a8b41dabbd03465926a9bedfb91914dd973d05e325f3d77a7db1d8e01a06bb2134653010ab0cec4aa5e51d66acf4b9c0835477bed4a96949fe0cc15db35a06432039ef421f4cf53ada8b7149e0852cc705668a6713d00aa0ddbfc9b84604f",
+ "signatureLegacy": {
+ "r": "0x1c1a043ba56d5a6edec9efd4b24695f996204caec12978d32bf086d8e21bcf96",
+ "s": "0x4a5144d22ab0ddd5335bc3e1b22cd24e66bd742865f1a4447e2675ee00a2227f",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x6d75cb007c3b91b7465c095e28b841d93da110bbb66280a0f5920083ad4e80f2",
+ "s": "0x26b07e9427fd6dc320c23ced73fb8aae9872af808f20db04f0bb67287da0373b",
+ "v": "0x895e7c"
+ },
+ "signatureBerlin": {
+ "r": "0xf69b776f4eee48cf9a220abb4e132d2cd915b320e409fe99376535aa6984f69e",
+ "s": "0x45b9071a002c820e368070525657f2cd47053ea600d24c1da75b7adbd076cfcf",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0x74ffa09bd37c038326cebdd9794d5f90fe95aa9210e903fb6a5c44a735bbafc6",
+ "s": "0x1605e0f1db258100a878445341892825483f14cfa7b9bde7bd001f4f3e6eed4a",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x6bb2134653010ab0cec4aa5e51d66acf4b9c0835477bed4a96949fe0cc15db35",
+ "s": "0x6432039ef421f4cf53ada8b7149e0852cc705668a6713d00aa0ddbfc9b84604f",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_13.json b/bolt-contracts/test/testdata/transactions/random_13.json
new file mode 100644
index 000000000..e46f587f3
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_13.json
@@ -0,0 +1,88 @@
+{
+ "name": "random-13",
+ "transaction": {
+ "to": "0x02CA24BF68010E087C89FBBBBA726265f75F453f",
+ "nonce": 230,
+ "gasLimit": "0x9b",
+ "gasPrice": "0xee0c40da",
+ "maxFeePerGas": "0x6911bd4911",
+ "maxPriorityFeePerGas": "0x8d",
+ "data": "0x9057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18c",
+ "value": "0xe5cf3dc063",
+ "accessList": [
+ {
+ "address": "0x9a39070F38D75E2c56dbb0f5992F87D4B8c4516f",
+ "storageKeys": [
+ "0x5a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6f",
+ "0xbb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6",
+ "0x573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63"
+ ]
+ },
+ {
+ "address": "0x727e5f27d18F70fDA8F234082421872B68C3Dda0",
+ "storageKeys": [
+ "0x5a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6f",
+ "0xbb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6",
+ "0x573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63"
+ ]
+ },
+ {
+ "address": "0x81df03F5cd0f415eAB92E0593998Dd288B02d583",
+ "storageKeys": [
+ "0x5a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6f",
+ "0xbb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6",
+ "0x573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63"
+ ]
+ },
+ {
+ "address": "0x3C465948C080239deECAbc2b8A7fccbe4130B3E6",
+ "storageKeys": [
+ "0x5a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6f",
+ "0xbb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6",
+ "0x573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63"
+ ]
+ }
+ ],
+ "chainId": "0x2a",
+ "maxFeePerBlobGas": "0x8d5e0eac8355",
+ "blobVersionedHashes": [
+ "0x014fe494c02bd67264c92caae4891a3b0888fdc0bd520c0872ba0d540991d2ef"
+ ]
+ },
+ "privateKey": "0x70dae63ff96bc4831355131fc24468c3e57dbac4c0a82334953baa752df10cff",
+ "unsignedLegacy": "0xf88b81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18c",
+ "unsignedEip155": "0xf88e81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18c2a8080",
+ "unsignedBerlin": "0x01f9027f2a81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63",
+ "unsignedLondon": "0x02f902822a81e6818d856911bd4911819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63",
+ "unsignedCancun": "0x03f902ab2a81e6818d856911bd4911819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63868d5e0eac8355e1a0014fe494c02bd67264c92caae4891a3b0888fdc0bd520c0872ba0d540991d2ef",
+ "signedLegacy": "0xf8ce81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18c1ca01f91e4f825a52eb8bbaa3be1cb1cb861857cf6d0db05543a91f4e201afe63634a030680d1f0dec57bf64f8fa85a835fcfca06aa5108c7a46b60df9da8572518087",
+ "signedEip155": "0xf8ce81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18c78a05cb862c473f3dfdeaa0e8520291cb4ba8c3a62550a6ca0e404d51ef8791cf7f8a052d4ad17f007fa089126d119c609adb461436fd4e07640c0fc487bb444caea99",
+ "signedBerlin": "0x01f902c22a81e684ee0c40da819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab6301a04f2bb0a31a56bb2b1aed5089f7a05c1dd10d759fafdc07ad9ed698885981412ba0042167f0f1ba66c56f1b39f5ab4ad95442cf98c2e4b0a4066f4995b22a663bf3",
+ "signedLondon": "0x02f902c52a81e6818d856911bd4911819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab6301a03fd00e5a6b50a78f53922d62a73777ccfe7695f1e6e30f8be33323119bc2c0efa0050ac86674b894e33b3292bb153eb1fbefa4efdaf5775eba835391477f6831c4",
+ "signedCancun": "0x03f902ee2a81e6818d856911bd4911819b9402ca24bf68010e087c89fbbbba726265f75f453f85e5cf3dc063b8659057177cbea0fcc2e52f03fd99ae750cd5e1df00a897ae7cc0a37f609358089d09690aa727fc2a5514964930ebcdfaa64d48ca730848ba3bdf55dfafcdbc2615fcc0aff5b5804bc592b4a9129b30858f7e56d895e655ee66b8d39a5fd8e22590fec373b18cf901f0f87a949a39070f38d75e2c56dbb0f5992f87d4b8c4516ff863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a94727e5f27d18f70fda8f234082421872b68c3dda0f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a9481df03f5cd0f415eab92e0593998dd288b02d583f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63f87a943c465948c080239deecabc2b8a7fccbe4130b3e6f863a05a2edcbe00c7f2a4054eb642539fc70f3b18e636eee4a800a307d1b38bf2cf6fa0bb923a5c3ae3b14becbfc2ce1f9bcac3ac7f9e529bf121ebe45589f97ea7a5d6a0573a0bef0d2674001e858247d3fe613ca0c17a6b733979e6516af7fa8ef3ab63868d5e0eac8355e1a0014fe494c02bd67264c92caae4891a3b0888fdc0bd520c0872ba0d540991d2ef01a061188dabcd2879c18c49f3f83cb4663d597da58f856390b87921f64182ea9745a0757d18b772ff141ddff69196b2755bd76ef422a102a3a0fa1ca68b2e901b468c",
+ "signatureLegacy": {
+ "r": "0x1f91e4f825a52eb8bbaa3be1cb1cb861857cf6d0db05543a91f4e201afe63634",
+ "s": "0x30680d1f0dec57bf64f8fa85a835fcfca06aa5108c7a46b60df9da8572518087",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x5cb862c473f3dfdeaa0e8520291cb4ba8c3a62550a6ca0e404d51ef8791cf7f8",
+ "s": "0x52d4ad17f007fa089126d119c609adb461436fd4e07640c0fc487bb444caea99",
+ "v": "0x78"
+ },
+ "signatureBerlin": {
+ "r": "0x4f2bb0a31a56bb2b1aed5089f7a05c1dd10d759fafdc07ad9ed698885981412b",
+ "s": "0x042167f0f1ba66c56f1b39f5ab4ad95442cf98c2e4b0a4066f4995b22a663bf3",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x3fd00e5a6b50a78f53922d62a73777ccfe7695f1e6e30f8be33323119bc2c0ef",
+ "s": "0x050ac86674b894e33b3292bb153eb1fbefa4efdaf5775eba835391477f6831c4",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x61188dabcd2879c18c49f3f83cb4663d597da58f856390b87921f64182ea9745",
+ "s": "0x757d18b772ff141ddff69196b2755bd76ef422a102a3a0fa1ca68b2e901b468c",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_14.json b/bolt-contracts/test/testdata/transactions/random_14.json
new file mode 100644
index 000000000..b2bec0420
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_14.json
@@ -0,0 +1,71 @@
+{
+ "name": "random-14",
+ "transaction": {
+ "to": "0xd4Ed79414A9F8bF363E9CAaa5A74380716F7dBE0",
+ "nonce": 355,
+ "gasLimit": "0xc3393abf8e",
+ "gasPrice": "0x530f336b",
+ "maxFeePerGas": "0xc4550ba0e2",
+ "maxPriorityFeePerGas": "0xf9fe",
+ "data": "0x9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82",
+ "value": "0x5590f0f87b",
+ "accessList": [
+ {
+ "address": "0x3acE6cc41a8DF5B6518B24e6ecd490c13aCfC677",
+ "storageKeys": []
+ },
+ {
+ "address": "0x65F3540A4A7aa93d074A77313622786513f0199d",
+ "storageKeys": []
+ },
+ {
+ "address": "0x0dad5e012C02f680FB256bf6b0e0d9999c8c74A0",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0x602a",
+ "maxFeePerBlobGas": "0xe61327d8d776ed",
+ "blobVersionedHashes": [
+ "0x0122070325084d4e3cba54d4dffef3501e22290c965e099620b3dbd48626b731",
+ "0x018df851a2bdd53f12054dc427d35171bbc81e2141c0ef7afe94964e4cdd3c1f",
+ "0x01386600d37c5951685dc66bc09b1a3e98c040621dde84b172cf93c30d66842f",
+ "0x017f6d70f99ec3a8937f73f2da032c78dde8a6f831befc31fd23189ff66e4b07"
+ ]
+ },
+ "privateKey": "0x6c268eef2efdb15a96ae69774e732b9214a3ebb03c0fd01602bc7a5fcd21c060",
+ "unsignedLegacy": "0xf85782016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82",
+ "unsignedEip155": "0xf85c82016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe8282602a8080",
+ "unsignedBerlin": "0x01f8a182602a82016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c0",
+ "unsignedLondon": "0x02f8a582602a82016382f9fe85c4550ba0e285c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c0",
+ "unsignedCancun": "0x03f9013382602a82016382f9fe85c4550ba0e285c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c087e61327d8d776edf884a00122070325084d4e3cba54d4dffef3501e22290c965e099620b3dbd48626b731a0018df851a2bdd53f12054dc427d35171bbc81e2141c0ef7afe94964e4cdd3c1fa001386600d37c5951685dc66bc09b1a3e98c040621dde84b172cf93c30d66842fa0017f6d70f99ec3a8937f73f2da032c78dde8a6f831befc31fd23189ff66e4b07",
+ "signedLegacy": "0xf89a82016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe821ca03e5ece8f862a9b5d42b42c314dceef20c98a55ae8e89e76ea36574f2dbcd0a6fa06cf670f45c89f30ccf175619d7a48c30c0303d723e8c942afe3bf99bb622a365",
+ "signedEip155": "0xf89c82016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe8282c077a07984b5662bf52721770ca23a49f719f0dca47263a736a7cb6da28b359c3275d5a0080a94bd846cb4d5d71a41c78f5f4a893f9cd8316a52f3bba02b2e6d5865d13e",
+ "signedBerlin": "0x01f8e482602a82016384530f336b85c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c001a0a0983d54539712f6f848fe9fad8631479338d8277207d75ccf4a26ae3ed34e79a00acd07196e5fa670c9109ced9ccf4722f2c33ae49eec06f14ea3766c6d0e7c3e",
+ "signedLondon": "0x02f8e882602a82016382f9fe85c4550ba0e285c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c001a0990f968b8f06d13ac77614e1acd01591259050753e76bc7419062bdf6ac71a11a0201883043f4f176b82d7cc14ffd524f28fcf9cc490cea858bacac84d7373261a",
+ "signedCancun": "0x03f9017682602a82016382f9fe85c4550ba0e285c3393abf8e94d4ed79414a9f8bf363e9caaa5a74380716f7dbe0855590f0f87bad9421f8e15d313f08424f56798539e338ff345b530bef1256810200922813e50a176ca7e987dbedb94917acfe82f845d6943ace6cc41a8df5b6518b24e6ecd490c13acfc677c0d69465f3540a4a7aa93d074a77313622786513f0199dc0d6940dad5e012c02f680fb256bf6b0e0d9999c8c74a0c087e61327d8d776edf884a00122070325084d4e3cba54d4dffef3501e22290c965e099620b3dbd48626b731a0018df851a2bdd53f12054dc427d35171bbc81e2141c0ef7afe94964e4cdd3c1fa001386600d37c5951685dc66bc09b1a3e98c040621dde84b172cf93c30d66842fa0017f6d70f99ec3a8937f73f2da032c78dde8a6f831befc31fd23189ff66e4b0780a0da2f7f2f95d6b3207aeb56b5b6aa5aa30a2674f90d338427674c8e5e7850f078a0646d4c7d050a38c73bf1f22b8ba41e2a84430c3550ec20843e885248cc90b53b",
+ "signatureLegacy": {
+ "r": "0x3e5ece8f862a9b5d42b42c314dceef20c98a55ae8e89e76ea36574f2dbcd0a6f",
+ "s": "0x6cf670f45c89f30ccf175619d7a48c30c0303d723e8c942afe3bf99bb622a365",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x7984b5662bf52721770ca23a49f719f0dca47263a736a7cb6da28b359c3275d5",
+ "s": "0x080a94bd846cb4d5d71a41c78f5f4a893f9cd8316a52f3bba02b2e6d5865d13e",
+ "v": "0xc077"
+ },
+ "signatureBerlin": {
+ "r": "0xa0983d54539712f6f848fe9fad8631479338d8277207d75ccf4a26ae3ed34e79",
+ "s": "0x0acd07196e5fa670c9109ced9ccf4722f2c33ae49eec06f14ea3766c6d0e7c3e",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x990f968b8f06d13ac77614e1acd01591259050753e76bc7419062bdf6ac71a11",
+ "s": "0x201883043f4f176b82d7cc14ffd524f28fcf9cc490cea858bacac84d7373261a",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0xda2f7f2f95d6b3207aeb56b5b6aa5aa30a2674f90d338427674c8e5e7850f078",
+ "s": "0x646d4c7d050a38c73bf1f22b8ba41e2a84430c3550ec20843e885248cc90b53b",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_15.json b/bolt-contracts/test/testdata/transactions/random_15.json
new file mode 100644
index 000000000..1be8cfac2
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_15.json
@@ -0,0 +1,76 @@
+{
+ "name": "random-15",
+ "transaction": {
+ "to": "0x2C4Ad0E9a08DD6fE31Ba497fd4a893A0C1fc6B34",
+ "nonce": 252,
+ "gasLimit": "0x50ef8434",
+ "gasPrice": "0xd9ca73",
+ "maxFeePerGas": "0x280244cd52ab",
+ "maxPriorityFeePerGas": "0x143b17",
+ "data": "0x59a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92d",
+ "value": "0x6f9f",
+ "accessList": [
+ {
+ "address": "0x3266CE0AB053240CE5d95993d420c6b9a1dA1fDC",
+ "storageKeys": [
+ "0xa0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211",
+ "0x80b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0",
+ "0xa628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8",
+ "0x35167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1"
+ ]
+ },
+ {
+ "address": "0xf199b9f871065E2E18291CdeC3BF7C1eD32FBa0a",
+ "storageKeys": [
+ "0xa0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211",
+ "0x80b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0",
+ "0xa628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8",
+ "0x35167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1"
+ ]
+ }
+ ],
+ "chainId": "0x18314c",
+ "maxFeePerBlobGas": "0x81ec2df3d713c4",
+ "blobVersionedHashes": [
+ "0x01d63574eb4fd029b67d6233aeae60815e225597f647de565b8050d2b5a876b3",
+ "0x01224249cfed8dce6d9785f85c2ca29b6496d91ca16dbafabf1581ef75f47b29",
+ "0x01a4547275516123b7b2ea3413c57c4caea24441dd9956f7308f09b5bb0947df"
+ ]
+ },
+ "privateKey": "0xffe260f37af5da6e4a18799bc6b7f0fab7d57bc456346bca7261a53cb04fb48d",
+ "unsignedLegacy": "0xf85781fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92d",
+ "unsignedEip155": "0xf85d81fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92d8318314c8080",
+ "unsignedBerlin": "0x01f901988318314c81fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1",
+ "unsignedLondon": "0x02f9019f8318314c81fc83143b1786280244cd52ab8450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1",
+ "unsignedCancun": "0x03f9020c8318314c81fc83143b1786280244cd52ab8450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b18781ec2df3d713c4f863a001d63574eb4fd029b67d6233aeae60815e225597f647de565b8050d2b5a876b3a001224249cfed8dce6d9785f85c2ca29b6496d91ca16dbafabf1581ef75f47b29a001a4547275516123b7b2ea3413c57c4caea24441dd9956f7308f09b5bb0947df",
+ "signedLegacy": "0xf89a81fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92d1ba0c32aaeca7f3f7d189f6493edfb99452baa5e5a4c3ba136275b92c28a722a59f7a07073e03733555ceb1a3479c3193cfac043530c657b1c26cf2d12ffac912d132d",
+ "signedEip155": "0xf89d81fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92d833062bca02a5acf0ccf63515e394899176fdc70cfc880e8f34ba5199de5baa72e0dc11e8fa07560075eef0250df34dd68a1854864b219ab0972e96e3d70017b21f608c5e928",
+ "signedBerlin": "0x01f901db8318314c81fc83d9ca738450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b101a0c46f5930fbf08b22c97615c2d8db15697dfa5d0a269bb91a9afd7ddb7ac44768a066e4c7ad9c783be0dd722b4b85dc6518892f8c632c09b2ec2c0980c84fb897b7",
+ "signedLondon": "0x02f901e28318314c81fc83143b1786280244cd52ab8450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b180a0bdebff14388befbaf0d7c8509f82355aafd0cbdad7ba5d721fa6357e239cf380a06efc7089e04a05f4e52b43fb3799e6ebd9274128566e870efef293a9d8624271",
+ "signedCancun": "0x03f9024f8318314c81fc83143b1786280244cd52ab8450ef8434942c4ad0e9a08dd6fe31ba497fd4a893a0c1fc6b34826f9fb359a1b826916e4a012ffaaf5e21251e27317a1f565a716b8ff184d8c689f93f5a7359abcbffd18d7bea36ee123ce5cddbbdd92df9013af89b943266ce0ab053240ce5d95993d420c6b9a1da1fdcf884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b1f89b94f199b9f871065e2e18291cdec3bf7c1ed32fba0af884a0a0568d48a6cc9e0c92080dec2155382a276181f25c746da97f8199f3a8464211a080b165a49183e897da692ca460c5c88a0597e5e4ad7b43471bac25bfd9a780e0a0a628e4d1cfbcc69964473b21b3df585f15eca27bdd2e0c0d7e2f1db3e2c0a1f8a035167ab9f9061b5f986f5c960aebe912340a5a72935aa334afa8fbfe586d05b18781ec2df3d713c4f863a001d63574eb4fd029b67d6233aeae60815e225597f647de565b8050d2b5a876b3a001224249cfed8dce6d9785f85c2ca29b6496d91ca16dbafabf1581ef75f47b29a001a4547275516123b7b2ea3413c57c4caea24441dd9956f7308f09b5bb0947df80a04214e832f45d5ba706f01b7dce5941397c8f0850ddfc30d0bf70e896e542bbd1a027208b27598709a564ca5981d54c0c3aa4b33b7c618de45f378576b229bf07f5",
+ "signatureLegacy": {
+ "r": "0xc32aaeca7f3f7d189f6493edfb99452baa5e5a4c3ba136275b92c28a722a59f7",
+ "s": "0x7073e03733555ceb1a3479c3193cfac043530c657b1c26cf2d12ffac912d132d",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x2a5acf0ccf63515e394899176fdc70cfc880e8f34ba5199de5baa72e0dc11e8f",
+ "s": "0x7560075eef0250df34dd68a1854864b219ab0972e96e3d70017b21f608c5e928",
+ "v": "0x3062bc"
+ },
+ "signatureBerlin": {
+ "r": "0xc46f5930fbf08b22c97615c2d8db15697dfa5d0a269bb91a9afd7ddb7ac44768",
+ "s": "0x66e4c7ad9c783be0dd722b4b85dc6518892f8c632c09b2ec2c0980c84fb897b7",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0xbdebff14388befbaf0d7c8509f82355aafd0cbdad7ba5d721fa6357e239cf380",
+ "s": "0x6efc7089e04a05f4e52b43fb3799e6ebd9274128566e870efef293a9d8624271",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0x4214e832f45d5ba706f01b7dce5941397c8f0850ddfc30d0bf70e896e542bbd1",
+ "s": "0x27208b27598709a564ca5981d54c0c3aa4b33b7c618de45f378576b229bf07f5",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_16.json b/bolt-contracts/test/testdata/transactions/random_16.json
new file mode 100644
index 000000000..10608fce7
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_16.json
@@ -0,0 +1,64 @@
+{
+ "name": "random-16",
+ "transaction": {
+ "to": "0xb26832F9Eebe1bb94cb67D5C6bABF358d15f834E",
+ "nonce": 306,
+ "gasLimit": "0xfb38",
+ "gasPrice": "0x7029692f45",
+ "maxFeePerGas": "0xf631d7b0a81f",
+ "maxPriorityFeePerGas": "0xb5",
+ "data": "0x3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779",
+ "value": "0xb3",
+ "accessList": [
+ {
+ "address": "0x820A408021ed575Bd94e89AF5672C9C4FaAA93AD",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0x03",
+ "maxFeePerBlobGas": "0x9b643a13",
+ "blobVersionedHashes": [
+ "0x0106f3b8583c3b187099347666d5b01b3ae85b5458a2291ca45884084bf8a728",
+ "0x012d5ba14538fe600b4f258429247337527a13d6a28c3b4b445f00d3098df94f",
+ "0x011a4647e1b61123ab33b318dff3609fc11da8c4cfa9abe25f99c41923a9f581",
+ "0x01cb8b689043f5bdcc56fce5c26490ca28a88b0a2f53a6d1f51e3f298e65d295",
+ "0x01531f010e1e10d3d3332b2caef340f0a6af1e366a98126ef035c3ce80b8a1db"
+ ]
+ },
+ "privateKey": "0x504264c14141b98fdad31cff441c443d69458e4033c11eeb0b4928ce9452c5a1",
+ "unsignedLegacy": "0xf890820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779",
+ "unsignedEip155": "0xf893820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779038080",
+ "unsignedBerlin": "0x01f8a903820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc0",
+ "unsignedLondon": "0x02f8ac0382013281b586f631d7b0a81f82fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc0",
+ "unsignedCancun": "0x03f901580382013281b586f631d7b0a81f82fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc0849b643a13f8a5a00106f3b8583c3b187099347666d5b01b3ae85b5458a2291ca45884084bf8a728a0012d5ba14538fe600b4f258429247337527a13d6a28c3b4b445f00d3098df94fa0011a4647e1b61123ab33b318dff3609fc11da8c4cfa9abe25f99c41923a9f581a001cb8b689043f5bdcc56fce5c26490ca28a88b0a2f53a6d1f51e3f298e65d295a001531f010e1e10d3d3332b2caef340f0a6af1e366a98126ef035c3ce80b8a1db",
+ "signedLegacy": "0xf8d3820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b0596297791ca09eb445c3fb16cea6f435cca3fd89869351b0152442c77013ee2787ab9363c64fa012daa8f1bbc55ef013376a0b10a608634e9fe03ce051bcf11874132f4b37b1c1",
+ "signedEip155": "0xf8d3820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b05962977929a03aacb5053d4586e75d7c2f59a16aa559f1b335a1e5b81d5e04ee3f10370e0b78a00c4062ee9c4f3c10b72331bdbbe8b8f896cecaa7f79d291dca3cb6182473d6f3",
+ "signedBerlin": "0x01f8ec03820132857029692f4582fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc080a0cc30fef0ed85c2d3df5dae8321402cd6ee6cfec5778a191674d295a861be14b3a00874a36ad883e39d70744772063a504ff5887f020ece23afa159ec3d092f7dac",
+ "signedLondon": "0x02f8ef0382013281b586f631d7b0a81f82fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc080a0fe006607f7e8089cc51cafca96b281cd8c3a95d6f88a5a1175b3a8cc958b6b74a0105054c0d7d120b19d8eb03a67dccd82c2add7b8c04e6438fd30ab6e31cd4364",
+ "signedCancun": "0x03f9019b0382013281b586f631d7b0a81f82fb3894b26832f9eebe1bb94cb67d5c6babf358d15f834e81b3b86b3c5c6cbe06f0c46868590f0951afc1b55f38598f5caf4d2f811ed3bbb8a87de83f074bc2cf19ca7225a65533cbe03aed1f43fa7cd1ef178e1ec2de6076d3ec1a22729c8645de5442bc8e3c3c7781d68293d34ae667c247189089a4959ef7c0d35e4600be23b5b059629779d7d694820a408021ed575bd94e89af5672c9c4faaa93adc0849b643a13f8a5a00106f3b8583c3b187099347666d5b01b3ae85b5458a2291ca45884084bf8a728a0012d5ba14538fe600b4f258429247337527a13d6a28c3b4b445f00d3098df94fa0011a4647e1b61123ab33b318dff3609fc11da8c4cfa9abe25f99c41923a9f581a001cb8b689043f5bdcc56fce5c26490ca28a88b0a2f53a6d1f51e3f298e65d295a001531f010e1e10d3d3332b2caef340f0a6af1e366a98126ef035c3ce80b8a1db80a0bbe878a38e5cf8d2e439fe021d2063d07b41b2a2ffe78edde5e50ae8c908c6dfa00386dfa5733210ac6b5fb0ddc0fdd493190417ddbd8573c3db375b811b59f970",
+ "signatureLegacy": {
+ "r": "0x9eb445c3fb16cea6f435cca3fd89869351b0152442c77013ee2787ab9363c64f",
+ "s": "0x12daa8f1bbc55ef013376a0b10a608634e9fe03ce051bcf11874132f4b37b1c1",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x3aacb5053d4586e75d7c2f59a16aa559f1b335a1e5b81d5e04ee3f10370e0b78",
+ "s": "0x0c4062ee9c4f3c10b72331bdbbe8b8f896cecaa7f79d291dca3cb6182473d6f3",
+ "v": "0x29"
+ },
+ "signatureBerlin": {
+ "r": "0xcc30fef0ed85c2d3df5dae8321402cd6ee6cfec5778a191674d295a861be14b3",
+ "s": "0x0874a36ad883e39d70744772063a504ff5887f020ece23afa159ec3d092f7dac",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xfe006607f7e8089cc51cafca96b281cd8c3a95d6f88a5a1175b3a8cc958b6b74",
+ "s": "0x105054c0d7d120b19d8eb03a67dccd82c2add7b8c04e6438fd30ab6e31cd4364",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xbbe878a38e5cf8d2e439fe021d2063d07b41b2a2ffe78edde5e50ae8c908c6df",
+ "s": "0x0386dfa5733210ac6b5fb0ddc0fdd493190417ddbd8573c3db375b811b59f970",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_17.json b/bolt-contracts/test/testdata/transactions/random_17.json
new file mode 100644
index 000000000..7f2762712
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_17.json
@@ -0,0 +1,92 @@
+{
+ "name": "random-17",
+ "transaction": {
+ "to": "0x7b9830530eFD81a5BB21Ed03A1E20ac977c13003",
+ "nonce": 103,
+ "gasLimit": "0xa193e9",
+ "gasPrice": "0xfcb9",
+ "maxFeePerGas": "0x9288e311910556",
+ "maxPriorityFeePerGas": "0xac",
+ "data": "0xee2035dab48b94002f3fa4ba662590127a2fcb9e978af4",
+ "value": "0xf9eff8",
+ "accessList": [
+ {
+ "address": "0x469ED43B08A100e13e84b7746fA7206d3A11a5F7",
+ "storageKeys": [
+ "0x434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950",
+ "0x56498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5",
+ "0xe83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e"
+ ]
+ },
+ {
+ "address": "0x588C330B029E06B820Dfb1d3a5b774EdcF6a9FEE",
+ "storageKeys": [
+ "0x434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950",
+ "0x56498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5",
+ "0xe83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e"
+ ]
+ },
+ {
+ "address": "0xd7Bb918eB068B37E44b12A19402F5112dE656566",
+ "storageKeys": [
+ "0x434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950",
+ "0x56498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5",
+ "0xe83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e"
+ ]
+ },
+ {
+ "address": "0x29EFB653e6b98eA36995EC81968b0EAC36678A60",
+ "storageKeys": [
+ "0x434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950",
+ "0x56498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5",
+ "0xe83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e"
+ ]
+ }
+ ],
+ "chainId": "0x6653a6",
+ "maxFeePerBlobGas": "0xe34d8405aa",
+ "blobVersionedHashes": [
+ "0x01bd0963b45ef67408366b1293b5f94b4fb19340d15f8337a4512a83ae40004d",
+ "0x013058ace831dd6002ac010bbe261fa4b1087a1a22ef376e8af21c68881cd6a6",
+ "0x01529e39d8bda59658a661cd10747b680b8f944a5f4276f794859265c718c25c",
+ "0x010b500df39903a4f96523b8fc5e742898222ec23fcc06facf7384ebd6712236",
+ "0x01d8fc2341c6b3a4c2aa2aba23344939f851119c19299c9aac4594425577e5ee"
+ ]
+ },
+ "privateKey": "0x58f3d57fde2c0dae771d5cb66092b8905ba5691226740d7398459c60712a5793",
+ "unsignedLegacy": "0xf8396782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4",
+ "unsignedEip155": "0xf83f6782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4836653a68080",
+ "unsignedBerlin": "0x01f90230836653a66782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e",
+ "unsignedLondon": "0x02f90237836653a66781ac879288e31191055683a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e",
+ "unsignedCancun": "0x03f902e4836653a66781ac879288e31191055683a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e85e34d8405aaf8a5a001bd0963b45ef67408366b1293b5f94b4fb19340d15f8337a4512a83ae40004da0013058ace831dd6002ac010bbe261fa4b1087a1a22ef376e8af21c68881cd6a6a001529e39d8bda59658a661cd10747b680b8f944a5f4276f794859265c718c25ca0010b500df39903a4f96523b8fc5e742898222ec23fcc06facf7384ebd6712236a001d8fc2341c6b3a4c2aa2aba23344939f851119c19299c9aac4594425577e5ee",
+ "signedLegacy": "0xf87c6782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af41ca0b889cf400786bc43a9bc32317a5368a1f0898b5f60fec9c4da7931867fe5c3fca06dd97c8b894c3a14ec5099eaa39b76f369f49c0ad9330dd852f797b22fb6962e",
+ "signedEip155": "0xf87f6782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af483cca770a04ad07bd0c147962802aee8e943f48a5adfadbc5d14c502e10fe108b6a3206691a039150202e4bacfbda9757c56c93858d0dc2ae5c36d69e7f5d61e6d2fb9a4d7e0",
+ "signedBerlin": "0x01f90273836653a66782fcb983a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e80a0bbeb29da87bb1efcc76efc18dc3fb4d06c2fbe9d0a8695e3bc887f84ab35796ea046f04724d13b40f6e5f8ca963862c821857f58772169706ab4a286feec54196f",
+ "signedLondon": "0x02f9027a836653a66781ac879288e31191055683a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e80a0d6593cf036acec58fa43ac9b1a19b961b26acf1b27f23ed3cfa5fc2dd63b1b73a06cf24189b044480b469c1b6bbe97bd3caf522e0a9c141d9d8a5b7e5ad225bebc",
+ "signedCancun": "0x03f90327836653a66781ac879288e31191055683a193e9947b9830530efd81a5bb21ed03a1e20ac977c1300383f9eff897ee2035dab48b94002f3fa4ba662590127a2fcb9e978af4f901f0f87a94469ed43b08a100e13e84b7746fa7206d3a11a5f7f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94588c330b029e06b820dfb1d3a5b774edcf6a9feef863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a94d7bb918eb068b37e44b12a19402f5112de656566f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94ef87a9429efb653e6b98ea36995ec81968b0eac36678a60f863a0434cab87455b41c8023691c4e836ec1e1d90fd639e1207274b07574059394950a056498d76fcc165010f94437317015d8a9a939ab1fd21216c5fcfed335f5d6ad5a0e83f7815e6bf7d521dd0341e9445415ab565b2471ab4dbbf0a0ddcc564bdb94e85e34d8405aaf8a5a001bd0963b45ef67408366b1293b5f94b4fb19340d15f8337a4512a83ae40004da0013058ace831dd6002ac010bbe261fa4b1087a1a22ef376e8af21c68881cd6a6a001529e39d8bda59658a661cd10747b680b8f944a5f4276f794859265c718c25ca0010b500df39903a4f96523b8fc5e742898222ec23fcc06facf7384ebd6712236a001d8fc2341c6b3a4c2aa2aba23344939f851119c19299c9aac4594425577e5ee01a0cff1301e6d636143fc801005349c6d64687f4e6d0c3cf0efcaf4f04863adb321a00f46aa1d9c1c2006e42981f19e75db3c5087f7535b759faabafa3642cde03879",
+ "signatureLegacy": {
+ "r": "0xb889cf400786bc43a9bc32317a5368a1f0898b5f60fec9c4da7931867fe5c3fc",
+ "s": "0x6dd97c8b894c3a14ec5099eaa39b76f369f49c0ad9330dd852f797b22fb6962e",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x4ad07bd0c147962802aee8e943f48a5adfadbc5d14c502e10fe108b6a3206691",
+ "s": "0x39150202e4bacfbda9757c56c93858d0dc2ae5c36d69e7f5d61e6d2fb9a4d7e0",
+ "v": "0xcca770"
+ },
+ "signatureBerlin": {
+ "r": "0xbbeb29da87bb1efcc76efc18dc3fb4d06c2fbe9d0a8695e3bc887f84ab35796e",
+ "s": "0x46f04724d13b40f6e5f8ca963862c821857f58772169706ab4a286feec54196f",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xd6593cf036acec58fa43ac9b1a19b961b26acf1b27f23ed3cfa5fc2dd63b1b73",
+ "s": "0x6cf24189b044480b469c1b6bbe97bd3caf522e0a9c141d9d8a5b7e5ad225bebc",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xcff1301e6d636143fc801005349c6d64687f4e6d0c3cf0efcaf4f04863adb321",
+ "s": "0x0f46aa1d9c1c2006e42981f19e75db3c5087f7535b759faabafa3642cde03879",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_18.json b/bolt-contracts/test/testdata/transactions/random_18.json
new file mode 100644
index 000000000..7a6eaf1f3
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_18.json
@@ -0,0 +1,65 @@
+{
+ "name": "random-18",
+ "transaction": {
+ "to": "0x0bbe326563703D26cB074Ef29Ed04404Ab1Fd0cC",
+ "nonce": 503,
+ "gasLimit": "0xa5cdc1",
+ "gasPrice": "0x2a",
+ "maxFeePerGas": "0x52b5f1114c1a",
+ "maxPriorityFeePerGas": "0x59d1ad",
+ "data": "0xb477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871",
+ "value": "0x8a1d",
+ "accessList": [
+ {
+ "address": "0x47421DbF48444B1c15748fFB721AC9b341c35347",
+ "storageKeys": [
+ "0x1e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279",
+ "0xed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90e",
+ "0x4ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b"
+ ]
+ }
+ ],
+ "chainId": "0x27",
+ "maxFeePerBlobGas": "0x6a1ab8a28e74c2",
+ "blobVersionedHashes": [
+ "0x0135a251a4d47cbb2e3ec7eab672f3505d49261cb433895e559539ba21441daa",
+ "0x0125f41da5f8e7146e8e5e4508967f0379dd4ae378ddf56f6f379814c056913a"
+ ]
+ },
+ "privateKey": "0xea795d7cacc6025f7bd6650aedd689f688617f9d8ca86236fa8ca5e654b767c4",
+ "unsignedLegacy": "0xf8418201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871",
+ "unsignedEip155": "0xf8448201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871278080",
+ "unsignedBerlin": "0x01f8c0278201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b",
+ "unsignedLondon": "0x02f8ca278201f78359d1ad8652b5f1114c1a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b",
+ "unsignedCancun": "0x03f90116278201f78359d1ad8652b5f1114c1a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b876a1ab8a28e74c2f842a00135a251a4d47cbb2e3ec7eab672f3505d49261cb433895e559539ba21441daaa00125f41da5f8e7146e8e5e4508967f0379dd4ae378ddf56f6f379814c056913a",
+ "signedLegacy": "0xf8848201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a8711ca0c888e9068c768fa7d14a6298902c6b4be841794498ab8f5a5b4ec8027197ee14a04561fbe2803fd2d4ffd8f491c6371816b213832776a93dc48c922887e8bbc671",
+ "signedEip155": "0xf8848201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a87172a052aea6be6b07d11486dc584c42cf1bd34ff8ec9096894fbd34cb7cc0d1aecce8a074220e1ac0c3ef2b2b60d1dea95db96d17872e1a55dc55b4835333d2c4ecdf95",
+ "signedBerlin": "0x01f90103278201f72a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b80a0e6a87722ab1ce90d062a73fe2fe3ebf7135d9b3946b7bd7edb3b4b298504e8c5a01cfad4408ddaa8e38d031b99277a81b7cd118f760acc8946b5025d2d77200197",
+ "signedLondon": "0x02f9010d278201f78359d1ad8652b5f1114c1a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b01a0f8825407a5607db5184718258869e3ba1eef9bd4c1bf4826315cfc8693593894a07aad01abea832c052973a749a3a623d63922fbbb090f5bbdb4582992d5bd508f",
+ "signedCancun": "0x03f90159278201f78359d1ad8652b5f1114c1a83a5cdc1940bbe326563703d26cb074ef29ed04404ab1fd0cc828a1da0b477bf440adf79ace31d2590393c7bbd5c191c2e40b4523e94b9c75afce3a871f87cf87a9447421dbf48444b1c15748ffb721ac9b341c35347f863a01e4e2be542b128bf053d245b5efe9be008785740189d3b5b2324a395630c3279a0ed8dbc976b685b16dc203291d38dd688d247ae09d2fea99189d5171ddcd7c90ea04ba4902e9df30d2ea5dc662cfa7887d327dc09fcd28d3dbf5c64f5e78f5e599b876a1ab8a28e74c2f842a00135a251a4d47cbb2e3ec7eab672f3505d49261cb433895e559539ba21441daaa00125f41da5f8e7146e8e5e4508967f0379dd4ae378ddf56f6f379814c056913a01a0dd72f86ce46f065e917d86c73aa3ca7e683bfeb711a84bc10d3f253161a30cdaa00c4e90ba71f6c3ee0f989353e221408330e877d65cbd0adea67e2414dd5308d7",
+ "signatureLegacy": {
+ "r": "0xc888e9068c768fa7d14a6298902c6b4be841794498ab8f5a5b4ec8027197ee14",
+ "s": "0x4561fbe2803fd2d4ffd8f491c6371816b213832776a93dc48c922887e8bbc671",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x52aea6be6b07d11486dc584c42cf1bd34ff8ec9096894fbd34cb7cc0d1aecce8",
+ "s": "0x74220e1ac0c3ef2b2b60d1dea95db96d17872e1a55dc55b4835333d2c4ecdf95",
+ "v": "0x72"
+ },
+ "signatureBerlin": {
+ "r": "0xe6a87722ab1ce90d062a73fe2fe3ebf7135d9b3946b7bd7edb3b4b298504e8c5",
+ "s": "0x1cfad4408ddaa8e38d031b99277a81b7cd118f760acc8946b5025d2d77200197",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xf8825407a5607db5184718258869e3ba1eef9bd4c1bf4826315cfc8693593894",
+ "s": "0x7aad01abea832c052973a749a3a623d63922fbbb090f5bbdb4582992d5bd508f",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0xdd72f86ce46f065e917d86c73aa3ca7e683bfeb711a84bc10d3f253161a30cda",
+ "s": "0x0c4e90ba71f6c3ee0f989353e221408330e877d65cbd0adea67e2414dd5308d7",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_19.json b/bolt-contracts/test/testdata/transactions/random_19.json
new file mode 100644
index 000000000..5a46f9935
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_19.json
@@ -0,0 +1,73 @@
+{
+ "name": "random-19",
+ "transaction": {
+ "to": "0x6D9186AD35Ba39086D341c03B212a22dAd55A70c",
+ "nonce": 47,
+ "gasLimit": "0x8e2d",
+ "gasPrice": "0xed",
+ "maxFeePerGas": "0x8f001cace664",
+ "maxPriorityFeePerGas": "0x86495d",
+ "data": "0xa6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9",
+ "value": "0x4028",
+ "accessList": [
+ {
+ "address": "0x27CBC99f4161665Fc37a013015B0B99A959f6fcb",
+ "storageKeys": []
+ },
+ {
+ "address": "0x296e9Be34584A14761c7b4cE82E99d4394A5f109",
+ "storageKeys": []
+ },
+ {
+ "address": "0xeD1C55cDE477E627081Ac4D17376f4Bf9A2dc51E",
+ "storageKeys": []
+ },
+ {
+ "address": "0x7a46971171Aa87E7Ef026C381053940714f4Ad91",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0xf9bf2e",
+ "maxFeePerBlobGas": "0xfcaa613f0b5f",
+ "blobVersionedHashes": [
+ "0x010e2214c267eb6c514aaf6eab956c7043760e0cf06f8b854ce38a637c886a92",
+ "0x01971b32a91a83e04ef14d049bf10a13377c3f18bde9c5ea313f8c2da061bb62"
+ ]
+ },
+ "privateKey": "0x1b01010d8955e5cecc6705b73064baa37a703d65cb970a5b97c1cc9b76aa9473",
+ "unsignedLegacy": "0xf89d2f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9",
+ "unsignedEip155": "0xf8a32f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad983f9bf2e8080",
+ "unsignedBerlin": "0x01f8ff83f9bf2e2f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c0",
+ "unsignedLondon": "0x02f9010883f9bf2e2f8386495d868f001cace664828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c0",
+ "unsignedCancun": "0x03f9015383f9bf2e2f8386495d868f001cace664828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c086fcaa613f0b5ff842a0010e2214c267eb6c514aaf6eab956c7043760e0cf06f8b854ce38a637c886a92a001971b32a91a83e04ef14d049bf10a13377c3f18bde9c5ea313f8c2da061bb62",
+ "signedLegacy": "0xf8e02f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad91ba0804162984a09f440a312f2231b192dc7b43fb49546ede924e1ef2b4b54abcd31a04635d7370c171b9e6ac7fa3bca544d0c227d32f0a1c5d065cc0bb02b79e8eaa1",
+ "signedEip155": "0xf8e42f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad98401f37e80a05914063f10cf79eea0d3c5cf10bf7765ffc648638c656ddf0c678057b6d570aaa00a89d79bcfa95f96290ec7938ab7c5552423736a16e0a39d84b140c5307a8bab",
+ "signedBerlin": "0x01f9014283f9bf2e2f81ed828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c080a0888f5e0b4c258b4f7c488d4e1f7dafc82d7835381b5470bc1e9571ad6bd21929a07a8a19227f4f2cb93bccbc7b4bcb0b731701a0284df8ed3fb90936e6ac0a18d1",
+ "signedLondon": "0x02f9014b83f9bf2e2f8386495d868f001cace664828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c001a0f9ba41389da7c4a5471d8af8235fb765d90ec76b370516ec0108d43eda880c84a00bca4f8cb35a6023cbeee50d38e0b62588b6cfc47d3e8023413f2bdfb04f3a8e",
+ "signedCancun": "0x03f9019683f9bf2e2f8386495d868f001cace664828e2d946d9186ad35ba39086d341c03b212a22dad55a70c824028b87da6e0079be77374ee2617550eadd15e206a8c7eb76eca2de96d48c4824b4646206720dfeb24956f1e712b58b9ef96fc92e637cf4c30614d86532d3110f670c5a70e5bdeef3dc5cf94ef8d3afffc50151c00a5ad1289fae8168bb120e9dbdb24c422eacfc4d8291c6dc942a872545c76b5c697806c84f478d361026a2ad9f85cd69427cbc99f4161665fc37a013015b0b99a959f6fcbc0d694296e9be34584a14761c7b4ce82e99d4394a5f109c0d694ed1c55cde477e627081ac4d17376f4bf9a2dc51ec0d6947a46971171aa87e7ef026c381053940714f4ad91c086fcaa613f0b5ff842a0010e2214c267eb6c514aaf6eab956c7043760e0cf06f8b854ce38a637c886a92a001971b32a91a83e04ef14d049bf10a13377c3f18bde9c5ea313f8c2da061bb6201a0e5af277a499dbd89c8b25adfab873a4bade8e963e403dbaaba3398c1b4f6d0c1a00282b21217a21a6e57c3713da3700265dc4e86803f364175d26d3ab727f812e3",
+ "signatureLegacy": {
+ "r": "0x804162984a09f440a312f2231b192dc7b43fb49546ede924e1ef2b4b54abcd31",
+ "s": "0x4635d7370c171b9e6ac7fa3bca544d0c227d32f0a1c5d065cc0bb02b79e8eaa1",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x5914063f10cf79eea0d3c5cf10bf7765ffc648638c656ddf0c678057b6d570aa",
+ "s": "0x0a89d79bcfa95f96290ec7938ab7c5552423736a16e0a39d84b140c5307a8bab",
+ "v": "0x1f37e80"
+ },
+ "signatureBerlin": {
+ "r": "0x888f5e0b4c258b4f7c488d4e1f7dafc82d7835381b5470bc1e9571ad6bd21929",
+ "s": "0x7a8a19227f4f2cb93bccbc7b4bcb0b731701a0284df8ed3fb90936e6ac0a18d1",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xf9ba41389da7c4a5471d8af8235fb765d90ec76b370516ec0108d43eda880c84",
+ "s": "0x0bca4f8cb35a6023cbeee50d38e0b62588b6cfc47d3e8023413f2bdfb04f3a8e",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0xe5af277a499dbd89c8b25adfab873a4bade8e963e403dbaaba3398c1b4f6d0c1",
+ "s": "0x0282b21217a21a6e57c3713da3700265dc4e86803f364175d26d3ab727f812e3",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_2.json b/bolt-contracts/test/testdata/transactions/random_2.json
new file mode 100644
index 000000000..f42ea1ed5
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_2.json
@@ -0,0 +1,64 @@
+{
+ "name": "random-2",
+ "transaction": {
+ "to": "0xD0F118AFa9C2c9cc50F10d94cCC1cbDda2758b36",
+ "nonce": 15,
+ "gasLimit": "0x331bce0f90",
+ "gasPrice": "0xe15a1b",
+ "maxFeePerGas": "0x58e8d1dda1",
+ "maxPriorityFeePerGas": "0x91bcff",
+ "data": "0xe0d1a7227d34c2ca72e3c0",
+ "value": "0x91e32e2f5a",
+ "accessList": [
+ {
+ "address": "0x032f5546f1B932555cB43e2b16C844d118078430",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0xac9f74e3",
+ "maxFeePerBlobGas": "0xb301566e1b",
+ "blobVersionedHashes": [
+ "0x01ffa0354a85a918e71cda7e37145c0a2f7c6fb1f10ff992ef1de8ebe3c40fd5",
+ "0x01cf50b584016c19732d845cc9c8d3a43ce413626a4f67acb98ccb3b2f1843cd",
+ "0x01d391295779890c162f2833ea186b025e6808b615b03854e2ec2664686711bc",
+ "0x01eac2c29509cb2843deba916c97d9607d460eb0cd6e58744d823f568ab4e89e",
+ "0x01873bc19156620621a96fe8bc4a0038cbac0737aa0453777816cc6ba916e9c2"
+ ]
+ },
+ "privateKey": "0xa248ca1e9bd9c10fb163baefd567da6658f7419f3b7ec8bb13800ef2c546cdc9",
+ "unsignedLegacy": "0xf20f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0",
+ "unsignedEip155": "0xf8390f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c084ac9f74e38080",
+ "unsignedBerlin": "0x01f84f84ac9f74e30f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c0",
+ "unsignedLondon": "0x02f85584ac9f74e30f8391bcff8558e8d1dda185331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c0",
+ "unsignedCancun": "0x03f9010284ac9f74e30f8391bcff8558e8d1dda185331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c085b301566e1bf8a5a001ffa0354a85a918e71cda7e37145c0a2f7c6fb1f10ff992ef1de8ebe3c40fd5a001cf50b584016c19732d845cc9c8d3a43ce413626a4f67acb98ccb3b2f1843cda001d391295779890c162f2833ea186b025e6808b615b03854e2ec2664686711bca001eac2c29509cb2843deba916c97d9607d460eb0cd6e58744d823f568ab4e89ea001873bc19156620621a96fe8bc4a0038cbac0737aa0453777816cc6ba916e9c2",
+ "signedLegacy": "0xf8750f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c01ba0bde01f2c9907b076015708bff05e24058fd153bfc2f551a0adafcd7d61a088a0a01035bfd65df2799749536217c832d8d81b915448ac0ab31935d50917136e4c18",
+ "signedEip155": "0xf87a0f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c08501593ee9e9a0f42dbeea4337d8b48ed8f2bc257c83eb16725af6f3077e99fba6bac48c5e65f8a01869eaf77bc621843ba6c8b23919f7c9ebf0ac0001a840045609e1e96f14ab5a",
+ "signedBerlin": "0x01f89284ac9f74e30f83e15a1b85331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c080a05cc5457e6561e9831b8e52fb09765dc00eaa07ba68f05dc7558ad0dab2d2b8c3a020c19425fafafdc458dd119f6a59780fc4dd3bbd7ae65f351d3adf5b217d2f4e",
+ "signedLondon": "0x02f89884ac9f74e30f8391bcff8558e8d1dda185331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c001a0e97853a3f394ebb19b1a152324c7f3f7ffde2e2a0dfa295dc61f8788dcfebe00a02f0c9626e967c4494ccd02f4e60c840ecce64216438f051efa95f0b8f196f7c9",
+ "signedCancun": "0x03f9014584ac9f74e30f8391bcff8558e8d1dda185331bce0f9094d0f118afa9c2c9cc50f10d94ccc1cbdda2758b368591e32e2f5a8be0d1a7227d34c2ca72e3c0d7d694032f5546f1b932555cb43e2b16c844d118078430c085b301566e1bf8a5a001ffa0354a85a918e71cda7e37145c0a2f7c6fb1f10ff992ef1de8ebe3c40fd5a001cf50b584016c19732d845cc9c8d3a43ce413626a4f67acb98ccb3b2f1843cda001d391295779890c162f2833ea186b025e6808b615b03854e2ec2664686711bca001eac2c29509cb2843deba916c97d9607d460eb0cd6e58744d823f568ab4e89ea001873bc19156620621a96fe8bc4a0038cbac0737aa0453777816cc6ba916e9c201a0840ee8400c949196bddbf386e82d5479a7607e5356f03a03360af4c466b309eaa026456727b69c25a095f29fed6375752693270a9989bfa3bb45fa01df37faf4dd",
+ "signatureLegacy": {
+ "r": "0xbde01f2c9907b076015708bff05e24058fd153bfc2f551a0adafcd7d61a088a0",
+ "s": "0x1035bfd65df2799749536217c832d8d81b915448ac0ab31935d50917136e4c18",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0xf42dbeea4337d8b48ed8f2bc257c83eb16725af6f3077e99fba6bac48c5e65f8",
+ "s": "0x1869eaf77bc621843ba6c8b23919f7c9ebf0ac0001a840045609e1e96f14ab5a",
+ "v": "0x1593ee9e9"
+ },
+ "signatureBerlin": {
+ "r": "0x5cc5457e6561e9831b8e52fb09765dc00eaa07ba68f05dc7558ad0dab2d2b8c3",
+ "s": "0x20c19425fafafdc458dd119f6a59780fc4dd3bbd7ae65f351d3adf5b217d2f4e",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xe97853a3f394ebb19b1a152324c7f3f7ffde2e2a0dfa295dc61f8788dcfebe00",
+ "s": "0x2f0c9626e967c4494ccd02f4e60c840ecce64216438f051efa95f0b8f196f7c9",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x840ee8400c949196bddbf386e82d5479a7607e5356f03a03360af4c466b309ea",
+ "s": "0x26456727b69c25a095f29fed6375752693270a9989bfa3bb45fa01df37faf4dd",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_20.json b/bolt-contracts/test/testdata/transactions/random_20.json
new file mode 100644
index 000000000..75ecbcc3b
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_20.json
@@ -0,0 +1,75 @@
+{
+ "name": "random-20",
+ "transaction": {
+ "to": "0x60fC630156D46bF9b1BF5B7d251C6CbE64702BF3",
+ "nonce": 812,
+ "gasLimit": "0xe010b5",
+ "gasPrice": "0x41f1af255d",
+ "maxFeePerGas": "0xc2214fa5",
+ "maxPriorityFeePerGas": "0x4a",
+ "data": "0x647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94",
+ "value": "0x6e7b",
+ "accessList": [
+ {
+ "address": "0x36aAaa454aAB9A9409b890C32E9304C42EC8b05A",
+ "storageKeys": []
+ },
+ {
+ "address": "0x7F7Ac0f60Be35f1e8F7c70A7AA5d596bC7648745",
+ "storageKeys": []
+ },
+ {
+ "address": "0xf9Fb9Bb09b08C0Fa137E76615BBd50B5cc63BCc4",
+ "storageKeys": []
+ },
+ {
+ "address": "0x69C6c93f57A0C11a765F2B9076a9575c4b015d03",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0x08454b",
+ "maxFeePerBlobGas": "0xdd2ab7977e4979",
+ "blobVersionedHashes": [
+ "0x013b327d88c9e88e3e246eeca1cc70db85b0bb0f98984ad796047d4baaea4373",
+ "0x01b7bd6841fda1dfb6cebfe85366cedd754408fdf1431865ab3a5744a4e17507",
+ "0x01136b560d707867f29fdaa25db50e2b926bb89e032a28fbd78f06a995088288",
+ "0x01d5dc7ebc9a67d8b006f15c809dfb1e4dfc5298eb1ba049d24847314bb3ac62"
+ ]
+ },
+ "privateKey": "0x6efb2ce4981ad5555974b8b6b4f767d774c32f9e09e0009d5d914e29614ec739",
+ "unsignedLegacy": "0xf89e82032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94",
+ "unsignedEip155": "0xf8a482032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d948308454b8080",
+ "unsignedBerlin": "0x01f901008308454b82032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c0",
+ "unsignedLondon": "0x02f901008308454b82032c4a84c2214fa583e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c0",
+ "unsignedCancun": "0x03f9018e8308454b82032c4a84c2214fa583e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c087dd2ab7977e4979f884a0013b327d88c9e88e3e246eeca1cc70db85b0bb0f98984ad796047d4baaea4373a001b7bd6841fda1dfb6cebfe85366cedd754408fdf1431865ab3a5744a4e17507a001136b560d707867f29fdaa25db50e2b926bb89e032a28fbd78f06a995088288a001d5dc7ebc9a67d8b006f15c809dfb1e4dfc5298eb1ba049d24847314bb3ac62",
+ "signedLegacy": "0xf8e182032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d941ca0707fd3dbc3b260752493e64be389a2689bf3b8b181a4e7baa7eeb0176ba84283a04868672b9d487c34906166770fd51f0598b026b3368bb5f4d0a99ca4fc0706e9",
+ "signedEip155": "0xf8e482032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d9483108abaa014c49179d7be5caf2590610b76eeace610c536ff3e1d1eab3b43c164fe90e472a06bb4caad9a679371ce6527da4f3a404d3cd1b36712a47774112f19add1ca4d2e",
+ "signedBerlin": "0x01f901438308454b82032c8541f1af255d83e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c001a0b445c3c80cfa7df6401a7f0c46094a0fcb63749ff073b44d72c6d1f88f5212a6a07d418e8c1f5bb3a928ea11e3049943f7c60bacbaef1c78c90f9899331574336e",
+ "signedLondon": "0x02f901438308454b82032c4a84c2214fa583e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c080a0dafce44a782685fd338895386a5b13b4183706b3621c55b92dc0e43b3dd947d0a0697a9b17a80710efd85c62104a68156c67399c69b50bc951c4888a2f6446cad4",
+ "signedCancun": "0x03f901d18308454b82032c4a84c2214fa583e010b59460fc630156d46bf9b1bf5b7d251c6cbe64702bf3826e7bb877647daf6b789306d2daeb51be942c40d66194d5e0e9dca946aa830d686d493c1d1a07090d9cde8d73c6b982f5d00161b6ee0b302cdc24f29ebd146e99149adc7666f4d1d12146f077cc6e7f78addae71d399b445a14ff15a67efb8f39dcfd32af41968639dd3665d282e4065caef58441e6ac3ebc4b8d94f85cd69436aaaa454aab9a9409b890c32e9304c42ec8b05ac0d6947f7ac0f60be35f1e8f7c70a7aa5d596bc7648745c0d694f9fb9bb09b08c0fa137e76615bbd50b5cc63bcc4c0d69469c6c93f57a0c11a765f2b9076a9575c4b015d03c087dd2ab7977e4979f884a0013b327d88c9e88e3e246eeca1cc70db85b0bb0f98984ad796047d4baaea4373a001b7bd6841fda1dfb6cebfe85366cedd754408fdf1431865ab3a5744a4e17507a001136b560d707867f29fdaa25db50e2b926bb89e032a28fbd78f06a995088288a001d5dc7ebc9a67d8b006f15c809dfb1e4dfc5298eb1ba049d24847314bb3ac6201a0ffd7654221802fa30c1c1be235fb78ca7b16d538a395476d890f0ff4132e852da05977fdaa1bf697432d5c6533adb63e77251c2ca149b2b2a629c752838a36f749",
+ "signatureLegacy": {
+ "r": "0x707fd3dbc3b260752493e64be389a2689bf3b8b181a4e7baa7eeb0176ba84283",
+ "s": "0x4868672b9d487c34906166770fd51f0598b026b3368bb5f4d0a99ca4fc0706e9",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x14c49179d7be5caf2590610b76eeace610c536ff3e1d1eab3b43c164fe90e472",
+ "s": "0x6bb4caad9a679371ce6527da4f3a404d3cd1b36712a47774112f19add1ca4d2e",
+ "v": "0x108aba"
+ },
+ "signatureBerlin": {
+ "r": "0xb445c3c80cfa7df6401a7f0c46094a0fcb63749ff073b44d72c6d1f88f5212a6",
+ "s": "0x7d418e8c1f5bb3a928ea11e3049943f7c60bacbaef1c78c90f9899331574336e",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0xdafce44a782685fd338895386a5b13b4183706b3621c55b92dc0e43b3dd947d0",
+ "s": "0x697a9b17a80710efd85c62104a68156c67399c69b50bc951c4888a2f6446cad4",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xffd7654221802fa30c1c1be235fb78ca7b16d538a395476d890f0ff4132e852d",
+ "s": "0x5977fdaa1bf697432d5c6533adb63e77251c2ca149b2b2a629c752838a36f749",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_3.json b/bolt-contracts/test/testdata/transactions/random_3.json
new file mode 100644
index 000000000..4fdd11f6a
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_3.json
@@ -0,0 +1,58 @@
+{
+ "name": "random-3",
+ "transaction": {
+ "to": "0x8Bee13B1D7E29953a7484A3aE8e344D86b4Ad2C5",
+ "nonce": 530,
+ "gasLimit": "0xccb4",
+ "gasPrice": "0x18956c24",
+ "maxFeePerGas": "0x57f3c57a41",
+ "maxPriorityFeePerGas": "0x5a492a",
+ "data": "0x4c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbd",
+ "value": "0x3b04",
+ "accessList": [],
+ "chainId": "0xb3",
+ "maxFeePerBlobGas": "0x7b0f34f9",
+ "blobVersionedHashes": [
+ "0x01b64c9600d28c83309131198d984f79284d6e2a5dd8ffeebc05f8bdcc21b03b",
+ "0x013305753b09589c2a8015bb6f975e2654a2164a41dc51812a439560bdd6d16d",
+ "0x013e6ffbf81a5911cc365712aa5511e93c28b14e4ead53ac786db6506caa1b4a",
+ "0x0146c688afbb49d2c0798642bbdbe25bfe7cdaa37632c32023aacce933f48dae"
+ ]
+ },
+ "privateKey": "0xa9e5fdc17c2302fce888f2dc9d6ec2b3d3fc06aa212ec06b07f4035f64fcc58f",
+ "unsignedLegacy": "0xf8688202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbd",
+ "unsignedEip155": "0xf86c8202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbd81b38080",
+ "unsignedBerlin": "0x01f86b81b38202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc0",
+ "unsignedLondon": "0x02f87081b3820212835a492a8557f3c57a4182ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc0",
+ "unsignedCancun": "0x03f8fb81b3820212835a492a8557f3c57a4182ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc0847b0f34f9f884a001b64c9600d28c83309131198d984f79284d6e2a5dd8ffeebc05f8bdcc21b03ba0013305753b09589c2a8015bb6f975e2654a2164a41dc51812a439560bdd6d16da0013e6ffbf81a5911cc365712aa5511e93c28b14e4ead53ac786db6506caa1b4aa00146c688afbb49d2c0798642bbdbe25bfe7cdaa37632c32023aacce933f48dae",
+ "signedLegacy": "0xf8ab8202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbd1ca07be350194f44b6a86cd7199f2c6a2dea34815d20abbd0f389cf3c55ef5a13566a0239482099fd9289999f06db6fb95194ac1a92fcd13c1639d2e99e3b66ab2f0b1",
+ "signedEip155": "0xf8ad8202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbd820189a0541bc2b78ae53dd11ac2c9845fe30878c493d34311513dfda43a92dbee1b4121a03f4ff8d77c247fc2238c02e2d5ca68dcd77e8f7a20d0d2f99145925af07beec0",
+ "signedBerlin": "0x01f8ae81b38202128418956c2482ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc001a035a7324dd7552976ee86c94623e89be43fddfa1ceed1354822a33726db13d9fba047347aee2460910314bbf20fbc091befb91a0ac236d30905312f828af49b2420",
+ "signedLondon": "0x02f8b381b3820212835a492a8557f3c57a4182ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc080a0c458cee1b97cfb1f6fa52b2adaa135d8501d63ba908c113f48796d40884496b1a03abaf292f68e0fc6a5c1012455d49d2d566cb4cf2eeebc8d52f1f5e3e20c808b",
+ "signedCancun": "0x03f9013e81b3820212835a492a8557f3c57a4182ccb4948bee13b1d7e29953a7484a3ae8e344d86b4ad2c5823b04b8434c4aaeceaa35f9cd275639e2319057a18f3c4d8bf242707b08a1a2a32531d3975a4dc67b8a92f5a11adbf12b6f8d8809d1544aed6f9da9b1f5e753307165e3e85afdbdc0847b0f34f9f884a001b64c9600d28c83309131198d984f79284d6e2a5dd8ffeebc05f8bdcc21b03ba0013305753b09589c2a8015bb6f975e2654a2164a41dc51812a439560bdd6d16da0013e6ffbf81a5911cc365712aa5511e93c28b14e4ead53ac786db6506caa1b4aa00146c688afbb49d2c0798642bbdbe25bfe7cdaa37632c32023aacce933f48dae01a0fb445840df9e94369942979f7c47b640f948a2fe812b497350ce00a04d0205ffa02c555565381a70547f13b807647a202705f67535104fa92524c5bd5507ba010f",
+ "signatureLegacy": {
+ "r": "0x7be350194f44b6a86cd7199f2c6a2dea34815d20abbd0f389cf3c55ef5a13566",
+ "s": "0x239482099fd9289999f06db6fb95194ac1a92fcd13c1639d2e99e3b66ab2f0b1",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x541bc2b78ae53dd11ac2c9845fe30878c493d34311513dfda43a92dbee1b4121",
+ "s": "0x3f4ff8d77c247fc2238c02e2d5ca68dcd77e8f7a20d0d2f99145925af07beec0",
+ "v": "0x189"
+ },
+ "signatureBerlin": {
+ "r": "0x35a7324dd7552976ee86c94623e89be43fddfa1ceed1354822a33726db13d9fb",
+ "s": "0x47347aee2460910314bbf20fbc091befb91a0ac236d30905312f828af49b2420",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0xc458cee1b97cfb1f6fa52b2adaa135d8501d63ba908c113f48796d40884496b1",
+ "s": "0x3abaf292f68e0fc6a5c1012455d49d2d566cb4cf2eeebc8d52f1f5e3e20c808b",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xfb445840df9e94369942979f7c47b640f948a2fe812b497350ce00a04d0205ff",
+ "s": "0x2c555565381a70547f13b807647a202705f67535104fa92524c5bd5507ba010f",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_4.json b/bolt-contracts/test/testdata/transactions/random_4.json
new file mode 100644
index 000000000..d5eb37ae5
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_4.json
@@ -0,0 +1,58 @@
+{
+ "name": "random-4",
+ "transaction": {
+ "to": "0xf5c58b3aC42d718024e914d90a0C17CeB4487F08",
+ "nonce": 150,
+ "gasLimit": "0x9b",
+ "gasPrice": "0x75fa",
+ "maxFeePerGas": "0x92719aae8f72c2",
+ "maxPriorityFeePerGas": "0xa16d",
+ "data": "0xfdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8a",
+ "value": "0x9c5b0d",
+ "accessList": [],
+ "chainId": "0x100800",
+ "maxFeePerBlobGas": "0xeac37693",
+ "blobVersionedHashes": [
+ "0x017a6fb74091418b95e7679cdb19efd23cb1a7de4f8d818ae3206e33b8e524eb",
+ "0x01e4398d25da2fec20b713fe51b4cc7f555a473c4928e7e5a0f7be47761d4d9c",
+ "0x01fa7aff790b19c63de804524b5e37b459fd5b8614008d0ef4fd513dcb9361fc",
+ "0x01af1c40ffd00db8e91a537456d387c8e788276b13212ce917514456bb7b69bb"
+ ]
+ },
+ "privateKey": "0xaf7e4892d96c6cd39bb8048b699386b24539fb8fb078f67e190fd3703f972f55",
+ "unsignedLegacy": "0xf88381968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8a",
+ "unsignedEip155": "0xf88981968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8a831008008080",
+ "unsignedBerlin": "0x01f8888310080081968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac0",
+ "unsignedLondon": "0x02f89083100800819682a16d8792719aae8f72c2819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac0",
+ "unsignedCancun": "0x03f9011b83100800819682a16d8792719aae8f72c2819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac084eac37693f884a0017a6fb74091418b95e7679cdb19efd23cb1a7de4f8d818ae3206e33b8e524eba001e4398d25da2fec20b713fe51b4cc7f555a473c4928e7e5a0f7be47761d4d9ca001fa7aff790b19c63de804524b5e37b459fd5b8614008d0ef4fd513dcb9361fca001af1c40ffd00db8e91a537456d387c8e788276b13212ce917514456bb7b69bb",
+ "signedLegacy": "0xf8c681968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8a1ba079230da6c89f320a59fe2f291f51e758e1e642851e2c6b6b8c4b3a32b1a75c4ba04a160e0e100e4244596580586abe09d7d3423e967ca417d3d3980a6609e94030",
+ "signedEip155": "0xf8c981968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8a83201023a08cbd6e5a98913d543e56fc8b8865adb020e081501c3549d54bfa5f1250666e84a04c364c7a1fdbc76a890b4468e2f7957fb340c78ea98793c304141a449adfa9b0",
+ "signedBerlin": "0x01f8cb8310080081968275fa819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac080a05c5fd1ac646c437d17e22ea102147265371d1358814c57346d1448ed6703d042a01e525dbe3024c1ac10b1229c7d69b4af119c60ae3b45eab73ee9c61926dbd91a",
+ "signedLondon": "0x02f8d383100800819682a16d8792719aae8f72c2819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac080a0c5817c5fca256b8084f994686f21a22e815f49049c1d7126dba60578a8dcecfba07756dd3accb21c6994c079795e3edc068e1c434c33109580215073701958f29d",
+ "signedCancun": "0x03f9015e83100800819682a16d8792719aae8f72c2819b94f5c58b3ac42d718024e914d90a0c17ceb4487f08839c5b0db861fdcd0c62ddc8b8c99d6768d3fe3097a66b388998e8f6294b69c95751da898430b9c9e7b02602393a9dc2026918c7741f4097e1de373a5b8d884d02e66b06742b29cc539a22675f5713a1ba9d247b9c9eaabf54f9cb48cc8f7429c80c31b3cbfa8ac084eac37693f884a0017a6fb74091418b95e7679cdb19efd23cb1a7de4f8d818ae3206e33b8e524eba001e4398d25da2fec20b713fe51b4cc7f555a473c4928e7e5a0f7be47761d4d9ca001fa7aff790b19c63de804524b5e37b459fd5b8614008d0ef4fd513dcb9361fca001af1c40ffd00db8e91a537456d387c8e788276b13212ce917514456bb7b69bb01a0b96f5303bcbb749f5fbf6c1d918eb08bfefe05242bfd2c4b9a9a55efb5b686ffa050b83d2a4dfcaa66b0339df55910ea553bbce6f22d4ab5e8c8562ddb15a03999",
+ "signatureLegacy": {
+ "r": "0x79230da6c89f320a59fe2f291f51e758e1e642851e2c6b6b8c4b3a32b1a75c4b",
+ "s": "0x4a160e0e100e4244596580586abe09d7d3423e967ca417d3d3980a6609e94030",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x8cbd6e5a98913d543e56fc8b8865adb020e081501c3549d54bfa5f1250666e84",
+ "s": "0x4c364c7a1fdbc76a890b4468e2f7957fb340c78ea98793c304141a449adfa9b0",
+ "v": "0x201023"
+ },
+ "signatureBerlin": {
+ "r": "0x5c5fd1ac646c437d17e22ea102147265371d1358814c57346d1448ed6703d042",
+ "s": "0x1e525dbe3024c1ac10b1229c7d69b4af119c60ae3b45eab73ee9c61926dbd91a",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0xc5817c5fca256b8084f994686f21a22e815f49049c1d7126dba60578a8dcecfb",
+ "s": "0x7756dd3accb21c6994c079795e3edc068e1c434c33109580215073701958f29d",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xb96f5303bcbb749f5fbf6c1d918eb08bfefe05242bfd2c4b9a9a55efb5b686ff",
+ "s": "0x50b83d2a4dfcaa66b0339df55910ea553bbce6f22d4ab5e8c8562ddb15a03999",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_5.json b/bolt-contracts/test/testdata/transactions/random_5.json
new file mode 100644
index 000000000..de4413b82
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_5.json
@@ -0,0 +1,92 @@
+{
+ "name": "random-5",
+ "transaction": {
+ "to": "0x4d1060d970674619005137921969b4bfe3EeA6B8",
+ "nonce": 577,
+ "gasLimit": "0xbe431918",
+ "gasPrice": "0xb3b1aaeb58",
+ "maxFeePerGas": "0x6bb02a65c7",
+ "maxPriorityFeePerGas": "0xa5",
+ "data": "0x7f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60f",
+ "value": "0x95927b2d76",
+ "accessList": [
+ {
+ "address": "0x8A632C23BF807681570c3fb6632Ce99FD98BdB23",
+ "storageKeys": [
+ "0x1c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25c",
+ "0x2b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919",
+ "0xc266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5",
+ "0xf49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e"
+ ]
+ },
+ {
+ "address": "0x2D78B31bA09E8a2888d655e3d000FE95c63789c4",
+ "storageKeys": [
+ "0x1c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25c",
+ "0x2b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919",
+ "0xc266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5",
+ "0xf49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e"
+ ]
+ },
+ {
+ "address": "0x3199b3433EE7f3eDcAE901cbce64C4E81125F7da",
+ "storageKeys": [
+ "0x1c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25c",
+ "0x2b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919",
+ "0xc266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5",
+ "0xf49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e"
+ ]
+ },
+ {
+ "address": "0xb8d669949683a728f76919fe2CC9896216E00A81",
+ "storageKeys": [
+ "0x1c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25c",
+ "0x2b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919",
+ "0xc266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5",
+ "0xf49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e"
+ ]
+ }
+ ],
+ "chainId": "0x60a7",
+ "maxFeePerBlobGas": "0x13c4c123e2373d",
+ "blobVersionedHashes": [
+ "0x014a782765b51a031ac55ad44ba18bc6f003634892312bc06ee169486aabeab0"
+ ]
+ },
+ "privateKey": "0x77065b8ddb2f89d3d2d83f46d0147efc081e3a3f1012406c698a9ce364b324e9",
+ "unsignedLegacy": "0xf89c82024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60f",
+ "unsignedEip155": "0xf8a182024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60f8260a78080",
+ "unsignedBerlin": "0x01f903168260a782024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e",
+ "unsignedLondon": "0x02f903188260a782024181a5856bb02a65c784be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e",
+ "unsignedCancun": "0x03f903428260a782024181a5856bb02a65c784be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e8713c4c123e2373de1a0014a782765b51a031ac55ad44ba18bc6f003634892312bc06ee169486aabeab0",
+ "signedLegacy": "0xf8df82024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60f1ba098cadec7a4cd4297f92828b79459baed65817cd8fcd40cb2025b750c8bb3d046a00c2e6848654d045d1502a9c1777bc917a63718e8a494dae94720f519ff07b3db",
+ "signedEip155": "0xf8e182024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60f82c172a009f309ce844417f1035eff7a0b3bc191253843fb29068769f09fed81a464e622a0722660636b83fcac33759285b9704a5976b7cf9aeeceacab451702ff454469dc",
+ "signedBerlin": "0x01f903598260a782024185b3b1aaeb5884be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e01a07c8b37dd82c37b9783d328b99528bdbf4abdc7956d5e3da3a65f4a165e4acb74a04db2486fbb725825041d85e055d0adf75e02790041d668d13662717695436410",
+ "signedLondon": "0x02f9035b8260a782024181a5856bb02a65c784be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e01a09f9feb4b5bf570aa4f121fcc51de36dd7de80d0e4398f3dc99a6a615d12570c9a05d6f01ed28fda456c5f63f5f652735161cb5eede641262e296d62d94c7b628f8",
+ "signedCancun": "0x03f903858260a782024181a5856bb02a65c784be431918944d1060d970674619005137921969b4bfe3eea6b88595927b2d76b8717f2239c398167e747939f64b2ed9458db8aa10eb367bfab1976a0bc6693cf152dd8d13aa16e4d655a38d6ac64eae0932e13d649f9516fca834cd5a49c7b6e5ba1286a30eea1ac2e89c78441c5418250f8e307cc72c95ef36a8e4cee60b56ece34984ed3302672e6183d28ef30f3e43d60ff90274f89b948a632c23bf807681570c3fb6632ce99fd98bdb23f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b942d78b31ba09e8a2888d655e3d000fe95c63789c4f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b943199b3433ee7f3edcae901cbce64c4e81125f7daf884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824ef89b94b8d669949683a728f76919fe2cc9896216e00a81f884a01c3124f271ea52d9e881bdd52c63020fb7c08a1b96263030415e4bc8146db25ca02b6d4aa754fa44f0e86e6fa0a936048674ffc4fef24c5a2b317c740630901919a0c266c51508b93a8f933e2e64505e458ac26cdb93e8e0bc7bd1609552b6210aa5a0f49934500a155bedea4f0bf25bfc62161fcb74fbf17ca480333f4747d5ad824e8713c4c123e2373de1a0014a782765b51a031ac55ad44ba18bc6f003634892312bc06ee169486aabeab001a033925f057eeeebb09785c26ff18a561741d30577c4eb97a064262592c279995ca04dd3a7404520f2d9b23fddc520c09fddccf0757248627616a6f7ea4f97e9e452",
+ "signatureLegacy": {
+ "r": "0x98cadec7a4cd4297f92828b79459baed65817cd8fcd40cb2025b750c8bb3d046",
+ "s": "0x0c2e6848654d045d1502a9c1777bc917a63718e8a494dae94720f519ff07b3db",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x09f309ce844417f1035eff7a0b3bc191253843fb29068769f09fed81a464e622",
+ "s": "0x722660636b83fcac33759285b9704a5976b7cf9aeeceacab451702ff454469dc",
+ "v": "0xc172"
+ },
+ "signatureBerlin": {
+ "r": "0x7c8b37dd82c37b9783d328b99528bdbf4abdc7956d5e3da3a65f4a165e4acb74",
+ "s": "0x4db2486fbb725825041d85e055d0adf75e02790041d668d13662717695436410",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x9f9feb4b5bf570aa4f121fcc51de36dd7de80d0e4398f3dc99a6a615d12570c9",
+ "s": "0x5d6f01ed28fda456c5f63f5f652735161cb5eede641262e296d62d94c7b628f8",
+ "v": "0x1"
+ },
+ "signatureCancun": {
+ "r": "0x33925f057eeeebb09785c26ff18a561741d30577c4eb97a064262592c279995c",
+ "s": "0x4dd3a7404520f2d9b23fddc520c09fddccf0757248627616a6f7ea4f97e9e452",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_6.json b/bolt-contracts/test/testdata/transactions/random_6.json
new file mode 100644
index 000000000..bf778827d
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_6.json
@@ -0,0 +1,56 @@
+{
+ "name": "random-6",
+ "transaction": {
+ "to": "0x47F8627a7925083e80e0d94dBB979ce2c44A2c74",
+ "nonce": 432,
+ "gasLimit": "0xa1fca9f195",
+ "gasPrice": "0x39",
+ "maxFeePerGas": "0x98501809f1",
+ "maxPriorityFeePerGas": "0xfb",
+ "data": "0xc90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8",
+ "value": "0x79ef27",
+ "accessList": [],
+ "chainId": "0xf76e64",
+ "maxFeePerBlobGas": "0x50799034",
+ "blobVersionedHashes": [
+ "0x01b7a799435fabc095b12caf67e311027aa01ac411f8c004f4ea493a982f3995",
+ "0x01ec2843771aeee34beaff5727081888ad2e7367f88c14eec961fceb476b63a1"
+ ]
+ },
+ "privateKey": "0xd9f6e52112da85a6f822054409e83460b9da070ce6c48843e4793c5720ad6910",
+ "unsignedLegacy": "0xf84b8201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8",
+ "unsignedEip155": "0xf8518201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a883f76e648080",
+ "unsignedBerlin": "0x01f85083f76e648201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c0",
+ "unsignedLondon": "0x02f85783f76e648201b081fb8598501809f185a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c0",
+ "unsignedCancun": "0x03f8a083f76e648201b081fb8598501809f185a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c08450799034f842a001b7a799435fabc095b12caf67e311027aa01ac411f8c004f4ea493a982f3995a001ec2843771aeee34beaff5727081888ad2e7367f88c14eec961fceb476b63a1",
+ "signedLegacy": "0xf88e8201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a81ba091ad22bc10dbc50ad469a26c6be65d65213a54b5de2ea949f8b7f47ee641d0aba03e69a58405b8bdcd6e41097b828b222b02fa2bfd6b001616a15241c41275c2df",
+ "signedEip155": "0xf8928201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a88401eedceba0b19933ddedae4fae6d63ed9ea7a4df4d1e48be652394203245429ef61e2728d6a033d8ab838c3eb536725ccd32ce73e75551eb660acd0608344887e838c41d7e3f",
+ "signedBerlin": "0x01f89383f76e648201b03985a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c001a0538d5ec11ad5bc2755057e684243d103469145984c437d317a231cd4ed7f891fa02f6051be9067d1e9322dc667dbd22f423a27354134b5070819f262eb85e8f754",
+ "signedLondon": "0x02f89a83f76e648201b081fb8598501809f185a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c080a07e88de3723e5ed269a26ebb27c5edf39da43e0246506f7c20861c286ed2e0118a0304a806691fee1223b8b505dc97142ad8eefaf949a7d8828e0483659accecfdd",
+ "signedCancun": "0x03f8e383f76e648201b081fb8598501809f185a1fca9f1959447f8627a7925083e80e0d94dbb979ce2c44a2c748379ef27a7c90411dca095641b87f3229df9e7613b2e73477c57fc4e28b4c06f436f9825b5aa4d839c3d07a8c08450799034f842a001b7a799435fabc095b12caf67e311027aa01ac411f8c004f4ea493a982f3995a001ec2843771aeee34beaff5727081888ad2e7367f88c14eec961fceb476b63a180a0b5f9efc37373be1d360b8f6a5dc88e111e4cad78b320aee3196fa8a413034a19a00747b3ad2aa3abce5b73d940d7f10990cd7ef496e6315e8143b122f83b9ae149",
+ "signatureLegacy": {
+ "r": "0x91ad22bc10dbc50ad469a26c6be65d65213a54b5de2ea949f8b7f47ee641d0ab",
+ "s": "0x3e69a58405b8bdcd6e41097b828b222b02fa2bfd6b001616a15241c41275c2df",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0xb19933ddedae4fae6d63ed9ea7a4df4d1e48be652394203245429ef61e2728d6",
+ "s": "0x33d8ab838c3eb536725ccd32ce73e75551eb660acd0608344887e838c41d7e3f",
+ "v": "0x1eedceb"
+ },
+ "signatureBerlin": {
+ "r": "0x538d5ec11ad5bc2755057e684243d103469145984c437d317a231cd4ed7f891f",
+ "s": "0x2f6051be9067d1e9322dc667dbd22f423a27354134b5070819f262eb85e8f754",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x7e88de3723e5ed269a26ebb27c5edf39da43e0246506f7c20861c286ed2e0118",
+ "s": "0x304a806691fee1223b8b505dc97142ad8eefaf949a7d8828e0483659accecfdd",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xb5f9efc37373be1d360b8f6a5dc88e111e4cad78b320aee3196fa8a413034a19",
+ "s": "0x0747b3ad2aa3abce5b73d940d7f10990cd7ef496e6315e8143b122f83b9ae149",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_7.json b/bolt-contracts/test/testdata/transactions/random_7.json
new file mode 100644
index 000000000..e4804a03c
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_7.json
@@ -0,0 +1,84 @@
+{
+ "name": "random-7",
+ "transaction": {
+ "to": "0xD6E75AAf5C27963b31DD4Ad638e4C1d07b2Cc010",
+ "nonce": 667,
+ "gasLimit": "0x95ead5c7",
+ "gasPrice": "0x86",
+ "maxFeePerGas": "0x2e54dcae",
+ "maxPriorityFeePerGas": "0x0cb9",
+ "data": "0xe931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294",
+ "value": "0x632af6c3",
+ "accessList": [
+ {
+ "address": "0xD0a4e08ACDA2A8B3AE50Db94DB3246C4a2F34b60",
+ "storageKeys": [
+ "0xe85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11",
+ "0x2c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b"
+ ]
+ },
+ {
+ "address": "0x3e4eE3DE26502A40C8dc33886c1bB7e079916194",
+ "storageKeys": [
+ "0xe85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11",
+ "0x2c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b"
+ ]
+ },
+ {
+ "address": "0xe54E80bd490910C719D5F11602a97FaE4D5C0F11",
+ "storageKeys": [
+ "0xe85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11",
+ "0x2c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b"
+ ]
+ },
+ {
+ "address": "0x6D1fd21ed3E7D4DE1F58361cB78De7a882dEcC79",
+ "storageKeys": [
+ "0xe85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11",
+ "0x2c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b"
+ ]
+ }
+ ],
+ "chainId": "0x295d",
+ "maxFeePerBlobGas": "0xd76e69ca95e7",
+ "blobVersionedHashes": [
+ "0x01a7921375f6c2798377acd9c4db7719b9980b28c49dbc0ef3962381d961d725"
+ ]
+ },
+ "privateKey": "0xa6fe9bfe96da66b7788f98b3107c588bee30ccc844e129a7772df540c3193239",
+ "unsignedLegacy": "0xf86f82029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294",
+ "unsignedEip155": "0xf87482029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d497791029482295d8080",
+ "unsignedBerlin": "0x01f901e182295d82029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b",
+ "unsignedLondon": "0x02f901e782295d82029b820cb9842e54dcae8495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b",
+ "unsignedCancun": "0x03f9021082295d82029b820cb9842e54dcae8495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b86d76e69ca95e7e1a001a7921375f6c2798377acd9c4db7719b9980b28c49dbc0ef3962381d961d725",
+ "signedLegacy": "0xf8b282029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d49779102941ca02bacdc3053183b5f2a6f56ed6c2f126a6bbb592ef64d912bc704293d5620e188a0163d3e7cb0ad4ab63d10314c24728248699e5f5acebf84e84d67ca21ebe835ca",
+ "signedEip155": "0xf8b482029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d49779102948252dda0988356e8e6b000200035901fa00b736677e25d01022d8e7ff0c6f9a9f44cb530a01d04ca133497f2a7f07a36022a132027df576c9d53935fee792fa6a01e2d18d0",
+ "signedBerlin": "0x01f9022482295d82029b81868495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b01a0f243d534dfae5cfb18cf59a41daa31520d91f03fc9b5830ee8a512dca82bbdcda02507b456d89080c847848fa2199998ffc363cbb0d4835ce8d03621980a3f135a",
+ "signedLondon": "0x02f9022a82295d82029b820cb9842e54dcae8495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b80a08e1d67786ffd5edb50e2aff66566c76a1523e9dcabc667b92f540f536d47bccea05d6a00e50ad155e59fbba841afda7592ec16788da4cfd895b447f4c13197d1ab",
+ "signedCancun": "0x03f9025382295d82029b820cb9842e54dcae8495ead5c794d6e75aaf5c27963b31dd4ad638e4c1d07b2cc01084632af6c3b849e931d4333e6bb3d32f215bfdb13209a0905f740b180cadcea26405d9c79e108e50e87b78a1e7192c313e22f09632ed2882774f8e7536b7dfa987f7d063fc8db54ad5bd8d4977910294f9016cf85994d0a4e08acda2a8b3ae50db94db3246c4a2f34b60f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859943e4ee3de26502a40c8dc33886c1bb7e079916194f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf85994e54e80bd490910c719d5f11602a97fae4d5c0f11f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0bf859946d1fd21ed3e7d4de1f58361cb78de7a882decc79f842a0e85938a8c29ab7b82264cc2e0822673fe17637364d6b384eb49f89e1adf61a11a02c10ec7831da9a49dbf10818882d783f6511dda96c06b8ad2999aeb9f9f82d0b86d76e69ca95e7e1a001a7921375f6c2798377acd9c4db7719b9980b28c49dbc0ef3962381d961d72501a006f7d3d75727c212f2e3fe74af34e4b7db7825291f919e1f291a36161c4d7867a07706e6e9c6b49b83157519cb113dab63707214ad2d9d3af77c65b671bd2958d8",
+ "signatureLegacy": {
+ "r": "0x2bacdc3053183b5f2a6f56ed6c2f126a6bbb592ef64d912bc704293d5620e188",
+ "s": "0x163d3e7cb0ad4ab63d10314c24728248699e5f5acebf84e84d67ca21ebe835ca",
+ "v": "0x1c"
+ },
+ "signatureEip155": {
+ "r": "0x988356e8e6b000200035901fa00b736677e25d01022d8e7ff0c6f9a9f44cb530",
+ "s": "0x1d04ca133497f2a7f07a36022a132027df576c9d53935fee792fa6a01e2d18d0",
+ "v": "0x52dd"
+ },
+ "signatureBerlin": {
+ "r": "0xf243d534dfae5cfb18cf59a41daa31520d91f03fc9b5830ee8a512dca82bbdcd",
+ "s": "0x2507b456d89080c847848fa2199998ffc363cbb0d4835ce8d03621980a3f135a",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x8e1d67786ffd5edb50e2aff66566c76a1523e9dcabc667b92f540f536d47bcce",
+ "s": "0x5d6a00e50ad155e59fbba841afda7592ec16788da4cfd895b447f4c13197d1ab",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0x06f7d3d75727c212f2e3fe74af34e4b7db7825291f919e1f291a36161c4d7867",
+ "s": "0x7706e6e9c6b49b83157519cb113dab63707214ad2d9d3af77c65b671bd2958d8",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_8.json b/bolt-contracts/test/testdata/transactions/random_8.json
new file mode 100644
index 000000000..edc2787d4
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_8.json
@@ -0,0 +1,60 @@
+{
+ "name": "random-8",
+ "transaction": {
+ "to": "0x74A3Ddaa5CD03Ba0e9B7cd5535BDa1E23F691932",
+ "nonce": 965,
+ "gasLimit": "0x35",
+ "gasPrice": "0x11ac8f05",
+ "maxFeePerGas": "0xae07efb83ed7ae",
+ "maxPriorityFeePerGas": "0xdeb8",
+ "data": "0xb7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142",
+ "value": "0xa62ec77a",
+ "accessList": [
+ {
+ "address": "0xE125F9D848135f52ecc65FcF216146541688f4ec",
+ "storageKeys": []
+ }
+ ],
+ "chainId": "0x473eafa7",
+ "maxFeePerBlobGas": "0x1a6c2938773134",
+ "blobVersionedHashes": [
+ "0x01244e9f27106e77da21d39868d5400854a0cf90e2f58a1134b655704acb213e"
+ ]
+ },
+ "privateKey": "0xbcc0a8d59a4ebe22dbcc8a52e2975f634f6e3095716602f08e214e4017684180",
+ "unsignedLegacy": "0xf8688203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142",
+ "unsignedEip155": "0xf86f8203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f14284473eafa78080",
+ "unsignedBerlin": "0x01f88584473eafa78203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc0",
+ "unsignedLondon": "0x02f88b84473eafa78203c582deb887ae07efb83ed7ae359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc0",
+ "unsignedCancun": "0x03f8b584473eafa78203c582deb887ae07efb83ed7ae359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc0871a6c2938773134e1a001244e9f27106e77da21d39868d5400854a0cf90e2f58a1134b655704acb213e",
+ "signedLegacy": "0xf8ab8203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f1421ba049a9667e37b4619f39fb925cac786d8ec36b44f7f524cf936ce36cc97ebb04eda074679d25c7bd881450bb623002cb1d5b09b45e9435ebc469b9cea2d05feae86a",
+ "signedEip155": "0xf8af8203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142848e7d5f72a0e211ba6a21a4ae8bf4cafb29b98d2382bcee9435181a5261964f517ca91b33ffa038351fa3cfe37c88b2bd3aa16421bd37abf6356491f05789624c9f0e25682a38",
+ "signedBerlin": "0x01f8c884473eafa78203c58411ac8f05359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc001a0cf7dfd4507fb9331f4344fe787ecdbfb74d5c09cf71f2fcccb84d12be2a11f5fa07359816992767a631758506f7000bd4cb576cc62388bc490922ab96ee353cc82",
+ "signedLondon": "0x02f8ce84473eafa78203c582deb887ae07efb83ed7ae359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc080a04b1e128faa1365b45eacf1da245d78a3c6c10619a265e268657bd8db50c2ac3ea02538dcbc52e6648a320c641268b5eef3bd9eb24d5f17aa1a3535c194b1404e58",
+ "signedCancun": "0x03f8f884473eafa78203c582deb887ae07efb83ed7ae359474a3ddaa5cd03ba0e9b7cd5535bda1e23f69193284a62ec77ab843b7c4964ed0cebae0f12c2cbb2adadd2b7ef1b2183c1d5df71354ba8da3bff86e14975031ee08b2239ded1369a235c68a72cc994af2744f70a7214babec31160cd6f142d7d694e125f9d848135f52ecc65fcf216146541688f4ecc0871a6c2938773134e1a001244e9f27106e77da21d39868d5400854a0cf90e2f58a1134b655704acb213e80a0e0e7119acfae63aabe8754ff93738fba1714ef044cbf0cf00bb155e5fd32a629a064c04c7a22c0070cc3f6fa061677be23c3336359cdb428f02f53945d73fd4e77",
+ "signatureLegacy": {
+ "r": "0x49a9667e37b4619f39fb925cac786d8ec36b44f7f524cf936ce36cc97ebb04ed",
+ "s": "0x74679d25c7bd881450bb623002cb1d5b09b45e9435ebc469b9cea2d05feae86a",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0xe211ba6a21a4ae8bf4cafb29b98d2382bcee9435181a5261964f517ca91b33ff",
+ "s": "0x38351fa3cfe37c88b2bd3aa16421bd37abf6356491f05789624c9f0e25682a38",
+ "v": "0x8e7d5f72"
+ },
+ "signatureBerlin": {
+ "r": "0xcf7dfd4507fb9331f4344fe787ecdbfb74d5c09cf71f2fcccb84d12be2a11f5f",
+ "s": "0x7359816992767a631758506f7000bd4cb576cc62388bc490922ab96ee353cc82",
+ "v": "0x1"
+ },
+ "signatureLondon": {
+ "r": "0x4b1e128faa1365b45eacf1da245d78a3c6c10619a265e268657bd8db50c2ac3e",
+ "s": "0x2538dcbc52e6648a320c641268b5eef3bd9eb24d5f17aa1a3535c194b1404e58",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0xe0e7119acfae63aabe8754ff93738fba1714ef044cbf0cf00bb155e5fd32a629",
+ "s": "0x64c04c7a22c0070cc3f6fa061677be23c3336359cdb428f02f53945d73fd4e77",
+ "v": "0x0"
+ }
+}
diff --git a/bolt-contracts/test/testdata/transactions/random_9.json b/bolt-contracts/test/testdata/transactions/random_9.json
new file mode 100644
index 000000000..3e9f1867a
--- /dev/null
+++ b/bolt-contracts/test/testdata/transactions/random_9.json
@@ -0,0 +1,56 @@
+{
+ "name": "random-9",
+ "transaction": {
+ "to": "0x671a63481F6AdF6E231296c3ec838D21919b5Ec0",
+ "nonce": 776,
+ "gasLimit": "0x905469",
+ "gasPrice": "0xe8",
+ "maxFeePerGas": "0x64b085b92ba07f",
+ "maxPriorityFeePerGas": "0xbe",
+ "data": "0x6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6f",
+ "value": "0x4f88",
+ "accessList": [],
+ "chainId": "0x348f3fcb",
+ "maxFeePerBlobGas": "0xb8cc0be94a",
+ "blobVersionedHashes": [
+ "0x0134b8b105e49b6c97a9aade76ec97bd4357053c030b7f4370241fca10ab6ef3",
+ "0x0198df85d8142d33554e7369ed5695dc0a351b38741626e573f918812fa994ae"
+ ]
+ },
+ "privateKey": "0x63dd633e51d725e1f444b9a724729fb8bd052ec4737eceba804e846a32b4530c",
+ "unsignedLegacy": "0xf87d82030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6f",
+ "unsignedEip155": "0xf88482030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6f84348f3fcb8080",
+ "unsignedBerlin": "0x01f88384348f3fcb82030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc0",
+ "unsignedLondon": "0x02f88b84348f3fcb82030881be8764b085b92ba07f8390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc0",
+ "unsignedCancun": "0x03f8d584348f3fcb82030881be8764b085b92ba07f8390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc085b8cc0be94af842a00134b8b105e49b6c97a9aade76ec97bd4357053c030b7f4370241fca10ab6ef3a00198df85d8142d33554e7369ed5695dc0a351b38741626e573f918812fa994ae",
+ "signedLegacy": "0xf8c082030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6f1ba0d567c29e1775043d150df4e740d0e581d723f4950b2d35a5adb5a3a1b527686ea00fbcddbd1e8f33eb73a957c985b1ab85c333a42074c8801fe230ace1f246f826",
+ "signedEip155": "0xf8c482030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6f84691e7fb9a0900464b1bcd8e581911e8761960913c5b23ef063886533b4a9759c74ffcd4d80a0319be70fd62d7b2722aa1572b456755ae71fc7d1d3c02cdaafebbf4941033409",
+ "signedBerlin": "0x01f8c684348f3fcb82030881e88390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc080a0d2418f0ef98efb8120c5f35803fcc2b0def21e7bad27537fa8be144169393df7a0561b3ee3efe7f4cc07ff4289897c8b316eb6d4de32f60eddd71833d70c13c488",
+ "signedLondon": "0x02f8ce84348f3fcb82030881be8764b085b92ba07f8390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc080a04dfe6f43e3da767dcb2d717d7555f67448a47d8329ba40ba0b25e19ef217837da01612656de7ebf9929a66b7479a63927bb895b225af797adf3d32a890b279ab82",
+ "signedCancun": "0x03f9011884348f3fcb82030881be8764b085b92ba07f8390546994671a63481f6adf6e231296c3ec838d21919b5ec0824f88b85a6e8a5785fbcffa9d6303b7a22818c0c67aa5e3b1c097aa35af45f95038224f7b1e0724b14a2de88e07469fa80c6c1e172353269d5bd75af8c91b77585a6aa1b99c462b9fbf7010a9b8f4194260bfb307da9ba5b6202da6f0ef6fc085b8cc0be94af842a00134b8b105e49b6c97a9aade76ec97bd4357053c030b7f4370241fca10ab6ef3a00198df85d8142d33554e7369ed5695dc0a351b38741626e573f918812fa994ae01a051e0cac495af080a3d0777ccd5d6a514edfbcc156c0aa4f4218a8bb9259a5e3da00c17cfe7183fd3a1df95478e37f1b75442f4229f9f8ed61e364cfd5407b1a6d2",
+ "signatureLegacy": {
+ "r": "0xd567c29e1775043d150df4e740d0e581d723f4950b2d35a5adb5a3a1b527686e",
+ "s": "0x0fbcddbd1e8f33eb73a957c985b1ab85c333a42074c8801fe230ace1f246f826",
+ "v": "0x1b"
+ },
+ "signatureEip155": {
+ "r": "0x900464b1bcd8e581911e8761960913c5b23ef063886533b4a9759c74ffcd4d80",
+ "s": "0x319be70fd62d7b2722aa1572b456755ae71fc7d1d3c02cdaafebbf4941033409",
+ "v": "0x691e7fb9"
+ },
+ "signatureBerlin": {
+ "r": "0xd2418f0ef98efb8120c5f35803fcc2b0def21e7bad27537fa8be144169393df7",
+ "s": "0x561b3ee3efe7f4cc07ff4289897c8b316eb6d4de32f60eddd71833d70c13c488",
+ "v": "0x0"
+ },
+ "signatureLondon": {
+ "r": "0x4dfe6f43e3da767dcb2d717d7555f67448a47d8329ba40ba0b25e19ef217837d",
+ "s": "0x1612656de7ebf9929a66b7479a63927bb895b225af797adf3d32a890b279ab82",
+ "v": "0x0"
+ },
+ "signatureCancun": {
+ "r": "0x51e0cac495af080a3d0777ccd5d6a514edfbcc156c0aa4f4218a8bb9259a5e3d",
+ "s": "0x0c17cfe7183fd3a1df95478e37f1b75442f4229f9f8ed61e364cfd5407b1a6d2",
+ "v": "0x1"
+ }
+}
diff --git a/bolt-contracts/test/testdata/tx_mpt_proof_20785012.json b/bolt-contracts/test/testdata/tx_mpt_proof_20785012.json
new file mode 100644
index 000000000..82b1faeae
--- /dev/null
+++ b/bolt-contracts/test/testdata/tx_mpt_proof_20785012.json
@@ -0,0 +1,11 @@
+{
+ "root": "0x87bb9183296ce9e3b7a3246f6d3a778b99a5d7daaba2174750707407c7297365",
+ "proof": [
+ "f90131a060b2a2267b5925093805abbff6a9427865d9b0277666178def5e6d7f74cbea7da02d03b55bc0b1007e572b92e13d09e02cd1d740d81fc52c25fb522dcdc3c25717a08ce7808266f8a29dffb636d860c7fc13dccb47cfb5c1869d50da5f847faee9dda02d872b0e5b790bfd8f8851a539bdd67c8324812698ef8420bd3db06063f23383a046d720b85dbd71b53f9b46dc5908aafd65e0dc8d2a73bde7b548a6e59cdc1789a076e14ef77554b242d92b4832fbce89211b7b140ae202a4e4f465ef823204c9f7a0af30522b1024223eb65217ebcb5ddc22c1a62fbf43147fb43413e96eef821f66a02b8f47754c761574aa5ee4c348efc736b533c61aa7fa17051a5ea59fbeb3d7d2a08c8cc819bcd01c3ac2ba2d4926fba56579bc06722695d883d4ca1ea303a8f8c68080808080808080",
+ "f851a083516ffb4e11ae53aeb06863db356a756ef78cebe38802d69417148d369b986ca0351cee818717275e8e3962a54e1fa98e4b1b990aaad376b3d3e346cfb8fe4be6808080808080808080808080808080",
+ "f8d18080808080808080a05fd1f4eb5d94e828602fc1e19f03aae255efe360d81b9fc59099e21099327a7ba0219f99f84f7c704ca56575a4a3c59eaef6ee3ee3209cdc3ee4abe33c3c070c24a0afb31c1b8e96e6123e8a992d07ad7c0fe67648fbf5a6e4596f09be1affe80915a0572ca737439b54219818239a4420435dcd1546106e9f8074e00249eaf3127bb7a048862625cbbaffc46c2660d39f118c330e9fb3a3ceb2ca014e1ce285cf3e5e3ea0aec03c27f570694d19c676635151ee2abee5f19c2bf8c4701d5200df98d12ccd808080",
+ "f90211a008159cadb4be04f22dc7d9ee2f553720d7376797be70180f9860d9e6678d2b5fa0ba8725a1ca2023b727390f3fdb6dbddc51f23f95688c62a965a2d92d592dc074a0d27651ea59c6b3c5a7aa0b85d72b738d67a4cb9a1b6641e27fce330c12cbcc90a0448680ef94e3a42fa2045aa621bd984539c977927828b1690cd3f490f8f28202a0ddfa57de1a8e6477bfca9e07f7d567e8f9ead53e12dd6b26e46af8bb9ab2d42ba098de76237020aed42e4764dacf726cab9549f279a811158ef7d0a1865da49981a05ee4fbdd47c61ae9d9af574b90b3b14cbe146b3a377b6168662393a5799c53b0a05a710ac33be934301f2ec6d0d0eb911112c9ad90a783f1be56b3ea7a7751e856a0bf6171383c71ae373d3f86f4c1bf2bf0b34e117ea83ee8cdb0127e6f995b1b72a090cd396cf68946d396acc5b6b52474360ff402f21b622131b5e7ba0d370c8c07a06eb177bcd78f1db1138999efd996a1bfa9b32fe044b098bb0f8812798bfcad59a05cb6ed66df666831a1f9e01d2c809020d162024e066d5aa1248c4eb8137e4a71a020289d8787475543406639e2a9757c3e271b4947117ba617cd3f4791a7a0bd9aa0306aba70af61802a577df56b1dbe4bc93658e75fc6300f53e80ec8fd4f9fd8b6a0c88b6ebfdee7e968ef2f0eeec5d9d9001111df281a806b2326499472f3705937a0aa2a3290eda38ae66d13186f452410332f1a0e30ee9c5719cd420871779b830380",
+ "f87a20b87702f8740181eb8473a20d008507e172a822825208940ff71973b5243005b192d5bcf552fc2532b7bdec88015842095ebc400080c080a07a955ea50e980729226f547245bcf1e0e4ab69467cf82490a4bb0dbe9f35ae84a05f17d1a8f65b60905535ef0c36a8ec6d560c355949aeee2d3ab9791e7fe18fb4"
+ ],
+ "index": 179
+}
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
+}
diff --git a/bolt-sidecar/src/client/constraints_client.rs b/bolt-sidecar/src/client/constraints_client.rs
index 90730361c..47c4f926c 100644
--- a/bolt-sidecar/src/client/constraints_client.rs
+++ b/bolt-sidecar/src/client/constraints_client.rs
@@ -255,7 +255,7 @@ impl ConstraintsApi for ConstraintsClient {
mod tests {
use reqwest::Url;
- use crate::ConstraintsClient;
+ use super::ConstraintsClient;
#[test]
fn test_join_endpoints() {
diff --git a/bolt-sidecar/src/primitives/commitment.rs b/bolt-sidecar/src/primitives/commitment.rs
index 403808550..c8d69958e 100644
--- a/bolt-sidecar/src/primitives/commitment.rs
+++ b/bolt-sidecar/src/primitives/commitment.rs
@@ -261,8 +261,38 @@ impl ECDSASignatureExt for Signature {
#[cfg(test)]
mod tests {
+ use std::str::FromStr;
+
+ use alloy::{
+ hex,
+ primitives::{Address, Signature},
+ };
+
use super::{CommitmentRequest, InclusionRequest};
+ #[test]
+ fn test_create_digest() {
+ let json_req = r#"{
+ "slot": 633067,
+ "txs": ["0xf86b82016e84042343e0830f424094deaddeaddeaddeaddeaddeaddeaddeaddeaddead0780850344281a21a0e525fc31b5574722ff064bdd127c4441b0fc66de7dc44928e163cb68e9d807e5a00b3ec02fc1e34b0209f252369ad10b745cd5a51c88384a340f7a150d0e45e471"]
+ }"#;
+
+ let req: InclusionRequest = serde_json::from_str(json_req).unwrap();
+ let digest = req.digest();
+ assert_eq!(
+ hex::encode(digest.as_slice()),
+ "52ecc7832625c3d107aaba5b55d4509b48cd9f4f7ce375d6696d09bbf3310525"
+ );
+
+ // Verify signature over the digest
+ let sig = Signature::from_str("0xcdd20b2abbd8cdfb77ec2608e1227f8ce0f66133b9d0ec0ea68102c2152b82193e3be0d6967b7c20b83e1a2530daa3a07713556541dc2aa16a46d922e6145a2b01").unwrap();
+ let recovered = sig.recover_address_from_prehash(&digest).unwrap();
+ assert_eq!(
+ recovered,
+ Address::from_str("0x27083ED52464625660f3e30Aa5B9C20A30D7E110").unwrap()
+ );
+ }
+
#[test]
fn test_deserialize_inclusion_request() {
let json_req = r#"{