diff --git a/.gitignore b/.gitignore index a9b9f4768d5..cd37a7719b4 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,7 @@ __pycache__/ # direnv .envrc .direnv/ + +# Optimism test artifacts +crates/optimism/tests/proofs/contracts/artifacts +crates/optimism/tests/proofs/contracts/cache \ No newline at end of file diff --git a/crates/optimism/tests/Makefile b/crates/optimism/tests/Makefile index 153067f2432..7354b09810f 100644 --- a/crates/optimism/tests/Makefile +++ b/crates/optimism/tests/Makefile @@ -7,7 +7,7 @@ DEVNET := simple-historical-proof GO_PKG_NAME := proofs SOURCE_DIR := $(shell pwd) -.PHONY: all build run clean help +.PHONY: all build build-contracts run clean help # Default target all: build run @@ -26,8 +26,13 @@ run: fi; \ kurtosis run $(KURTOSIS_PACKAGE) --args-file $$DEVNET_PATH --enclave $(DEVNET) +# Build smart contract artifacts with Foundry +build-contracts: + @echo "Building contracts with forge..." + @cd "$(SOURCE_DIR)/proofs/contracts" && forge build || { echo "forge build failed"; exit 1; } + # Run E2E tests using Kurtosis -test-e2e-kurtosis: +test-e2e-kurtosis: build-contracts @echo "Running E2E tests with Kurtosis for $(DEVNET)" @DEVNET_PATH="$(SOURCE_DIR)/devnets/$(DEVNET).yaml"; \ if [ ! -z "$(DEVNET_CUSTOM_PATH)" ]; then \ diff --git a/crates/optimism/tests/proofs/contracts/foundry.toml b/crates/optimism/tests/proofs/contracts/foundry.toml new file mode 100644 index 00000000000..cad32fb0c14 --- /dev/null +++ b/crates/optimism/tests/proofs/contracts/foundry.toml @@ -0,0 +1,3 @@ +[profile.default] +src = "src" +out = "artifacts" diff --git a/crates/optimism/tests/proofs/contracts/src/SimpleStorage.sol b/crates/optimism/tests/proofs/contracts/src/SimpleStorage.sol new file mode 100644 index 00000000000..a975eab781f --- /dev/null +++ b/crates/optimism/tests/proofs/contracts/src/SimpleStorage.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract SimpleStorage { + uint256 public value; + + function setValue(uint256 newValue) external { + value = newValue; + } +} \ No newline at end of file diff --git a/crates/optimism/tests/proofs/init_test.go b/crates/optimism/tests/proofs/init_test.go index 7ac34347adf..b3c9b8d8027 100644 --- a/crates/optimism/tests/proofs/init_test.go +++ b/crates/optimism/tests/proofs/init_test.go @@ -1,4 +1,4 @@ -package l2reorg +package proofs import ( "testing" @@ -9,5 +9,5 @@ import ( // TestMain creates the test-setups against the shared backend func TestMain(m *testing.M) { // Other setups may be added here, hydrated from the same orchestrator - presets.DoMain(m, presets.WithMinimal()) + presets.DoMain(m, presets.WithSingleChainMultiNode()) } diff --git a/crates/optimism/tests/proofs/simple_storage_test.go b/crates/optimism/tests/proofs/simple_storage_test.go new file mode 100644 index 00000000000..f6893c20407 --- /dev/null +++ b/crates/optimism/tests/proofs/simple_storage_test.go @@ -0,0 +1,147 @@ +package proofs + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "os" + "strings" + "testing" + + "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/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 +} + +func TestStorageProofUsingSimpleStorageContract(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSingleChainMultiNode(t) + artifactPath := "contracts/artifacts/SimpleStorage.sol/SimpleStorage.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) + + type caseEntry struct { + Block uint64 + Value *big.Int + } + 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() + } + + cases = append(cases, caseEntry{ + Block: callRes.BlockNumber.Uint64(), + Value: writeVal, + }) + t.Logf("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() + } + + 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() + } + + require.Equal(t, gethProofRes, rethProofRes, "geth and reth proofs should match") + + 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() + } + } +}