-
Notifications
You must be signed in to change notification settings - Fork 3.9k
op-validator: Add op-validator #14433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package mockrpc | ||
mslipper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| package mockrpc | ||
mslipper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| bin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
mslipper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - `--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 | ||
| ``` | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.