diff --git a/callback.go b/callback.go index 23f4a65db03cee5b78f01ba9ad9a1acc803146a4..2b076348d55870b4af7f7d3763becac359a12fd8 100644 --- a/callback.go +++ b/callback.go @@ -24,18 +24,30 @@ type Handler interface { type Request struct { ctx context.Context msg jsonrpcMessage + + peer PeerInfo } func (r *Request) Method() string { return r.msg.Method } + func (r *Request) Params() json.RawMessage { return r.msg.Params } + func (r *Request) Context() context.Context { return r.ctx } +func (r *Request) Remote() string { + return r.peer.RemoteAddr +} + +func (r *Request) Peer() PeerInfo { + return r.peer +} + func (r *Request) WithContext(ctx context.Context) *Request { if ctx == nil { panic("nil context") @@ -47,6 +59,7 @@ func (r *Request) WithContext(ctx context.Context) *Request { r2.msg = r.msg return r2 } + func (r *Request) Msg() jsonrpcMessage { return r.msg } diff --git a/doc.go b/doc.go index 3d620cad584233590929331c15f4ccf66cde51dd..1d2e59de071f8e910232ad0fc997a61f8e06a9ae 100644 --- a/doc.go +++ b/doc.go @@ -1,110 +1,4 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - /* -Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports. - -It provides access to the exported methods of an object across a network or other I/O -connection. After creating a server or client instance, objects can be registered to make -them visible as 'services'. Exported methods that follow specific conventions can be -called remotely. It also has support for the publish/subscribe pattern. - -RPC Methods - -Methods that satisfy the following criteria are made available for remote access: - - - method must be exported - - method returns 0, 1 (response or error) or 2 (response and error) values - -An example method: - - func (s *CalcService) Add(a, b int) (int, error) - -When the returned error isn't nil the returned integer is ignored and the error is sent -back to the client. Otherwise the returned integer is sent back to the client. - -Optional arguments are supported by accepting pointer values as arguments. E.g. if we want -to do the addition in an optional finite field we can accept a mod argument as pointer -value. - - func (s *CalcService) Add(a, b int, mod *int) (int, error) - -This RPC method can be called with 2 integers and a null value as third argument. In that -case the mod argument will be nil. Or it can be called with 3 integers, in that case mod -will be pointing to the given third argument. Since the optional argument is the last -argument the RPC package will also accept 2 integers as arguments. It will pass the mod -argument as nil to the RPC method. - -The server offers the ServeCodec method which accepts a ServerCodec instance. It will read -requests from the codec, process the request and sends the response back to the client -using the codec. The server can execute requests concurrently. Responses can be sent back -to the client out of order. - -An example server which uses the JSON codec: - - type CalculatorService struct {} - - func (s *CalculatorService) Add(a, b int) int { - return a + b - } - - func (s *CalculatorService) Div(a, b int) (int, error) { - if b == 0 { - return 0, errors.New("divide by zero") - } - return a/b, nil - } - - calculator := new(CalculatorService) - server := NewServer() - server.RegisterName("calculator", calculator) - l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"}) - server.ServeListener(l) - -Subscriptions - -The package also supports the publish subscribe pattern through the use of subscriptions. -A method that is considered eligible for notifications must satisfy the following -criteria: - - - method must be exported - - first method argument type must be context.Context - - method must have return types (rpc.Subscription, error) - -An example method: - - func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) { - ... - } - -When the service containing the subscription method is registered to the server, for -example under the "blockchain" namespace, a subscription is created by calling the -"blockchain_subscribe" method. - -Subscriptions are deleted when the user sends an unsubscribe request or when the -connection which was used to create the subscription is closed. This can be initiated by -the client and server. The server will close the connection for any write error. - -For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB. - -Reverse Calls - -In any method handler, an instance of rpc.Client can be accessed through the -ClientFromContext method. Using this client instance, server-to-client method calls can be -performed on the RPC connection. -*/ + */ package jrpc diff --git a/handler.go b/handler.go index be05ee8bcc1859fca8167bd664d8b1c46852afe0..1c4265f6483bf465df77d8c60550dbaad138a131 100644 --- a/handler.go +++ b/handler.go @@ -55,6 +55,8 @@ type handler struct { cancelRoot func() // cancel function for rootCtx conn jsonWriter // where responses will be sent log *zlog.Logger + + peer PeerInfo } type callProc struct { @@ -64,6 +66,7 @@ type callProc struct { func newHandler(connCtx context.Context, conn jsonWriter, reg Router) *handler { rootCtx, cancelRoot := context.WithCancel(connCtx) h := &handler{ + peer: PeerInfoFromContext(connCtx), reg: reg, conn: conn, respWait: make(map[string]*requestOp), @@ -71,10 +74,13 @@ func newHandler(connCtx context.Context, conn jsonWriter, reg Router) *handler { cancelRoot: cancelRoot, log: zlog.Ctx(connCtx), } - if conn.remoteAddr() != "" { + if h.peer.RemoteAddr != "" { cl := h.log.With().Str("conn", conn.remoteAddr()).Logger() h.log = &cl } + if h.peer.RemoteAddr == "" { + h.log.Error().Msg("CONNECTION WITHOUT REMOTE IP DETECTED. PLEASE MAKE SURE YOU KNOW WHAT YOU ARE DOING, OTHERWISE, THIS COULD BE A SECURITY ISSUE") + } return h } @@ -204,28 +210,29 @@ func (h *handler) handleResponse(msg *jsonrpcMessage) { } // handleCallMsg executes a call message and returns the answer. +// TODO: export prometheus metrics maybe? func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage { - start := NewTimer() + // start := NewTimer() switch { case msg.isNotification(): h.handleCall(ctx, msg) - h.log.Debug().Str("method", msg.Method).Dur("duration", start.Since()).Msg("Served") + // h.log.Debug().Str("method", msg.Method).Dur("duration", start.Since()).Msg("Served") return nil case msg.isCall(): resp := h.handleCall(ctx, msg) - var ctx []any - log2 := h.log.With() - log2.Str("reqid", string(msg.ID)).Dur("duration", start.Since()) + // var ctx []any + // log2 := h.log.With() + // log2.Str("reqid", string(msg.ID)).Dur("duration", start.Since()) if resp.Error != nil { - log2.Str("err", resp.Error.Message) - if resp.Error.Data != nil { - log2.Interface("errdata", resp.Error.Data) - } - sl := log2.Logger() - sl.Warn().Str("method", msg.Method).Interface("ctx", ctx).Msg("Served") + // log2.Str("err", resp.Error.Message) + // if resp.Error.Data != nil { + // log2.Interface("errdata", resp.Error.Data) + // } + // sl := log2.Logger() + // sl.Warn().Str("method", msg.Method).Interface("ctx", ctx).Msg("Served") } else { - sl := log2.Logger() - sl.Debug().Str("method", msg.Method).Interface("ctx", ctx).Msg("Served") + // sl := log2.Logger() + // sl.Debug().Str("method", msg.Method).Interface("ctx", ctx).Msg("Served") } return resp case msg.hasValidID(): @@ -241,29 +248,25 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage if !callb { return msg.errorResponse(&methodNotFoundError{method: msg.Method}) } + start := time.Now() - //TODO: there's a copy here - req := &Request{msg: *msg} - req.WithContext(cp.ctx) - answer := h.runHandler(req) - // Collect the statistics for RPC calls if metrics is enabled. - // We only care about pure rpc call. Filter out subscription. - rpcRequestGauge.Inc(1) - if answer.Error != nil { - failedRequestGauge.Inc(1) - } else { - successfulRequestGauge.Inc(1) + // TODO: there's a copy here + // TODO: add buffer pool here + req := &Request{ctx: cp.ctx, msg: *msg, peer: h.peer} + mw := NewReaderResponseWriterMsg(req) + h.reg.ServeRPC(mw, req) + { + // Collect the statistics for RPC calls if metrics is enabled. + // We only care about pure rpc call. Filter out subscription. + rpcRequestGauge.Inc(1) + if mw.msg.Error != nil { + failedRequestGauge.Inc(1) + } else { + successfulRequestGauge.Inc(1) + } + rpcServingTimer.UpdateSince(start) + updateServeTimeHistogram(msg.Method, mw.msg.Error == nil, time.Since(start)) } - rpcServingTimer.UpdateSince(start) - updateServeTimeHistogram(msg.Method, answer.Error == nil, time.Since(start)) - return answer -} - -//TODO: add buffer pool here -func (h *handler) runHandler(r *Request) *jsonrpcMessage { - // NOTE: this is where the callback is invoked - mw := NewReaderResponseWriterMsg(r) - h.reg.ServeRPC(mw, r) return mw.msg } diff --git a/middleware/logger.go b/middleware/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..553bddf707c3a83289e6987627a7ed847413d895 --- /dev/null +++ b/middleware/logger.go @@ -0,0 +1,171 @@ +package middleware + +// from +// https://github.com/go-chi/chi + +import ( + "bytes" + "context" + "log" + "net/http" + "os" + "runtime" + "time" + + "gfx.cafe/open/jrpc" +) + +var ( + // LogEntryCtxKey is the context.Context key to store the request log entry. + LogEntryCtxKey = &contextKey{"LogEntry"} + + // DefaultLogger is called by the Logger middleware handler to log each request. + // Its made a package-level variable so that it can be reconfigured for custom + // logging configurations. + DefaultLogger func(next jrpc.Handler) jrpc.Handler +) + +// Logger is a middleware that logs the start and end of each request, along +// with some useful data about what was requested, what the response status was, +// and how long it took to return. When standard output is a TTY, Logger will +// print in color, otherwise it will print in black and white. Logger prints a +// request ID if one is provided. +// +// IMPORTANT NOTE: Logger should go before any other middleware that may change +// the response, such as middleware.Recoverer. Example: +// r := chi.NewRouter() +// r.Use(middleware.Logger) // <--<< Logger should come before Recoverer +// r.Use(middleware.Recoverer) +// r.Get("/", handler) +func Logger(next jrpc.Handler) jrpc.Handler { + return DefaultLogger(next) +} + +// RequestLogger returns a logger handler using a custom LogFormatter. +func RequestLogger(f LogFormatter) func(next jrpc.Handler) jrpc.Handler { + return func(next jrpc.Handler) jrpc.Handler { + fn := func(w jrpc.ResponseWriter, r *jrpc.Request) { + entry := f.NewLogEntry(r) + ww := NewWrapResponseWriter(w, r.ProtoMajor) + + t1 := time.Now() + defer func() { + entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), nil) + }() + + next.ServeRPC(ww, WithLogEntry(r, entry)) + } + return jrpc.HandlerFunc(fn) + } +} + +// LogFormatter initiates the beginning of a new LogEntry per request. +// See DefaultLogFormatter for an example implementation. +type LogFormatter interface { + NewLogEntry(r *jrpc.Request) LogEntry +} + +// LogEntry records the final log when a request completes. +// See defaultLogEntry for an example implementation. +type LogEntry interface { + Write(status, bytes int, elapsed time.Duration, extra interface{}) + Panic(v interface{}, stack []byte) +} + +// GetLogEntry returns the in-context LogEntry for a request. +func GetLogEntry(r *jrpc.Request) LogEntry { + entry, _ := r.Context().Value(LogEntryCtxKey).(LogEntry) + return entry +} + +// WithLogEntry sets the in-context LogEntry for a request. +func WithLogEntry(r *jrpc.Request, entry LogEntry) *jrpc.Request { + r = r.WithContext(context.WithValue(r.Context(), LogEntryCtxKey, entry)) + return r +} + +// LoggerInterface accepts printing to stdlib logger or compatible logger. +type LoggerInterface interface { + Print(v ...interface{}) +} + +// DefaultLogFormatter is a simple logger that implements a LogFormatter. +type DefaultLogFormatter struct { + Logger LoggerInterface + NoColor bool +} + +// NewLogEntry creates a new LogEntry for the request. +func (l *DefaultLogFormatter) NewLogEntry(r *jrpc.Request) LogEntry { + useColor := !l.NoColor + entry := &defaultLogEntry{ + DefaultLogFormatter: l, + request: r, + buf: &bytes.Buffer{}, + useColor: useColor, + } + + reqID := GetReqID(r.Context()) + if reqID != "" { + cW(entry.buf, useColor, nYellow, "[%s] ", reqID) + } + cW(entry.buf, useColor, nCyan, "\"") + cW(entry.buf, useColor, bMagenta, "%s ", r.Method) + + scheme := "jrpc" + cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Method(), r.Params(), r.Msg().ID) + + entry.buf.WriteString("from ") + // TODO: remote addr/ real ip + // entry.buf.WriteString(r.RemoteAddr) + entry.buf.WriteString(" - ") + + return entry +} + +type defaultLogEntry struct { + *DefaultLogFormatter + request *jrpc.Request + buf *bytes.Buffer + useColor bool +} + +func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration, extra interface{}) { + switch { + case status < 200: + cW(l.buf, l.useColor, bBlue, "%03d", status) + case status < 300: + cW(l.buf, l.useColor, bGreen, "%03d", status) + case status < 400: + cW(l.buf, l.useColor, bCyan, "%03d", status) + case status < 500: + cW(l.buf, l.useColor, bYellow, "%03d", status) + default: + cW(l.buf, l.useColor, bRed, "%03d", status) + } + + cW(l.buf, l.useColor, bBlue, " %dB", bytes) + + l.buf.WriteString(" in ") + if elapsed < 500*time.Millisecond { + cW(l.buf, l.useColor, nGreen, "%s", elapsed) + } else if elapsed < 5*time.Second { + cW(l.buf, l.useColor, nYellow, "%s", elapsed) + } else { + cW(l.buf, l.useColor, nRed, "%s", elapsed) + } + + l.Logger.Print(l.buf.String()) +} + +func (l *defaultLogEntry) Panic(v interface{}, stack []byte) { + PrintPrettyStack(v) +} + +func init() { + color := true + if runtime.GOOS == "windows" { + color = false + } + DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags), NoColor: !color}) +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..77cf2de09856a09f47ec858c2cb9dbd79b77f2ff --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,23 @@ +package middleware + +import "gfx.cafe/open/jrpc" + +// New will create a new middleware handler from a jrpc.Handler. +func New(h jrpc.Handler) func(next jrpc.Handler) jrpc.Handler { + return func(next jrpc.Handler) jrpc.Handler { + return jrpc.HandlerFunc(func(w jrpc.ResponseWriter, r *jrpc.Request) { + h.ServeRPC(w, r) + }) + } +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/jrpc. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "jrpc/middleware context value " + k.name +} diff --git a/middleware/request_id.go b/middleware/request_id.go new file mode 100644 index 0000000000000000000000000000000000000000..2ed30185c877b082edead3c6b99ce55cc24cc1ee --- /dev/null +++ b/middleware/request_id.go @@ -0,0 +1,96 @@ +package middleware + +// Ported from Goji's middleware, source: +// jrpcs://github.com/zenazn/goji/tree/master/web/middleware + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "os" + "strings" + "sync/atomic" + + "gfx.cafe/open/jrpc" +) + +// Key to use when setting the request ID. +type ctxKeyRequestID int + +// RequestIDKey is the key that holds the unique request ID in a request context. +const RequestIDKey ctxKeyRequestID = 0 + +// RequestIDHeader is the name of the jrpc Header which contains the request id. +// Exported so that it can be changed by developers +var RequestIDHeader = "X-Request-Id" + +var ( + prefix string + reqid uint64 +) + +// A quick note on the statistics here: we're trying to calculate the chance that +// two randomly generated base62 prefixes will collide. We use the formula from +// jrpc://en.wikipedia.org/wiki/Birthday_problem +// +// P[m, n] \approx 1 - e^{-m^2/2n} +// +// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server +// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$ +// +// For a $k$ character base-62 identifier, we have $n(k) = 62^k$ +// +// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for +// our purposes, and is surely more than anyone would ever need in practice -- a +// process that is rebooted a handful of times a day for a hundred years has less +// than a millionth of a percent chance of generating two colliding IDs. + +func init() { + hostname, err := os.Hostname() + if hostname == "" || err != nil { + hostname = "localhost" + } + var buf [12]byte + var b64 string + for len(b64) < 10 { + rand.Read(buf[:]) + b64 = base64.StdEncoding.EncodeToString(buf[:]) + b64 = strings.NewReplacer("+", "", "/", "").Replace(b64) + } + + prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10]) +} + +// RequestID is a middleware that injects a request ID into the context of each +// request. A request ID is a string of the form "host.example.com/random-0001", +// where "random" is a base62 random string that uniquely identifies this go +// process, and where the last number is an atomically incremented request +// counter. +func RequestID(next jrpc.Handler) jrpc.Handler { + fn := func(w jrpc.ResponseWriter, r *jrpc.Request) { + ctx := r.Context() + myid := atomic.AddUint64(&reqid, 1) + requestID := fmt.Sprintf("%s-%06d", prefix, myid) + ctx = context.WithValue(ctx, RequestIDKey, requestID) + next.ServeRPC(w, r.WithContext(ctx)) + } + return jrpc.HandlerFunc(fn) +} + +// GetReqID returns a request ID from the given context if one is present. +// Returns the empty string if a request ID cannot be found. +func GetReqID(ctx context.Context) string { + if ctx == nil { + return "" + } + if reqID, ok := ctx.Value(RequestIDKey).(string); ok { + return reqID + } + return "" +} + +// NextRequestID generates the next request ID in the sequence. +func NextRequestID() uint64 { + return atomic.AddUint64(&reqid, 1) +} diff --git a/middleware/terminal.go b/middleware/terminal.go new file mode 100644 index 0000000000000000000000000000000000000000..477e67cbfbe4a2c55274e4fb97c4fef17cf06340 --- /dev/null +++ b/middleware/terminal.go @@ -0,0 +1,64 @@ +package middleware + +// Ported from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware +// https://github.com/go-chi/chi + +import ( + "fmt" + "io" + "os" +) + +var ( + // Normal colors + nBlack = []byte{'\033', '[', '3', '0', 'm'} + nRed = []byte{'\033', '[', '3', '1', 'm'} + nGreen = []byte{'\033', '[', '3', '2', 'm'} + nYellow = []byte{'\033', '[', '3', '3', 'm'} + nBlue = []byte{'\033', '[', '3', '4', 'm'} + nMagenta = []byte{'\033', '[', '3', '5', 'm'} + nCyan = []byte{'\033', '[', '3', '6', 'm'} + nWhite = []byte{'\033', '[', '3', '7', 'm'} + // Bright colors + bBlack = []byte{'\033', '[', '3', '0', ';', '1', 'm'} + bRed = []byte{'\033', '[', '3', '1', ';', '1', 'm'} + bGreen = []byte{'\033', '[', '3', '2', ';', '1', 'm'} + bYellow = []byte{'\033', '[', '3', '3', ';', '1', 'm'} + bBlue = []byte{'\033', '[', '3', '4', ';', '1', 'm'} + bMagenta = []byte{'\033', '[', '3', '5', ';', '1', 'm'} + bCyan = []byte{'\033', '[', '3', '6', ';', '1', 'm'} + bWhite = []byte{'\033', '[', '3', '7', ';', '1', 'm'} + + reset = []byte{'\033', '[', '0', 'm'} +) + +var IsTTY bool + +func init() { + // This is sort of cheating: if stdout is a character device, we assume + // that means it's a TTY. Unfortunately, there are many non-TTY + // character devices, but fortunately stdout is rarely set to any of + // them. + // + // We could solve this properly by pulling in a dependency on + // code.google.com/p/go.crypto/ssh/terminal, for instance, but as a + // heuristic for whether to print in color or in black-and-white, I'd + // really rather not. + fi, err := os.Stdout.Stat() + if err == nil { + m := os.ModeDevice | os.ModeCharDevice + IsTTY = fi.Mode()&m == m + } +} + +// colorWrite +func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) { + if IsTTY && useColor { + w.Write(color) + } + fmt.Fprintf(w, s, args...) + if IsTTY && useColor { + w.Write(reset) + } +} diff --git a/server.go b/server.go index 0c777fb7aabf94dacfc792373a4df7896111488a..5608478d031b5bae54deb1b4d391f66e755a6796 100644 --- a/server.go +++ b/server.go @@ -9,8 +9,10 @@ import ( mapset "github.com/deckarep/golang-set" ) -const MetadataApi = "rpc" -const EngineApi = "engine" +const ( + MetadataApi = "rpc" + EngineApi = "engine" +) // CodecOption specifies which type of messages a codec supports. @@ -39,12 +41,16 @@ func NewServer(r ...Router) *Server { return server } +func (s *Server) Router() Router { + return s.services +} + // RegisterName creates a service for the given receiver type under the given name. When no // methods on the given receiver match the criteria to be either a RPC method or a // subscription an error is returned. Otherwise a new service is created and added to the // service collection this server provides to clients. func (s *Server) RegisterName(name string, receiver any) error { - return registerStruct(s.services, name, receiver) + return RegisterStruct(s.services, name, receiver) } // ServeCodec reads incoming requests from codec, calls the appropriate callback and writes diff --git a/service.go b/service.go index 0f5e210347f5771a1f3ea318a5f1348652572c32..42c2824f98c70bbb786934c678fed6119ab134df 100644 --- a/service.go +++ b/service.go @@ -33,9 +33,9 @@ var ( stringType = reflect.TypeOf("") ) -// Supported for legacy reasons. -//TODO: we should redo our tests such that we no longer need this function. -func registerStruct(r Router, name string, rcvr any) error { +// A helper function that mimics the behavior of the handlers in the go-ethereum rpc package +// if you don't know how to use this, just use the chi-like interface instead. +func RegisterStruct(r Router, name string, rcvr any) error { rcvrVal := reflect.ValueOf(rcvr) if name == "" { return fmt.Errorf("no service name for type %s", rcvrVal.Type().String()) diff --git a/types.go b/types.go index a5e939b6ba741ba141b40cbb8f1ce3e2c1babdec..b2c54f5378c4ad8e461dbf0e25d63129239d4059 100644 --- a/types.go +++ b/types.go @@ -18,13 +18,9 @@ package jrpc import ( "context" - "encoding/json" - "fmt" - "math" "strconv" "strings" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) @@ -49,187 +45,6 @@ type jsonWriter interface { remoteAddr() string } -type BlockNumber int64 - -const ( - SafeBlockNumber = BlockNumber(-4) - FinalizedBlockNumber = BlockNumber(-3) - PendingBlockNumber = BlockNumber(-2) - LatestBlockNumber = BlockNumber(-1) - EarliestBlockNumber = BlockNumber(0) -) - -// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports: -// - "latest", "earliest" or "pending" as string arguments -// - the block number -// Returned errors: -// - an invalid block number error when the given argument isn't a known strings -// - an out of range error when the given block number is either too little or too large -func (bn *BlockNumber) UnmarshalJSON(data []byte) error { - input := trimQuotes(strings.ToLower(strings.TrimSpace(string(data)))) - - switch input { - case "earliest": - *bn = EarliestBlockNumber - return nil - case "latest": - *bn = LatestBlockNumber - return nil - case "pending": - *bn = PendingBlockNumber - return nil - case "finalized": - *bn = FinalizedBlockNumber - return nil - case "safe": - *bn = SafeBlockNumber - return nil - } - - blckNum, err := hexutil.DecodeUint64(input) - if err != nil { - return err - } - if blckNum > math.MaxInt64 { - return fmt.Errorf("block number larger than int64") - } - *bn = BlockNumber(blckNum) - return nil -} - -// MarshalText implements encoding.TextMarshaler. It marshals: -// - "latest", "earliest" or "pending" as strings -// - other numbers as hex -func (bn BlockNumber) MarshalText() ([]byte, error) { - switch bn { - case EarliestBlockNumber: - return []byte("earliest"), nil - case LatestBlockNumber: - return []byte("latest"), nil - case PendingBlockNumber: - return []byte("pending"), nil - case FinalizedBlockNumber: - return []byte("finalized"), nil - case SafeBlockNumber: - return []byte("safe"), nil - default: - return hexutil.Uint64(bn).MarshalText() - } -} - -func (bn BlockNumber) Int64() int64 { - return (int64)(bn) -} - -type BlockNumberOrHash struct { - BlockNumber *BlockNumber `json:"blockNumber,omitempty"` - BlockHash *common.Hash `json:"blockHash,omitempty"` - RequireCanonical bool `json:"requireCanonical,omitempty"` -} - -func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { - type erased BlockNumberOrHash - e := erased{} - err := json.Unmarshal(data, &e) - if err == nil { - if e.BlockNumber != nil && e.BlockHash != nil { - return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other") - } - bnh.BlockNumber = e.BlockNumber - bnh.BlockHash = e.BlockHash - bnh.RequireCanonical = e.RequireCanonical - return nil - } - var input string - err = json.Unmarshal(data, &input) - if err != nil { - return err - } - switch input { - case "earliest": - bn := EarliestBlockNumber - bnh.BlockNumber = &bn - return nil - case "latest": - bn := LatestBlockNumber - bnh.BlockNumber = &bn - return nil - case "pending": - bn := PendingBlockNumber - bnh.BlockNumber = &bn - return nil - case "finalized": - bn := FinalizedBlockNumber - bnh.BlockNumber = &bn - return nil - case "safe": - bn := SafeBlockNumber - bnh.BlockNumber = &bn - return nil - default: - if len(input) == 66 { - hash := common.Hash{} - err := hash.UnmarshalText([]byte(input)) - if err != nil { - return err - } - bnh.BlockHash = &hash - return nil - } else { - blckNum, err := hexutil.DecodeUint64(input) - if err != nil { - return err - } - if blckNum > math.MaxInt64 { - return fmt.Errorf("blocknumber too high") - } - bn := BlockNumber(blckNum) - bnh.BlockNumber = &bn - return nil - } - } -} - -func (bnh *BlockNumberOrHash) Number() (BlockNumber, bool) { - if bnh.BlockNumber != nil { - return *bnh.BlockNumber, true - } - return BlockNumber(0), false -} - -func (bnh *BlockNumberOrHash) String() string { - if bnh.BlockNumber != nil { - return strconv.Itoa(int(*bnh.BlockNumber)) - } - if bnh.BlockHash != nil { - return bnh.BlockHash.String() - } - return "nil" -} - -func (bnh *BlockNumberOrHash) Hash() (common.Hash, bool) { - if bnh.BlockHash != nil { - return *bnh.BlockHash, true - } - return common.Hash{}, false -} - -func BlockNumberOrHashWithNumber(blockNr BlockNumber) BlockNumberOrHash { - return BlockNumberOrHash{ - BlockNumber: &blockNr, - BlockHash: nil, - RequireCanonical: false, - } -} - -func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHash { - return BlockNumberOrHash{ - BlockNumber: nil, - BlockHash: &hash, - RequireCanonical: canonical, - } -} - // DecimalOrHex unmarshals a non-negative decimal or hex parameter into a uint64. type DecimalOrHex uint64 diff --git a/types_test.go b/types_test.go deleted file mode 100644 index 03cb91b102064bc349869058468ce926ea79e55f..0000000000000000000000000000000000000000 --- a/types_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package jrpc - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" -) - -func TestBlockNumberJSONUnmarshal(t *testing.T) { - tests := []struct { - input string - mustFail bool - expected BlockNumber - }{ - 0: {`"0x"`, true, BlockNumber(0)}, - 1: {`"0x0"`, false, BlockNumber(0)}, - 2: {`"0X1"`, false, BlockNumber(1)}, - 3: {`"0x00"`, true, BlockNumber(0)}, - 4: {`"0x01"`, true, BlockNumber(0)}, - 5: {`"0x1"`, false, BlockNumber(1)}, - 6: {`"0x12"`, false, BlockNumber(18)}, - 7: {`"0x7fffffffffffffff"`, false, BlockNumber(math.MaxInt64)}, - 8: {`"0x8000000000000000"`, true, BlockNumber(0)}, - 9: {"0", true, BlockNumber(0)}, - 10: {`"ff"`, true, BlockNumber(0)}, - 11: {`"pending"`, false, PendingBlockNumber}, - 12: {`"latest"`, false, LatestBlockNumber}, - 13: {`"earliest"`, false, EarliestBlockNumber}, - 14: {`someString`, true, BlockNumber(0)}, - 15: {`""`, true, BlockNumber(0)}, - 16: {``, true, BlockNumber(0)}, - } - - for i, test := range tests { - var num BlockNumber - err := json.Unmarshal([]byte(test.input), &num) - if test.mustFail && err == nil { - t.Errorf("Test %d should fail", i) - continue - } - if !test.mustFail && err != nil { - t.Errorf("Test %d should pass but got err: %v", i, err) - continue - } - if num != test.expected { - t.Errorf("Test %d got unexpected value, want %d, got %d", i, test.expected, num) - } - } -} - -func TestBlockNumberOrHash_UnmarshalJSON(t *testing.T) { - tests := []struct { - input string - mustFail bool - expected BlockNumberOrHash - }{ - 0: {`"0x"`, true, BlockNumberOrHash{}}, - 1: {`"0x0"`, false, BlockNumberOrHashWithNumber(0)}, - 2: {`"0X1"`, false, BlockNumberOrHashWithNumber(1)}, - 3: {`"0x00"`, true, BlockNumberOrHash{}}, - 4: {`"0x01"`, true, BlockNumberOrHash{}}, - 5: {`"0x1"`, false, BlockNumberOrHashWithNumber(1)}, - 6: {`"0x12"`, false, BlockNumberOrHashWithNumber(18)}, - 7: {`"0x7fffffffffffffff"`, false, BlockNumberOrHashWithNumber(math.MaxInt64)}, - 8: {`"0x8000000000000000"`, true, BlockNumberOrHash{}}, - 9: {"0", true, BlockNumberOrHash{}}, - 10: {`"ff"`, true, BlockNumberOrHash{}}, - 11: {`"pending"`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)}, - 12: {`"latest"`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)}, - 13: {`"earliest"`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)}, - 14: {`someString`, true, BlockNumberOrHash{}}, - 15: {`""`, true, BlockNumberOrHash{}}, - 16: {``, true, BlockNumberOrHash{}}, - 17: {`"0x0000000000000000000000000000000000000000000000000000000000000000"`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)}, - 18: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)}, - 19: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":false}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)}, - 20: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":true}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), true)}, - 21: {`{"blockNumber":"0x1"}`, false, BlockNumberOrHashWithNumber(1)}, - 22: {`{"blockNumber":"pending"}`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)}, - 23: {`{"blockNumber":"latest"}`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)}, - 24: {`{"blockNumber":"earliest"}`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)}, - 25: {`{"blockNumber":"0x1", "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, true, BlockNumberOrHash{}}, - } - - for i, test := range tests { - var bnh BlockNumberOrHash - err := json.Unmarshal([]byte(test.input), &bnh) - if test.mustFail && err == nil { - t.Errorf("Test %d should fail", i) - continue - } - if !test.mustFail && err != nil { - t.Errorf("Test %d should pass but got err: %v", i, err) - continue - } - hash, hashOk := bnh.Hash() - expectedHash, expectedHashOk := test.expected.Hash() - num, numOk := bnh.Number() - expectedNum, expectedNumOk := test.expected.Number() - if bnh.RequireCanonical != test.expected.RequireCanonical || - hash != expectedHash || hashOk != expectedHashOk || - num != expectedNum || numOk != expectedNumOk { - t.Errorf("Test %d got unexpected value, want %v, got %v", i, test.expected, bnh) - } - } -} - -func TestBlockNumberOrHash_WithNumber_MarshalAndUnmarshal(t *testing.T) { - tests := []struct { - name string - number int64 - }{ - {"max", math.MaxInt64}, - {"pending", int64(PendingBlockNumber)}, - {"latest", int64(LatestBlockNumber)}, - {"earliest", int64(EarliestBlockNumber)}, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - bnh := BlockNumberOrHashWithNumber(BlockNumber(test.number)) - marshalled, err := json.Marshal(bnh) - if err != nil { - t.Fatal("cannot marshal:", err) - } - var unmarshalled BlockNumberOrHash - err = json.Unmarshal(marshalled, &unmarshalled) - if err != nil { - t.Fatal("cannot unmarshal:", err) - } - if !reflect.DeepEqual(bnh, unmarshalled) { - t.Fatalf("wrong result: expected %v, got %v", bnh, unmarshalled) - } - }) - } -}