diff --git a/Gopkg.lock b/Gopkg.lock index 49ebbd2ec..392aec2e9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -5,7 +5,7 @@ branch = "master" name = "github.com/anthdm/neo-go" packages = ["pkg/util"] - revision = "da01cdae5c15dcf7d6a046b27d2710c95e2cc66a" + revision = "9a605513fe8c5250c0ec71b30f9ecad49bd56c0a" [[projects]] branch = "master" @@ -19,6 +19,24 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + name = "github.com/go-kit/kit" + packages = ["log"] + revision = "4dc7be5d2d12881735283bcab7352178e190fc71" + version = "v0.6.0" + +[[projects]] + name = "github.com/go-logfmt/logfmt" + packages = ["."] + revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" + version = "v0.3.0" + +[[projects]] + name = "github.com/go-stack/stack" + packages = ["."] + revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" + version = "v1.7.0" + [[projects]] name = "github.com/go-yaml/yaml" packages = ["."] @@ -31,6 +49,12 @@ packages = ["."] revision = "553a641470496b2327abcac10b36396bd98e45c9" +[[projects]] + branch = "master" + name = "github.com/kr/logfmt" + packages = ["."] + revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" + [[projects]] name = "github.com/pkg/errors" packages = ["."] @@ -72,7 +96,7 @@ "leveldb/table", "leveldb/util" ] - revision = "211f780988068502fe874c44dae530528ebd840f" + revision = "169b1b37be738edb2813dab48c97a549bcf99bb5" [[projects]] name = "github.com/urfave/cli" @@ -89,7 +113,7 @@ "scrypt", "ssh/terminal" ] - revision = "8c653846df49742c4c85ec37e5d9f8d3ba657895" + revision = "374053ea96cb300f8671b8d3b07edeeb06e203b4" [[projects]] branch = "master" @@ -98,7 +122,7 @@ "unix", "windows" ] - revision = "c28acc882ebcbfbe8ce9f0f14b9ac26ee138dd51" + revision = "2f1e207ee39ff70f3433e49c6eb52677a515e3b5" [[projects]] name = "golang.org/x/text" @@ -121,11 +145,11 @@ "go/buildutil", "go/loader" ] - revision = "73e16cff9e0d4a802937444bebb562458548241d" + revision = "96caea41033df6f8c3974c845ab094f8ec3bd345" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "83630d732c34b1ddf24cd34025fd9fdd982a5e5075ec93a450e7edada658c6c9" + inputs-digest = "d4338e14e8103a6626ecf662f3d0c08e972a39e667a6c76f31cc8938f59f2cba" solver-name = "gps-cdcl" solver-version = 1 diff --git a/VERSION b/VERSION index 9eb2aa3f1..be386c9ed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.32.0 +0.33.0 diff --git a/cli/server/server.go b/cli/server/server.go index b8be9eb6e..b59ef8a32 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -2,11 +2,15 @@ package server import ( "fmt" + "os" + "os/signal" "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/network" + "github.com/CityOfZion/neo-go/pkg/rpc" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -43,6 +47,9 @@ func startServer(ctx *cli.Context) error { return cli.NewExitError(err, 1) } + interruptChan := make(chan os.Signal, 1) + signal.Notify(interruptChan, os.Interrupt) + serverConfig := network.NewServerConfig(config) chain, err := newBlockchain(net, config.ApplicationConfiguration.DataDirectoryPath) if err != nil { @@ -55,7 +62,34 @@ func startServer(ctx *cli.Context) error { } fmt.Println(logo()) - network.NewServer(serverConfig, chain).Start() + server := network.NewServer(serverConfig, chain) + rpcServer := rpc.NewServer(chain, config.ApplicationConfiguration.RPCPort, server) + errChan := make(chan error) + + go server.Start(errChan) + go rpcServer.Start(errChan) + var shutdownErr error + +Main: + for { + select { + case err := <-errChan: + shutdownErr = errors.Wrap(err, "Error encountered by server") + interruptChan <- os.Kill + + case <-interruptChan: + server.Shutdown() + if serverErr := rpcServer.Shutdown(); serverErr != nil { + shutdownErr = errors.Wrap(serverErr, "Error encountered whilst shutting down server") + } + break Main + } + } + + if shutdownErr != nil { + return cli.NewExitError(shutdownErr, 1) + } + return nil } diff --git a/pkg/core/block.go b/pkg/core/block.go index 9be44871d..81b6018db 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -16,10 +16,10 @@ type Block struct { BlockBase // Transaction list. - Transactions []*transaction.Transaction + Transactions []*transaction.Transaction `json:"tx"` // True if this block is created from trimmed data. - Trimmed bool + Trimmed bool `json:"-"` } // Header returns the Header of the Block. diff --git a/pkg/core/block_base.go b/pkg/core/block_base.go index cb9558c9b..45db3776b 100644 --- a/pkg/core/block_base.go +++ b/pkg/core/block_base.go @@ -14,33 +14,33 @@ import ( // BlockBase holds the base info of a block type BlockBase struct { // Version of the block. - Version uint32 + Version uint32 `json:"version"` // hash of the previous block. - PrevHash util.Uint256 + PrevHash util.Uint256 `json:"previousblockhash"` // Root hash of a transaction list. - MerkleRoot util.Uint256 + MerkleRoot util.Uint256 `json:"merkleroot"` // The time stamp of each block must be later than previous block's time stamp. // Generally the difference of two block's time stamp is about 15 seconds and imprecision is allowed. // The height of the block must be exactly equal to the height of the previous block plus 1. - Timestamp uint32 + Timestamp uint32 `json:"time"` // index/height of the block - Index uint32 + Index uint32 `json:"height"` // Random number also called nonce - ConsensusData uint64 + ConsensusData uint64 `json:"nonce"` // Contract addresss of the next miner - NextConsensus util.Uint160 + NextConsensus util.Uint160 `json:"nextminer"` // Padding that is fixed to 1 _ uint8 // Script used to validate the block - Script *transaction.Witness + Script *transaction.Witness `json:"script"` // hash of this block, created when binary encoded. hash util.Uint256 diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 4566f4eeb..2d4d8c8f6 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -9,6 +9,7 @@ type Blockchainer interface { AddBlock(*Block) error BlockHeight() uint32 HeaderHeight() uint32 + GetBlock(hash util.Uint256) (*Block, error) GetHeaderHash(int) util.Uint256 CurrentHeaderHash() util.Uint256 CurrentBlockHash() util.Uint256 diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index 7141ba342..f80fabc8f 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -13,35 +13,35 @@ import ( // Transaction is a process recorded in the NEO blockchain. type Transaction struct { // The type of the transaction. - Type TXType + Type TXType `json:"type"` // The trading version which is currently 0. - Version uint8 + Version uint8 `json:"-"` // Data specific to the type of the transaction. // This is always a pointer to a Transaction. - Data TXer + Data TXer `json:"-"` // Transaction attributes. - Attributes []*Attribute + Attributes []*Attribute `json:"attributes"` // The inputs of the transaction. - Inputs []*Input + Inputs []*Input `json:"vin"` // The outputs of the transaction. - Outputs []*Output + Outputs []*Output `json:"vout"` // The scripts that comes with this transaction. // Scripts exist out of the verification script // and invocation script. - Scripts []*Witness + Scripts []*Witness `json:"scripts"` // hash of the transaction hash util.Uint256 // Trimmed indicates this is a transaction from trimmed // data. - Trimmed bool + Trimmed bool `json:"-"` } // NewTrimmedTX returns a trimmed transaction with only its hash diff --git a/pkg/core/transaction/witness.go b/pkg/core/transaction/witness.go index 4dc609754..953b86a5e 100644 --- a/pkg/core/transaction/witness.go +++ b/pkg/core/transaction/witness.go @@ -9,8 +9,8 @@ import ( // Witness contains 2 scripts. type Witness struct { - InvocationScript []byte - VerificationScript []byte + InvocationScript []byte `json:"stack"` + VerificationScript []byte `json:"redeem"` } // DecodeBinary implements the payload interface. diff --git a/pkg/network/discovery.go b/pkg/network/discovery.go index d7be8cc20..f8c7010f9 100644 --- a/pkg/network/discovery.go +++ b/pkg/network/discovery.go @@ -14,29 +14,38 @@ type Discoverer interface { BackFill(...string) PoolCount() int RequestRemote(int) + RegisterBadAddr(string) + UnconnectedPeers() []string + BadPeers() []string } -// DefaultDiscovery +// DefaultDiscovery default implementation of the Discoverer interface. type DefaultDiscovery struct { - transport Transporter - dialTimeout time.Duration - addrs map[string]bool - badAddrs map[string]bool - requestCh chan int - backFill chan string - pool chan string + transport Transporter + dialTimeout time.Duration + addrs map[string]bool + badAddrs map[string]bool + unconnectedAddrs map[string]bool + requestCh chan int + connectedCh chan string + backFill chan string + badAddrCh chan string + pool chan string } // NewDefaultDiscovery returns a new DefaultDiscovery. func NewDefaultDiscovery(dt time.Duration, ts Transporter) *DefaultDiscovery { d := &DefaultDiscovery{ - transport: ts, - dialTimeout: dt, - addrs: make(map[string]bool), - badAddrs: make(map[string]bool), - requestCh: make(chan int), - backFill: make(chan string), - pool: make(chan string, maxPoolSize), + transport: ts, + dialTimeout: dt, + addrs: make(map[string]bool), + badAddrs: make(map[string]bool), + unconnectedAddrs: make(map[string]bool), + requestCh: make(chan int), + connectedCh: make(chan string), + backFill: make(chan string), + badAddrCh: make(chan string), + pool: make(chan string, maxPoolSize), } go d.run() return d @@ -58,16 +67,42 @@ func (d *DefaultDiscovery) PoolCount() int { return len(d.pool) } -// Request will try to establish a connection with n nodes. +// RequestRemote will try to establish a connection with n nodes. func (d *DefaultDiscovery) RequestRemote(n int) { d.requestCh <- n } -func (d *DefaultDiscovery) work(addrCh, badAddrCh chan string) { +// RegisterBadAddr registers the given address as a bad address. +func (d *DefaultDiscovery) RegisterBadAddr(addr string) { + d.badAddrCh <- addr + d.RequestRemote(1) +} + +// UnconnectedPeers returns all addresses of unconnected addrs. +func (d *DefaultDiscovery) UnconnectedPeers() []string { + var addrs []string + for addr := range d.unconnectedAddrs { + addrs = append(addrs, addr) + } + return addrs +} + +// BadPeers returns all addresses of bad addrs. +func (d *DefaultDiscovery) BadPeers() []string { + var addrs []string + for addr := range d.badAddrs { + addrs = append(addrs, addr) + } + return addrs +} + +func (d *DefaultDiscovery) work(addrCh chan string) { for { addr := <-addrCh if err := d.transport.Dial(addr, d.dialTimeout); err != nil { - badAddrCh <- addr + d.badAddrCh <- addr + } else { + d.connectedCh <- addr } } } @@ -79,12 +114,11 @@ func (d *DefaultDiscovery) next() string { func (d *DefaultDiscovery) run() { var ( maxWorkers = 5 - badAddrCh = make(chan string) workCh = make(chan string) ) for i := 0; i < maxWorkers; i++ { - go d.work(workCh, badAddrCh) + go d.work(workCh) } for { @@ -95,6 +129,7 @@ func (d *DefaultDiscovery) run() { } if _, ok := d.addrs[addr]; !ok { d.addrs[addr] = true + d.unconnectedAddrs[addr] = true d.pool <- addr } case n := <-d.requestCh: @@ -103,11 +138,15 @@ func (d *DefaultDiscovery) run() { workCh <- d.next() } }() - case addr := <-badAddrCh: + case addr := <-d.badAddrCh: d.badAddrs[addr] = true + delete(d.unconnectedAddrs, addr) go func() { workCh <- d.next() }() + + case addr := <-d.connectedCh: + delete(d.unconnectedAddrs, addr) } } } diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 72014d3b7..c007cddf3 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -23,6 +23,9 @@ func (chain testChain) BlockHeight() uint32 { func (chain testChain) HeaderHeight() uint32 { return 0 } +func (chain testChain) GetBlock(hash util.Uint256) (*core.Block, error) { + return nil, nil +} func (chain testChain) GetHeaderHash(int) util.Uint256 { return util.Uint256{} } @@ -41,9 +44,12 @@ func (chain testChain) HasTransaction(util.Uint256) bool { type testDiscovery struct{} -func (d testDiscovery) BackFill(addrs ...string) {} -func (d testDiscovery) PoolCount() int { return 0 } -func (d testDiscovery) RequestRemote(n int) {} +func (d testDiscovery) BackFill(addrs ...string) {} +func (d testDiscovery) PoolCount() int { return 0 } +func (d testDiscovery) RegisterBadAddr(string) {} +func (d testDiscovery) UnconnectedPeers() []string { return []string{} } +func (d testDiscovery) RequestRemote(n int) {} +func (d testDiscovery) BadPeers() []string { return []string{} } type localTransport struct{} diff --git a/pkg/network/server.go b/pkg/network/server.go index 96134aff7..47ce3cf86 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -34,7 +34,7 @@ type ( // ServerConfig holds the Server configuration. ServerConfig - // id also known as the nonce of te server. + // id also known as the nonce of the server. id uint32 transport Transporter @@ -84,8 +84,13 @@ func NewServer(config ServerConfig, chain *core.Blockchain) *Server { return s } +// ID returns the servers ID. +func (s *Server) ID() uint32 { + return s.id +} + // Start will start the server and its underlying transport. -func (s *Server) Start() { +func (s *Server) Start(errChan chan error) { log.WithFields(log.Fields{ "blockHeight": s.chain.BlockHeight(), "headerHeight": s.chain.HeaderHeight(), @@ -96,6 +101,22 @@ func (s *Server) Start() { s.run() } +// Shutdown disconnects all peers and stops listening. +func (s *Server) Shutdown() { + log.WithFields(log.Fields{ + "peers": s.PeerCount(), + }).Info("shutting down server") + close(s.quit) +} + +func (s *Server) UnconnectedPeers() []string { + return s.discovery.UnconnectedPeers() +} + +func (s *Server) BadPeers() []string { + return s.discovery.BadPeers() +} + func (s *Server) run() { // Ask discovery to connect with remote nodes to fill up // the server minimum peer slots. @@ -130,7 +151,7 @@ func (s *Server) run() { "endpoint": p.Endpoint(), }).Info("new peer connected") case drop := <-s.unregister: - s.discovery.RequestRemote(1) + s.discovery.RegisterBadAddr(drop.peer.Endpoint().String()) delete(s.peers, drop.peer) log.WithFields(log.Fields{ "endpoint": drop.peer.Endpoint(), @@ -141,6 +162,12 @@ func (s *Server) run() { } } +// Peers returns the current list of peers connected to +// the server. +func (s *Server) Peers() map[Peer]bool { + return s.peers +} + // PeerCount returns the number of current connected peers. func (s *Server) PeerCount() int { s.lock.RLock() diff --git a/pkg/network/tcp_transport.go b/pkg/network/tcp_transport.go index 1e4c3c57e..915440b2c 100644 --- a/pkg/network/tcp_transport.go +++ b/pkg/network/tcp_transport.go @@ -2,6 +2,7 @@ package network import ( "net" + "regexp" "time" log "github.com/sirupsen/logrus" @@ -54,12 +55,31 @@ func (t *TCPTransport) Accept() { conn, err := l.Accept() if err != nil { log.Warnf("TCP accept error: %s", err) + if t.isCloseError(err) { + break + } + continue } go t.handleConn(conn) } } +func (t *TCPTransport) isCloseError(err error) bool { + regex, err := regexp.Compile(".* use of closed network connection") + if err != nil { + return false + } + + if opErr, ok := err.(*net.OpError); ok { + if regex.Match([]byte(opErr.Error())) { + return true + } + } + + return false +} + func (t *TCPTransport) handleConn(conn net.Conn) { p := NewTCPPeer(conn, t.proto) t.server.register <- p diff --git a/pkg/rpc/errors.go b/pkg/rpc/errors.go new file mode 100644 index 000000000..662b24c6b --- /dev/null +++ b/pkg/rpc/errors.go @@ -0,0 +1,64 @@ +package rpc + +import ( + "fmt" + "net/http" +) + +type ( + // Error object for outputting JSON-RPC 2.0 + // errors. + Error struct { + Code int64 `json:"code"` + HTTPCode int `json:"-"` + Cause error `json:"-"` + Message string `json:"message"` + Data string `json:"data,omitempty"` + } +) + +func newError(code int64, httpCode int, message string, data string, cause error) *Error { + return &Error{ + Code: code, + HTTPCode: httpCode, + Cause: cause, + Message: message, + Data: data, + } + +} + +// NewParseError creates a new error with code +// -32700.:%s +func NewParseError(data string, cause error) *Error { + return newError(-32700, http.StatusBadRequest, "Parse Error", data, cause) +} + +// NewInvalidRequestError creates a new error with +// code -32600. +func NewInvalidRequestError(data string, cause error) *Error { + return newError(-32600, http.StatusUnprocessableEntity, "Invalid Request", data, cause) +} + +// NewMethodNotFoundError creates a new error with +// code -32601. +func NewMethodNotFoundError(data string, cause error) *Error { + return newError(-32601, http.StatusMethodNotAllowed, "Method not found", data, cause) +} + +// NewInvalidParamsError creates a new error with +// code -32602. +func NewInvalidParamsError(data string, cause error) *Error { + return newError(-32602, http.StatusUnprocessableEntity, "Invalid Params", data, cause) +} + +// NewInternalServerError creates a new error with +// code -32603. +func NewInternalServerError(data string, cause error) *Error { + return newError(-32603, http.StatusInternalServerError, "Internal error", data, cause) +} + +// Error implements the error interface. +func (e Error) Error() string { + return fmt.Sprintf("%s (%d) - %s - %s", e.Message, e.Code, e.Data, e.Cause) +} diff --git a/pkg/rpc/param.go b/pkg/rpc/param.go new file mode 100644 index 000000000..bb3c93c37 --- /dev/null +++ b/pkg/rpc/param.go @@ -0,0 +1,21 @@ +package rpc + +import ( + "fmt" +) + +type ( + // Param represent a param either passed to + // the server or to send to a server using + // the client. + Param struct { + StringVal string + IntVal int + Type string + RawValue interface{} + } +) + +func (p Param) String() string { + return fmt.Sprintf("%v", p.RawValue) +} diff --git a/pkg/rpc/params.go b/pkg/rpc/params.go new file mode 100644 index 000000000..0995c734a --- /dev/null +++ b/pkg/rpc/params.go @@ -0,0 +1,62 @@ +package rpc + +import ( + "encoding/json" +) + +type ( + // Params represent the JSON-RPC params. + Params []Param +) + +// UnmarshalJSON implements the Unmarshaller +// interface. +func (p *Params) UnmarshalJSON(data []byte) error { + var params []interface{} + + err := json.Unmarshal(data, ¶ms) + if err != nil { + return err + } + + for i := 0; i < len(params); i++ { + param := Param{ + RawValue: params[i], + } + + switch val := params[i].(type) { + case string: + param.StringVal = val + param.Type = "string" + + case float64: + newVal, _ := params[i].(float64) + param.IntVal = int(newVal) + param.Type = "number" + } + + *p = append(*p, param) + } + + return nil +} + +// ValueAt returns the param struct for the given +// index if it exists. +func (p Params) ValueAt(index int) (*Param, bool) { + if len(p) > index { + return &p[index], true + } + + return nil, false +} + +// ValueAtAndType returns the param struct at the given index if it +// exists and matches the given type. +func (p Params) ValueAtAndType(index int, valueType string) (*Param, bool) { + if len(p) > index && valueType == p[index].Type { + return &p[index], true + } + + return nil, false +} diff --git a/pkg/rpc/request.go b/pkg/rpc/request.go new file mode 100644 index 000000000..554f799d5 --- /dev/null +++ b/pkg/rpc/request.go @@ -0,0 +1,119 @@ +package rpc + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + log "github.com/sirupsen/logrus" +) + +const ( + jsonRPCVersion = "2.0" +) + +type ( + // Request represents a standard JSON-RPC 2.0 + // request: http://www.jsonrpc.org/specification#request_object. + Request struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + RawParams json.RawMessage `json:"params,omitempty"` + RawID json.RawMessage `json:"id,omitempty"` + } + + // Response represents a standard JSON-RPC 2.0 + // response: http://www.jsonrpc.org/specification#response_object. + Response struct { + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` + ID json.RawMessage `json:"id,omitempty"` + } +) + +// NewRequest creates a new Request struct. +func NewRequest() *Request { + return &Request{ + JSONRPC: jsonRPCVersion, + } +} + +// DecodeData decodes the given reader into the the request +// struct. +func (r *Request) DecodeData(data io.ReadCloser) error { + defer data.Close() + + err := json.NewDecoder(data).Decode(r) + if err != nil { + return fmt.Errorf("Error parsing JSON payload: %s", err) + } + + if r.JSONRPC != jsonRPCVersion { + return fmt.Errorf("Invalid version, expected 2.0 got: '%s'", r.JSONRPC) + } + + return nil +} + +// Params takes a slice of any type and attempts to bind +// the params to it. +func (r *Request) Params() (*Params, error) { + params := Params{} + + err := json.Unmarshal(r.RawParams, ¶ms) + if err != nil { + return nil, fmt.Errorf("Error parsing params field in payload: %s", err) + } + + return ¶ms, nil +} + +// WriteErrorResponse writes an error response to the ResponseWriter. +func (r Request) WriteErrorResponse(w http.ResponseWriter, err error) { + jsonErr, ok := err.(*Error) + if !ok { + jsonErr = NewInternalServerError("Internal server error", err) + } + + response := Response{ + JSONRPC: r.JSONRPC, + Error: jsonErr, + ID: r.RawID, + } + + logFields := log.Fields{ + "err": jsonErr.Cause, + "method": r.Method, + } + params, err := r.Params() + if err == nil { + logFields["params"] = *params + } + + log.WithFields(logFields).Error("Error encountered with rpc request") + w.WriteHeader(jsonErr.HTTPCode) + r.writeServerResponse(w, response) +} + +// WriteResponse encodes the response and writes it to the ResponseWriter. +func (r Request) WriteResponse(w http.ResponseWriter, result interface{}) { + response := Response{ + JSONRPC: r.JSONRPC, + Result: result, + ID: r.RawID, + } + + r.writeServerResponse(w, response) +} + +func (r Request) writeServerResponse(w http.ResponseWriter, response Response) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + encoder := json.NewEncoder(w) + err := encoder.Encode(response) + + if err != nil { + fmt.Println(err) + } +} diff --git a/pkg/rpc/result/peers.go b/pkg/rpc/result/peers.go new file mode 100644 index 000000000..98442acaa --- /dev/null +++ b/pkg/rpc/result/peers.go @@ -0,0 +1,58 @@ +package result + +import ( + "strings" +) + +type ( + // Peers payload for outputting peers in `getpeers` RPC call. + Peers struct { + Unconnected []Peer `json:"unconnected"` + Connected []Peer `json:"connected"` + Bad []Peer `json:"bad"` + } + + // Peer represents the peer. + Peer struct { + Address string `json:"address"` + Port string `json:"port"` + } +) + +// NewPeers creates a new Peers struct. +func NewPeers() Peers { + return Peers{ + Unconnected: []Peer{}, + Connected: []Peer{}, + Bad: []Peer{}, + } +} + +// AddPeer adds a peer to the given peer type slice. +func (p *Peers) AddPeer(peerType string, addr string) { + addressParts := strings.Split(addr, ":") + peer := Peer{ + Address: addressParts[0], + Port: addressParts[1], + } + + switch peerType { + case "unconnected": + p.Unconnected = append( + p.Unconnected, + peer, + ) + + case "connected": + p.Connected = append( + p.Connected, + peer, + ) + + case "bad": + p.Bad = append( + p.Bad, + peer, + ) + } +} diff --git a/pkg/rpc/result/version.go b/pkg/rpc/result/version.go new file mode 100644 index 000000000..145d197b8 --- /dev/null +++ b/pkg/rpc/result/version.go @@ -0,0 +1,11 @@ +package result + +type ( + // Version model used for reporting server version + // info. + Version struct { + Port uint16 `json:"port"` + Nonce uint32 `json:"nonce"` + UserAgent string `json:"useragent"` + } +) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go new file mode 100644 index 000000000..f8f4c4651 --- /dev/null +++ b/pkg/rpc/server.go @@ -0,0 +1,181 @@ +package rpc + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/network" + "github.com/CityOfZion/neo-go/pkg/rpc/result" + "github.com/CityOfZion/neo-go/pkg/util" + log "github.com/sirupsen/logrus" +) + +type ( + // Server represents the JSON-RPC 2.0 server. + Server struct { + *http.Server + chain core.Blockchainer + coreServer *network.Server + } +) + +// NewServer creates a new Server struct. +func NewServer(chain core.Blockchainer, port uint16, coreServer *network.Server) Server { + return Server{ + Server: &http.Server{ + Addr: fmt.Sprintf(":%d", port), + }, + chain: chain, + coreServer: coreServer, + } +} + +// Start creates a new JSON-RPC server +// listening on the configured port. +func (s *Server) Start(errChan chan error) { + s.Handler = http.HandlerFunc(s.requestHandler) + log.WithFields(log.Fields{ + "endpoint": s.Addr, + }).Info("starting rpc-server") + + errChan <- s.ListenAndServe() +} + +// Shutdown overrride the http.Server Shutdown +// method. +func (s *Server) Shutdown() error { + log.WithFields(log.Fields{ + "endpoint": s.Addr, + }).Info("shutting down rpc-server") + return s.Server.Shutdown(context.Background()) +} + +func (s *Server) requestHandler(w http.ResponseWriter, httpRequest *http.Request) { + req := NewRequest() + + if httpRequest.Method != "POST" { + req.WriteErrorResponse( + w, + NewInvalidParamsError( + fmt.Sprintf("Invalid method '%s', please retry with 'POST'", httpRequest.Method), nil, + ), + ) + return + } + + err := req.DecodeData(httpRequest.Body) + if err != nil { + req.WriteErrorResponse(w, NewParseError("Problem parsing JSON-RPC request body", err)) + return + } + + reqParams, err := req.Params() + if err != nil { + req.WriteErrorResponse(w, NewInvalidParamsError("Problem parsing request parameters", err)) + return + } + + s.methodHandler(w, req, *reqParams) +} + +func (s *Server) methodHandler(w http.ResponseWriter, req *Request, reqParams Params) { + log.WithFields(log.Fields{ + "method": req.Method, + "params": fmt.Sprintf("%v", reqParams), + }).Info("processing rpc request") + + var results interface{} + var resultsErr *Error + + switch req.Method { + case "getbestblockhash": + results = s.chain.CurrentBlockHash().String() + + case "getblock": + var hash util.Uint256 + var err error + + param, exists := reqParams.ValueAt(0) + if !exists { + err = errors.New("Param at index at 0 doesn't exist") + resultsErr = NewInvalidParamsError(err.Error(), err) + break + } + + switch param.Type { + case "string": + hash, err = util.Uint256DecodeString(param.StringVal) + if err != nil { + resultsErr = NewInvalidParamsError("Problem decoding block hash", err) + break + } + case "number": + hash = s.chain.GetHeaderHash(param.IntVal) + case "default": + err = errors.New("Expected param at index 0 to be either string or number") + resultsErr = NewInvalidParamsError(err.Error(), err) + break + } + + results, err = s.chain.GetBlock(hash) + if err != nil { + resultsErr = NewInternalServerError(fmt.Sprintf("Problem locating block with hash: %s", hash), err) + break + } + + case "getblockcount": + results = s.chain.BlockHeight() + + case "getblockhash": + if param, exists := reqParams.ValueAtAndType(0, "number"); exists { + results = s.chain.GetHeaderHash(param.IntVal) + } else { + err := errors.New("Unable to parse parameter in position 0, expected a number") + resultsErr = NewInvalidParamsError(err.Error(), err) + break + } + + case "getconnectioncount": + results = s.coreServer.PeerCount() + + case "getversion": + results = result.Version{ + Port: s.coreServer.ListenTCP, + Nonce: s.coreServer.ID(), + UserAgent: s.coreServer.UserAgent, + } + + case "getpeers": + peers := result.NewPeers() + for _, addr := range s.coreServer.UnconnectedPeers() { + peers.AddPeer("unconnected", addr) + } + + for _, addr := range s.coreServer.BadPeers() { + peers.AddPeer("bad", addr) + } + + for addr := range s.coreServer.Peers() { + peers.AddPeer("connected", addr.Endpoint().String()) + } + + results = peers + + case "validateaddress", "getblocksysfee", "getcontractstate", "getrawmempool", "getrawtransaction", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript", "sendrawtransaction", "getaccountstate", "getassetstate": + results = "TODO" + + default: + resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil) + } + + if resultsErr != nil { + req.WriteErrorResponse(w, resultsErr) + } + + if results != nil { + req.WriteResponse(w, results) + } +} diff --git a/pkg/util/uint160.go b/pkg/util/uint160.go index f6bd0b9a5..88f51f879 100644 --- a/pkg/util/uint160.go +++ b/pkg/util/uint160.go @@ -2,6 +2,7 @@ package util import ( "encoding/hex" + "encoding/json" "fmt" ) @@ -56,3 +57,8 @@ func (u Uint160) Equals(other Uint160) bool { } return true } + +// MarshalJSON implements the json marshaller interface. +func (u Uint160) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} diff --git a/pkg/util/uint256.go b/pkg/util/uint256.go index c98bcc658..eef76f02f 100644 --- a/pkg/util/uint256.go +++ b/pkg/util/uint256.go @@ -2,6 +2,7 @@ package util import ( "encoding/hex" + "encoding/json" "fmt" ) @@ -57,3 +58,8 @@ func (u Uint256) Equals(other Uint256) bool { func (u Uint256) String() string { return hex.EncodeToString(ArrayReverse(u.Bytes())) } + +// MarshalJSON implements the json marshaller interface. +func (u Uint256) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +}