diff --git a/crates/optimism/tests/proofs/contracts/src/MultiStorage.sol b/crates/optimism/tests/proofs/contracts/src/MultiStorage.sol new file mode 100644 index 00000000000..fc0bac34e74 --- /dev/null +++ b/crates/optimism/tests/proofs/contracts/src/MultiStorage.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract MultiStorage { + uint256 public slotA; + uint256 public slotB; + address public owner; + + constructor() { + owner = msg.sender; + } + + function setValues(uint256 _a, uint256 _b) external { + slotA = _a; + slotB = _b; + } +} \ No newline at end of file diff --git a/crates/optimism/tests/proofs/contracts/src/TokenVault.sol b/crates/optimism/tests/proofs/contracts/src/TokenVault.sol new file mode 100644 index 00000000000..3ee6f256413 --- /dev/null +++ b/crates/optimism/tests/proofs/contracts/src/TokenVault.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title TokenVault - realistic contract for eth_getProof testing +/// @notice Demonstrates mappings, nested mappings, and dynamic arrays +contract TokenVault { + struct Allowance { + uint256 amount; + bool active; + } + + // Mapping: user => balance + mapping(address => uint256) public balances; + + // Nested Mapping: owner => spender => allowance info + mapping(address => mapping(address => Allowance)) public allowances; + + // Dynamic array: list of all depositors + address[] public depositors; + + constructor() { + // initialize contract with a few entries + address alice = address(0xA11CE); + address bob = address(0xB0B); + + balances[alice] = 1000; + balances[bob] = 2000; + + allowances[alice][bob] = Allowance({amount: 300, active: true}); + allowances[bob][alice] = Allowance({amount: 150, active: false}); + + depositors.push(alice); + depositors.push(bob); + } + + function deposit() external payable { + balances[msg.sender] += msg.value; + depositors.push(msg.sender); + } + + function approve(address spender, uint256 amount) external { + allowances[msg.sender][spender] = Allowance({ + amount: amount, + active: true + }); + } + + function deactivateAllowance(address spender) external { + allowances[msg.sender][spender].active = false; + } + + function getDepositors() external view returns (address[] memory) { + return depositors; + } +} \ No newline at end of file diff --git a/crates/optimism/tests/proofs/simple_storage_test.go b/crates/optimism/tests/proofs/simple_storage_test.go index f6893c20407..0d4f2323090 100644 --- a/crates/optimism/tests/proofs/simple_storage_test.go +++ b/crates/optimism/tests/proofs/simple_storage_test.go @@ -1,12 +1,7 @@ package proofs import ( - "context" - "encoding/json" - "fmt" "math/big" - "os" - "strings" "testing" "github.com/ethereum-optimism/optimism/op-devstack/devtest" @@ -16,51 +11,30 @@ import ( "github.com/ethereum-optimism/optimism/op-service/txplan" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/crypto" ) -// minimal parts of artifact -type artifact struct { - ABI json.RawMessage `json:"abi"` - Bytecode struct { - Object string `json:"object"` - } `json:"bytecode"` -} - -// loadArtifact reads the forge artifact JSON at artifactPath and returns the parsed ABI -// and the creation bytecode (as bytes). It prefers bytecode.object (creation) and falls -// back to deployedBytecode.object if needed. -func loadArtifact(artifactPath string) (abi.ABI, []byte, error) { - data, err := os.ReadFile(artifactPath) +func simpleStorageSetValue(t devtest.T, parsedABI *abi.ABI, user *dsl.EOA, contractAddress common.Address, value *big.Int) *types.Receipt { + ctx := t.Ctx() + callData, err := parsedABI.Pack("setValue", value) if err != nil { - return abi.ABI{}, nil, err - } - var art artifact - if err := json.Unmarshal(data, &art); err != nil { - return abi.ABI{}, nil, err + t.Error("failed to pack set call data: %v", err) + t.FailNow() } - parsedABI, err := abi.JSON(strings.NewReader(string(art.ABI))) + + callTx := txplan.NewPlannedTx(user.Plan(), txplan.WithTo(&contractAddress), txplan.WithData(callData)) + callRes, err := callTx.Included.Eval(ctx) if err != nil { - return abi.ABI{}, nil, err - } - binHex := strings.TrimSpace(art.Bytecode.Object) - if binHex == "" { - return parsedABI, nil, fmt.Errorf("artifact missing bytecode") + t.Error("failed to create set tx: %v", err) + t.FailNow() } - return parsedABI, common.FromHex(binHex), nil -} -// deployContract deploys the contract creation bytecode from the given artifact. -// user must provide a Plan() method compatible with txplan.NewPlannedTx (kept generic). -func deployContract(ctx context.Context, user *dsl.EOA, bin []byte) (common.Address, uint64, error) { - tx := txplan.NewPlannedTx(user.Plan(), txplan.WithData(bin)) - res, err := tx.Included.Eval(ctx) - if err != nil { - return common.Address{}, 0, fmt.Errorf("deployment eval: %w", err) + if callRes.Status != types.ReceiptStatusSuccessful { + t.Error("set transaction failed") + t.FailNow() } - return res.ContractAddress, res.BlockNumber.Uint64(), nil + return callRes } func TestStorageProofUsingSimpleStorageContract(gt *testing.T) { @@ -85,6 +59,9 @@ func TestStorageProofUsingSimpleStorageContract(gt *testing.T) { } t.Logf("contract deployed at address %s in L2 block %d", contractAddress.Hex(), blockNum) + // fetch and verify initial proof (should be zeroed storage) + fetchAndVerifyProofs(ctx, t, sys, contractAddress, []common.Hash{common.HexToHash("0x0")}, blockNum) + type caseEntry struct { Block uint64 Value *big.Int @@ -92,23 +69,7 @@ func TestStorageProofUsingSimpleStorageContract(gt *testing.T) { var cases []caseEntry for i := 1; i <= 5; i++ { writeVal := big.NewInt(int64(i * 10)) - callData, err := parsedABI.Pack("setValue", writeVal) - if err != nil { - t.Error("failed to pack set call data: %v", err) - t.FailNow() - } - - callTx := txplan.NewPlannedTx(user.Plan(), txplan.WithTo(&contractAddress), txplan.WithData(callData)) - callRes, err := callTx.Included.Eval(ctx) - if err != nil { - t.Error("failed to create set tx: %v", err) - t.FailNow() - } - - if callRes.Status != types.ReceiptStatusSuccessful { - t.Error("set transaction failed") - t.FailNow() - } + callRes := simpleStorageSetValue(t, &parsedABI, user, contractAddress, writeVal) cases = append(cases, caseEntry{ Block: callRes.BlockNumber.Uint64(), @@ -117,31 +78,255 @@ func TestStorageProofUsingSimpleStorageContract(gt *testing.T) { t.Logf("setValue transaction included in L2 block %d", callRes.BlockNumber) } + // test reset storage to zero + callRes := simpleStorageSetValue(t, &parsedABI, user, contractAddress, big.NewInt(0)) + cases = append(cases, caseEntry{ + Block: callRes.BlockNumber.Uint64(), + Value: big.NewInt(0), + }) + t.Logf("reset setValue transaction included in L2 block %d", callRes.BlockNumber) + // for each case, get proof and verify for _, c := range cases { - gethProofRes, err := sys.L2EL.Escape().L2EthClient().GetProof(ctx, contractAddress, []common.Hash{common.HexToHash("0x0")}, hexutil.Uint64(c.Block).String()) - if err != nil { - t.Errorf("failed to get proof from L2EL at block %d: %v", c.Block, err) - t.FailNow() - } + fetchAndVerifyProofs(ctx, t, sys, contractAddress, []common.Hash{common.HexToHash("0x0")}, c.Block) + } - rethProofRes, err := sys.L2ELB.Escape().L2EthClient().GetProof(ctx, contractAddress, []common.Hash{common.HexToHash("0x0")}, hexutil.Uint64(c.Block).String()) - if err != nil { - t.Errorf("failed to get proof from L2ELB at block %d: %v", c.Block, err) - t.FailNow() - } + // test with non-existent storage slot + nonExistentSlot := common.HexToHash("0xdeadbeef") + fetchAndVerifyProofs(ctx, t, sys, contractAddress, []common.Hash{nonExistentSlot}, cases[len(cases)-1].Block) +} + +func multiStorageSetValues(t devtest.T, parsedABI *abi.ABI, user *dsl.EOA, contractAddress common.Address, aVal, bVal *big.Int) *types.Receipt { + ctx := t.Ctx() + callData, err := parsedABI.Pack("setValues", aVal, bVal) + if err != nil { + t.Error("failed to pack set call data: %v", err) + t.FailNow() + } - require.Equal(t, gethProofRes, rethProofRes, "geth and reth proofs should match") + callTx := txplan.NewPlannedTx(user.Plan(), txplan.WithTo(&contractAddress), txplan.WithData(callData)) + callRes, err := callTx.Included.Eval(ctx) + if err != nil { + t.Error("failed to create set tx: %v", err) + t.FailNow() + } - block, err := sys.L2EL.Escape().L2EthClient().InfoByNumber(ctx, c.Block) - if err != nil { - t.Errorf("failed to get L2 block %d: %v", c.Block, err) - t.FailNow() - } - err = rethProofRes.Verify(block.Root()) - if err != nil { - t.Errorf("proof verification failed at block %d: %v", c.Block, err) - t.FailNow() + if callRes.Status != types.ReceiptStatusSuccessful { + t.Error("set transaction failed") + t.FailNow() + } + return callRes +} + +func TestStorageProofUsingMultiStorageContract(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSingleChainMultiNode(t) + artifactPath := "contracts/artifacts/MultiStorage.sol/MultiStorage.json" + parsedABI, bin, err := loadArtifact(artifactPath) + if err != nil { + t.Error("failed to load artifact: %v", err) + t.FailNow() + } + + user := sys.FunderL2.NewFundedEOA(eth.OneHundredthEther) + + // deploy contract via helper + contractAddress, blockNum, err := deployContract(ctx, user, bin) + if err != nil { + t.Error("failed to deploy contract: %v", err) + t.FailNow() + } + + t.Logf("contract deployed at address %s in L2 block %d", contractAddress.Hex(), blockNum) + + // fetch and verify initial proof (should be zeroed storage) + fetchAndVerifyProofs(ctx, t, sys, contractAddress, []common.Hash{common.HexToHash("0x0"), common.HexToHash("0x1")}, blockNum) + + // set multiple storage slots + type caseEntry struct { + Block uint64 + SlotValues map[common.Hash]*big.Int + } + var cases []caseEntry + + for i := 1; i <= 5; i++ { + aVal := big.NewInt(int64(i * 10)) + bVal := big.NewInt(int64(i * 20)) + callRes := multiStorageSetValues(t, &parsedABI, user, contractAddress, aVal, bVal) + + cases = append(cases, caseEntry{ + Block: callRes.BlockNumber.Uint64(), + SlotValues: map[common.Hash]*big.Int{ + common.HexToHash("0x0"): aVal, + common.HexToHash("0x1"): bVal, + }, + }) + t.Logf("setValues transaction included in L2 block %d", callRes.BlockNumber) + } + + // test reset storage slots to zero + callRes := multiStorageSetValues(t, &parsedABI, user, contractAddress, big.NewInt(0), big.NewInt(0)) + cases = append(cases, caseEntry{ + Block: callRes.BlockNumber.Uint64(), + SlotValues: map[common.Hash]*big.Int{ + common.HexToHash("0x0"): big.NewInt(0), + common.HexToHash("0x1"): big.NewInt(0), + }, + }) + t.Logf("reset setValues transaction included in L2 block %d", callRes.BlockNumber) + + // for each case, get proof and verify + for _, c := range cases { + var slots []common.Hash + for slot := range c.SlotValues { + slots = append(slots, slot) } + + fetchAndVerifyProofs(ctx, t, sys, contractAddress, slots, c.Block) + } +} + +// helper: compute mapping slot = keccak256(pad(key) ++ pad(slotIndex)) +func mappingSlot(key common.Address, slotIndex uint64) common.Hash { + keyBytes := common.LeftPadBytes(key.Bytes(), 32) + slotBytes := common.LeftPadBytes(new(big.Int).SetUint64(slotIndex).Bytes(), 32) + return crypto.Keccak256Hash(append(keyBytes, slotBytes...)) +} + +// nested mapping: allowances[owner][spender] where `slotIndex` is the storage slot of allowances mapping +// innerSlot = keccak256(pad(owner) ++ pad(slotIndex)) +// entrySlot = keccak256(pad(spender) ++ innerSlot) +func nestedMappingSlot(owner, spender common.Address, slotIndex uint64) common.Hash { + ownerBytes := common.LeftPadBytes(owner.Bytes(), 32) + slotBytes := common.LeftPadBytes(new(big.Int).SetUint64(slotIndex).Bytes(), 32) + inner := crypto.Keccak256(ownerBytes, slotBytes) + spenderBytes := common.LeftPadBytes(spender.Bytes(), 32) + return crypto.Keccak256Hash(append(spenderBytes, inner...)) +} + +// dynamic array element slot: element k stored at Big(keccak256(p)) + k +func arrayIndexSlot(slotIndex uint64, idx uint64) common.Hash { + slotBytes := common.LeftPadBytes(new(big.Int).SetUint64(slotIndex).Bytes(), 32) + base := crypto.Keccak256(slotBytes) + baseInt := new(big.Int).SetBytes(base) + elem := new(big.Int).Add(baseInt, new(big.Int).SetUint64(idx)) + return common.BigToHash(elem) +} + +func TestTokenVaultStorageProofs(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSingleChainMultiNode(t) + artifactPath := "contracts/artifacts/TokenVault.sol/TokenVault.json" + parsedABI, bin, err := loadArtifact(artifactPath) + if err != nil { + t.Errorf("failed to load artifact: %v", err) + t.FailNow() + } + + // funder EOA that will deploy / interact + alice := sys.FunderL2.NewFundedEOA(eth.OneEther) + bob := sys.FunderL2.NewFundedEOA(eth.OneEther) + + // deploy contract + contractAddr, deployBlock, err := deployContract(ctx, alice, bin) + if err != nil { + t.Errorf("deploy failed: %v", err) + t.FailNow() + } + t.Logf("TokenVault deployed at %s block=%d", contractAddr.Hex(), deployBlock) + + userAddr := alice.Address() + + // call deposit (payable) + depositAmount := eth.OneHundredthEther + depositCalldata, err := parsedABI.Pack("deposit") + if err != nil { + t.Errorf("failed to pack deposit: %v", err) + t.FailNow() } + depTx := txplan.NewPlannedTx(alice.Plan(), txplan.WithTo(&contractAddr), txplan.WithData(depositCalldata), txplan.WithValue(depositAmount)) + depRes, err := depTx.Included.Eval(ctx) + if err != nil { + t.Errorf("deposit tx failed: %v", err) + t.FailNow() + } + + if depRes.Status != types.ReceiptStatusSuccessful { + t.Error("set transaction failed") + t.FailNow() + } + + depositBlock := depRes.BlockNumber.Uint64() + t.Logf("deposit included in block %d", depositBlock) + + // call approve(spender, amount) - use same user as spender for simplicity, or create another funded EOA + approveAmount := big.NewInt(100) + spenderAddr := bob.Address() + approveCalldata, err := parsedABI.Pack("approve", spenderAddr, approveAmount) + if err != nil { + t.Errorf("failed to pack approve: %v", err) + t.FailNow() + } + approveTx := txplan.NewPlannedTx(alice.Plan(), txplan.WithTo(&contractAddr), txplan.WithData(approveCalldata)) + approveRes, err := approveTx.Included.Eval(ctx) + if err != nil { + t.Errorf("approve tx failed: %v", err) + t.FailNow() + } + + if approveRes.Status != types.ReceiptStatusSuccessful { + t.Error("approve transaction failed") + t.FailNow() + } + + approveBlock := approveRes.BlockNumber.Uint64() + t.Logf("approve included in block %d", approveBlock) + + // call deactivateAllowance(spender) + deactCalldata, err := parsedABI.Pack("deactivateAllowance", spenderAddr) + if err != nil { + t.Errorf("failed to pack deactivateAllowance: %v", err) + t.FailNow() + } + deactTx := txplan.NewPlannedTx(alice.Plan(), txplan.WithTo(&contractAddr), txplan.WithData(deactCalldata)) + deactRes, err := deactTx.Included.Eval(ctx) + if err != nil { + t.Errorf("deactivateAllowance tx failed: %v", err) + t.FailNow() + } + + if deactRes.Status != types.ReceiptStatusSuccessful { + t.Error("deactivateAllowance transaction failed") + t.FailNow() + } + + deactBlock := deactRes.BlockNumber.Uint64() + t.Logf("deactivateAllowance included in block %d", deactBlock) + + // --- compute storage slots and verify proofs --- + const pBalances = 0 // mapping(address => uint256) slot index + const pAllowances = 1 // mapping(address => mapping(address => uint256)) slot index + const pDepositors = 2 // dynamic array slot index + + // balance slot for user + balanceSlot := mappingSlot(userAddr, pBalances) + // nested allowance slot owner=user, spender=spenderAddr + allowanceSlot := nestedMappingSlot(userAddr, spenderAddr, pAllowances) + // depositors[0] element slot + depositor0Slot := arrayIndexSlot(pDepositors, 0) + + // fetch & verify proofs at appropriate blocks + // balance after deposit (depositBlock) + t.Logf("Verifying balance slot %s at deposit block %d", balanceSlot.Hex(), depositBlock) + fetchAndVerifyProofs(ctx, t, sys, contractAddr, []common.Hash{balanceSlot, depositor0Slot}, depositBlock) + // allowance after approve (approveBlock) + t.Logf("Verifying allowance slot %s at approve block %d", allowanceSlot.Hex(), approveBlock) + fetchAndVerifyProofs(ctx, t, sys, contractAddr, []common.Hash{allowanceSlot}, approveBlock) + // after deactivation, allowance should be zero at deactBlock + t.Logf("Verifying allowance slot %s at deactivate block %d", allowanceSlot.Hex(), deactBlock) + fetchAndVerifyProofs(ctx, t, sys, contractAddr, []common.Hash{allowanceSlot}, deactBlock) } diff --git a/crates/optimism/tests/proofs/utils.go b/crates/optimism/tests/proofs/utils.go new file mode 100644 index 00000000000..74aeb7e30e2 --- /dev/null +++ b/crates/optimism/tests/proofs/utils.go @@ -0,0 +1,189 @@ +package proofs + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/stretchr/testify/require" +) + +// minimal parts of artifact +type artifact struct { + ABI json.RawMessage `json:"abi"` + Bytecode struct { + Object string `json:"object"` + } `json:"bytecode"` +} + +// loadArtifact reads the forge artifact JSON at artifactPath and returns the parsed ABI +// and the creation bytecode (as bytes). It prefers bytecode.object (creation) and falls +// back to deployedBytecode.object if needed. +func loadArtifact(artifactPath string) (abi.ABI, []byte, error) { + data, err := os.ReadFile(artifactPath) + if err != nil { + return abi.ABI{}, nil, err + } + var art artifact + if err := json.Unmarshal(data, &art); err != nil { + return abi.ABI{}, nil, err + } + parsedABI, err := abi.JSON(strings.NewReader(string(art.ABI))) + if err != nil { + return abi.ABI{}, nil, err + } + binHex := strings.TrimSpace(art.Bytecode.Object) + if binHex == "" { + return parsedABI, nil, fmt.Errorf("artifact missing bytecode") + } + return parsedABI, common.FromHex(binHex), nil +} + +// deployContract deploys the contract creation bytecode from the given artifact. +// user must provide a Plan() method compatible with txplan.NewPlannedTx (kept generic). +func deployContract(ctx context.Context, user *dsl.EOA, bin []byte) (common.Address, uint64, error) { + tx := txplan.NewPlannedTx(user.Plan(), txplan.WithData(bin)) + res, err := tx.Included.Eval(ctx) + if err != nil { + return common.Address{}, 0, fmt.Errorf("deployment eval: %w", err) + } + return res.ContractAddress, res.BlockNumber.Uint64(), nil +} + +// normalizeProofResponse standardizes an AccountResult obtained from eth_getProof +// across different client implementations (e.g., Geth, Reth) so that they can be +// compared meaningfully in tests. +// +// Ethereum clients may encode empty or zeroed data structures differently while +// still representing the same logical state. For example: +// - An empty storage proof may appear as [] (Geth) or ["0x80"] (Reth). +// +// This function normalizes such differences by: +// - Converting single-element proofs containing "0x80" to an empty proof slice. +func normalizeProofResponse(res *eth.AccountResult) { + for i := range res.StorageProof { + if len(res.StorageProof[i].Proof) == 1 && bytes.Equal(res.StorageProof[i].Proof[0], []byte{0x80}) { + res.StorageProof[i].Proof = []hexutil.Bytes{} + } + } +} + +// VerifyProof verifies an account and its storage proofs against a given state root. +// +// This function extends the standard behavior of go-ethereum’s AccountResult.Verify() +// by gracefully handling the case where the account’s storage trie root is empty +// (0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421). +func VerifyProof(res *eth.AccountResult, stateRoot common.Hash) error { + // Skip storage proof verification if the storage trie is empty. + if res.StorageHash != types.EmptyRootHash { + for i, entry := range res.StorageProof { + // load all MPT nodes into a DB + db := memorydb.New() + for j, encodedNode := range entry.Proof { + nodeKey := encodedNode + if len(encodedNode) >= 32 { // small MPT nodes are not hashed + nodeKey = crypto.Keccak256(encodedNode) + } + if err := db.Put(nodeKey, encodedNode); err != nil { + return fmt.Errorf("failed to load storage proof node %d of storage value %d into mem db: %w", j, i, err) + } + } + path := crypto.Keccak256(entry.Key) + val, err := trie.VerifyProof(res.StorageHash, path, db) + if err != nil { + return fmt.Errorf("failed to verify storage value %d with key %s (path %x) in storage trie %s: %w", i, entry.Key.String(), path, res.StorageHash, err) + } + if val == nil && entry.Value.ToInt().Cmp(common.Big0) == 0 { // empty storage is zero by default + continue + } + comparison, err := rlp.EncodeToBytes(entry.Value.ToInt().Bytes()) + if err != nil { + return fmt.Errorf("failed to encode storage value %d with key %s (path %x) in storage trie %s: %w", i, entry.Key.String(), path, res.StorageHash, err) + } + if !bytes.Equal(val, comparison) { + return fmt.Errorf("value %d in storage proof does not match proven value at key %s (path %x)", i, entry.Key.String(), path) + } + } + } + + accountClaimed := []any{uint64(res.Nonce), res.Balance.ToInt().Bytes(), res.StorageHash, res.CodeHash} + accountClaimedValue, err := rlp.EncodeToBytes(accountClaimed) + if err != nil { + return fmt.Errorf("failed to encode account from retrieved values: %w", err) + } + + // create a db with all account trie nodes + db := memorydb.New() + for i, encodedNode := range res.AccountProof { + nodeKey := encodedNode + if len(encodedNode) >= 32 { // small MPT nodes are not hashed + nodeKey = crypto.Keccak256(encodedNode) + } + if err := db.Put(nodeKey, encodedNode); err != nil { + return fmt.Errorf("failed to load account proof node %d into mem db: %w", i, err) + } + } + path := crypto.Keccak256(res.Address[:]) + accountProofValue, err := trie.VerifyProof(stateRoot, path, db) + if err != nil { + return fmt.Errorf("failed to verify account value with key %s (path %x) in account trie %s: %w", res.Address, path, stateRoot, err) + } + + if !bytes.Equal(accountClaimedValue, accountProofValue) { + return fmt.Errorf("L1 RPC is tricking us, account proof does not match provided deserialized values:\n"+ + " claimed: %x\n"+ + " proof: %x", accountClaimedValue, accountProofValue) + } + return err +} + +func fetchAndVerifyProofs(ctx context.Context, t devtest.T, sys *presets.SingleChainMultiNode, contractAddress common.Address, slots []common.Hash, block uint64) { + gethProofRes, err := sys.L2EL.Escape().L2EthClient().GetProof(ctx, contractAddress, slots, hexutil.Uint64(block).String()) + if err != nil { + t.Errorf("failed to get proof from L2EL at block %d: %v", block, err) + t.FailNow() + } + + rethProofRes, err := sys.L2ELB.Escape().L2EthClient().GetProof(ctx, contractAddress, slots, hexutil.Uint64(block).String()) + if err != nil { + t.Errorf("failed to get proof from L2ELB at block %d: %v", block, err) + t.FailNow() + } + normalizeProofResponse(rethProofRes) + + require.Equal(t, gethProofRes, rethProofRes, "geth and reth proofs should match") + + blockInfo, err := sys.L2EL.Escape().L2EthClient().InfoByNumber(ctx, block) + if err != nil { + t.Errorf("failed to get L2 block %d: %v", block, err) + t.FailNow() + } + + err = VerifyProof(gethProofRes, blockInfo.Root()) + if err != nil { + t.Errorf("geth proof verification failed at block %d: %v", block, err) + t.FailNow() + } + + err = VerifyProof(rethProofRes, blockInfo.Root()) + if err != nil { + t.Errorf("reth proof verification failed at block %d: %v", block, err) + t.FailNow() + } +}