Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ workflows:
op-service
op-supervisor
op-deployer
op-validator
op-e2e/system
op-e2e/e2eutils
op-e2e/opgeth
Expand Down
71 changes: 71 additions & 0 deletions op-service/testutils/mockrpc/matchers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package mockrpc

import (
"bytes"
"encoding/json"
"sync"
)

type ParamsMatcher func(params json.RawMessage) bool

func AnyParamsMatcher() ParamsMatcher {
return func(params json.RawMessage) bool {
return true
}
}

func NullMatcher() ParamsMatcher {
return func(params json.RawMessage) bool {
return isNullish(params)
}
}

var bufPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}

func getBuf() *bytes.Buffer {
return bufPool.Get().(*bytes.Buffer)
}

func putBuf(b *bytes.Buffer) {
b.Reset()
bufPool.Put(b)
}

// JSONParamsMatcher returns a ParamsMatcher that compares the JSON representation
// of the expected and actual parameters. json.Indent is used to canonicalize the
// JSON representation. Newlines are removed from the input prior to indentation.
func JSONParamsMatcher(expected json.RawMessage) ParamsMatcher {
if isNullish(expected) {
return NullMatcher()
}

replaced := bytes.ReplaceAll(expected, []byte("\n"), nil)

expDst := getBuf()
if err := json.Indent(expDst, replaced, "", ""); err != nil {
panic(err)
}
expStr := expDst.String()
putBuf(expDst)

return func(params json.RawMessage) bool {
paramsDst := getBuf()
defer putBuf(paramsDst)

replaced := bytes.ReplaceAll(params, []byte("\n"), nil)
if err := json.Indent(paramsDst, replaced, "", ""); err != nil {
return false
}

actStr := paramsDst.String()
return expStr == actStr
}
}

func isNullish(params json.RawMessage) bool {
return params == nil || string(params) == "null"
}
252 changes: 252 additions & 0 deletions op-service/testutils/mockrpc/mockrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package mockrpc

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"testing"
"time"

"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)

var (
ErrNoMoreCalls = errors.New("no more calls")
ErrNoMatchingCalls = errors.New("no matching calls")
)

type jsonRPCReq struct {
ID json.RawMessage `json:"id"`
Params json.RawMessage `json:"params"`
Method string `json:"method"`
}

type jsonRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}

type jsonRPCResp struct {
ID json.RawMessage `json:"id"`
JSONRPC string `json:"jsonrpc"`
Result any `json:"result"`
Error *jsonRPCError `json:"error"`
}

func newResp(id json.RawMessage, result any) jsonRPCResp {
return jsonRPCResp{
JSONRPC: "2.0",
ID: id,
Result: result,
}
}

func newErrResp(id json.RawMessage, errCode int, err error) jsonRPCResp {
return jsonRPCResp{
JSONRPC: "2.0",
ID: id,
Error: &jsonRPCError{
Code: errCode,
Message: err.Error(),
},
}
}

type rpcCall struct {
Method string `json:"method"`
ParamsMatcher ParamsMatcher `json:"-"`
Params json.RawMessage `json:"params"`
Result any `json:"result"`
Err string `json:"err"`
ErrCode int `json:"errCode"`
}

type MockRPC struct {
calls []rpcCall
lgr log.Logger

lis net.Listener
err error
}

type Option func(*MockRPC)

func WithExpectationsFile(t *testing.T, path string) Option {
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()

var calls []rpcCall
require.NoError(t, json.NewDecoder(f).Decode(&calls))

return func(rpc *MockRPC) {
for _, call := range calls {
rpc.calls = append(rpc.calls, rpcCall{
Method: call.Method,
ParamsMatcher: JSONParamsMatcher(call.Params),
Result: call.Result,
Err: call.Err,
ErrCode: call.ErrCode,
})
}
}
}

func NewMockRPC(t *testing.T, lgr log.Logger, opts ...Option) *MockRPC {
m := &MockRPC{
lgr: lgr,
}
for _, opt := range opts {
opt(m)
}

lis, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
m.lis = lis

srv := &http.Server{
Handler: m,
}

errCh := make(chan error, 1)
go func() {
err := srv.Serve(m.lis)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
errCh <- err
}()

timer := time.NewTimer(10 * time.Millisecond)
select {
case err := <-errCh:
require.NoError(t, err)
case <-timer.C:
}

t.Cleanup(func() {
require.NoError(t, srv.Shutdown(context.Background()))
require.NoError(t, <-errCh)
})

return m
}

func (m *MockRPC) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if m.err != nil {
m.writeResp(w, newErrResp(nil, -32601, m.err))
return
}

if r.Method != http.MethodPost {
m.lgr.Warn("method not allowed", "method", r.Method)
http.Error(w, "only POST requests are allowed", http.StatusMethodNotAllowed)
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
m.lgr.Warn("error reading request body", "err", err)
m.writeResp(w, newErrResp(nil, -32700, err))
return
}

var reqs []jsonRPCReq
if body[0] == '[' {
if err := json.Unmarshal(body, &reqs); err != nil {
m.lgr.Warn("error unmarshalling request body", "err", err)
m.writeResp(w, newErrResp(nil, -32700, err))
return
}
} else {
var req jsonRPCReq
if err := json.Unmarshal(body, &req); err != nil {
m.lgr.Warn("error unmarshalling request body", "err", err)
m.writeResp(w, newErrResp(nil, -32700, err))
return
}
reqs = append(reqs, req)
}

var resps []jsonRPCResp
for _, req := range reqs {
if len(m.calls) == 0 {
m.err = ErrNoMoreCalls
resps = append(resps, newErrResp(req.ID, -32601, m.err))
continue
}

call := m.calls[0]
m.calls = m.calls[1:]

if call.Method != req.Method {
m.lgr.Warn("method mismatch", "expected", call.Method, "actual", req.Method)
m.err = ErrNoMatchingCalls
resps = append(resps, newErrResp(req.ID, -32601, m.err))
continue
}

if !call.ParamsMatcher(req.Params) {
m.lgr.Warn("params did not match", "method", req.Method)
m.err = ErrNoMatchingCalls
resps = append(resps, newErrResp(req.ID, -32602, m.err))
continue
}

var resp jsonRPCResp
if call.Err == "" {
resp = newResp(req.ID, call.Result)
} else {
resp = newErrResp(req.ID, call.ErrCode, errors.New(call.Err))
}
resps = append(resps, resp)
}

var respBytes []byte
if len(resps) == 1 {
respBytes, err = json.Marshal(resps[0])
} else {
respBytes, err = json.Marshal(resps)
}
if err != nil {
m.lgr.Warn("error marshalling response", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
m.lgr.Warn("error writing response", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

func (m *MockRPC) Endpoint() string {
return fmt.Sprintf("http://%s", m.lis.Addr().String())
}

func (m *MockRPC) AssertExpectations(t *testing.T) {
require.NoError(t, m.err)
require.Empty(t, m.calls)
}

func (m *MockRPC) writeResp(w http.ResponseWriter, in jsonRPCResp) {
respBytes, err := json.Marshal(in)
if err != nil {
m.lgr.Warn("error marshalling response", "err", err)
return
}

w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
m.lgr.Warn("error writing response", "err", err)
return
}
}
1 change: 1 addition & 0 deletions op-validator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin
50 changes: 50 additions & 0 deletions op-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# op-validator

The op-validator is a tool for validating Optimism chain configurations and deployments. It works by calling into the
StandardValidator smart contracts (StandardValidatorV180 and StandardValidatorV200). These then perform a set of checks,
and return error codes for any issues found. These checks include:

- Contract implementations and versions
- Proxy configurations
- System parameters
- Cross-component relationships
- Security settings

## Usage

The validator supports different protocol versions through subcommands:

```bash
op-validator validate [version] [flags]
```

Where version is one of:

- `v1.8.0` - For validating protocol version 1.8.0
- `v2.0.0` - For validating protocol version 2.0.0

### Required Flags

- `--l1-rpc-url`: L1 RPC URL (can also be set via L1_RPC_URL environment variable)
- `--absolute-prestate`: Absolute prestate as hex string
- `--proxy-admin`: Proxy admin address as hex string. This should be a specific chain's proxy admin contract on L1.
It is not the proxy admin owner or the superchain proxy admin.
- `--system-config`: System config proxy address as hex string
- `--l2-chain-id`: L2 chain ID

### Optional Flags

- `--fail`: Exit with non-zero code if validation errors are found (defaults to true)

### Example

```bash
op-validator validate v2.0.0 \
--l1-rpc-url "https://mainnet.infura.io/v3/YOUR-PROJECT-ID" \
--absolute-prestate "0x1234..." \
--proxy-admin "0xabcd..." \
--system-config "0xefgh..." \
--l2-chain-id "10" \
--fail
```

Loading