diff --git a/peer.go b/peer.go index 871440dc..adf2d25a 100644 --- a/peer.go +++ b/peer.go @@ -162,6 +162,10 @@ type peer struct { blockProcessed chan bool quit chan bool userAgent string + pingStatsMtx sync.Mutex // protects lastPing* + lastPingNonce uint64 // Set to nonce if we have a pending ping. + lastPingTime time.Time // Time we sent last ping. + lastPingMicros int64 // Time for last ping to return. } // String returns the peer's address and directionality as a human-readable @@ -921,6 +925,30 @@ func (p *peer) handlePingMsg(msg *btcwire.MsgPing) { } } +// handlePongMsg is invoked when a peer recieved a pong bitcoin message. +// recent clients (protocol version > BIP0031Version), and if we had send a ping +// previosuly we update our ping time statistics. If the client is too old or +// we had not send a ping we ignore it. +func (p *peer) handlePongMsg(msg *btcwire.MsgPong) { + p.pingStatsMtx.Lock() + defer p.pingStatsMtx.Unlock() + + // Arguably we could use a buffered channel here sending data + // in a fifo manner whenever we send a ping, or a list keeping track of + // the times of each ping. For now we just make a best effort and + // only record stats if it was for the last ping sent. Any preceding + // and overlapping pings will be ignored. It is unlikely to occur + // without large usage of the ping rpc call since we ping + // infrequently enough that if they overlap we would have timed out + // the peer. + if p.protocolVersion > btcwire.BIP0031Version && + p.lastPingNonce != 0 && msg.Nonce == p.lastPingNonce { + p.lastPingMicros = time.Now().Sub(p.lastPingTime).Nanoseconds() + p.lastPingMicros /= 1000 // convert to usec. + p.lastPingNonce = 0 + } +} + // readMessage reads the next bitcoin message from the peer with logging. func (p *peer) readMessage() (msg btcwire.Message, buf []byte, err error) { msg, buf, err = btcwire.ReadMessage(p.conn, p.protocolVersion, p.btcnet) @@ -1092,8 +1120,7 @@ out: markConnected = true case *btcwire.MsgPong: - // Don't do anything, but could try to work out network - // timing or similar. + p.handlePongMsg(msg) case *btcwire.MsgAlert: p.server.BroadcastMessage(msg, p) @@ -1324,13 +1351,20 @@ out: // specially. peerLog.Tracef("%s: recieved from queuehandler", p) reset := true - switch msg.msg.(type) { + switch m := msg.msg.(type) { case *btcwire.MsgVersion: // should get an ack case *btcwire.MsgGetAddr: // should get addresses case *btcwire.MsgPing: // expects pong + // Also set up statistics. + p.pingStatsMtx.Lock() + if p.protocolVersion > btcwire.BIP0031Version { + p.lastPingNonce = m.Nonce + p.lastPingTime = time.Now() + } + p.pingStatsMtx.Unlock() case *btcwire.MsgMemPool: // Should return an inv. case *btcwire.MsgGetData: diff --git a/rpcserver.go b/rpcserver.go index 3b094c4a..731f4cd3 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -49,28 +49,29 @@ type commandHandler func(*rpcServer, btcjson.Cmd) (interface{}, error) // a dependancy loop. var rpcHandlers map[string]commandHandler var rpcHandlersBeforeInit = map[string]commandHandler{ - "addnode": handleAddNode, - "createrawtransaction": handleCreateRawTransaction, - "debuglevel": handleDebugLevel, - "decoderawtransaction": handleDecodeRawTransaction, - "decodescript": handleDecodeScript, - "getbestblockhash": handleGetBestBlockHash, - "getblock": handleGetBlock, - "getblockcount": handleGetBlockCount, - "getblockhash": handleGetBlockHash, - "getconnectioncount": handleGetConnectionCount, - "getdifficulty": handleGetDifficulty, - "getgenerate": handleGetGenerate, - "gethashespersec": handleGetHashesPerSec, - "getpeerinfo": handleGetPeerInfo, - "getrawmempool": handleGetRawMempool, - "getrawtransaction": handleGetRawTransaction, - "help": handleHelp, - "sendrawtransaction": handleSendRawTransaction, - "setgenerate": handleSetGenerate, - "stop": handleStop, - "submitblock": handleSubmitBlock, - "verifychain": handleVerifyChain, + "addnode": handleAddNode, + "createrawtransaction": handleCreateRawTransaction, + "debuglevel": handleDebugLevel, + "decoderawtransaction": handleDecodeRawTransaction, + "decodescript": handleDecodeScript, + "getbestblockhash": handleGetBestBlockHash, + "getblock": handleGetBlock, + "getblockcount": handleGetBlockCount, + "getblockhash": handleGetBlockHash, + "getconnectioncount": handleGetConnectionCount, + "getdifficulty": handleGetDifficulty, + "getgenerate": handleGetGenerate, + "gethashespersec": handleGetHashesPerSec, + "getpeerinfo": handleGetPeerInfo, + "getrawmempool": handleGetRawMempool, + "getrawtransaction": handleGetRawTransaction, + "help": handleHelp, + "ping": handlePing, + "sendrawtransaction": handleSendRawTransaction, + "setgenerate": handleSetGenerate, + "stop": handleStop, + "submitblock": handleSubmitBlock, + "verifychain": handleVerifyChain, } func init() { @@ -126,15 +127,14 @@ var rpcAskWallet = map[string]bool{ // Commands that are temporarily unimplemented. var rpcUnimplemented = map[string]bool{ - "getaddednodeinfo": true, - "getblocktemplate": true, - "getinfo": true, - "getmininginfo": true, - "getnettotals": true, - "getnetworkhashps": true, - "getnewaddress": true, - "getwork": true, - "ping": true, + "getaddednodeinfo": true, + "getblocktemplate": true, + "getinfo": true, + "getmininginfo": true, + "getnettotals": true, + "getnetworkhashps": true, + "getnewaddress": true, + "getwork": true, } // rpcServer holds the items the rpc server may need to access (config, @@ -1037,7 +1037,7 @@ NOTE: btcd does not mine so this will always return false. The call is provided for compatibility only.`, "getpeerinfo": ` NOTE: btcd does not currently implement all fields. the "bytessent", -"bytesrecv", "pingtime", "pingwait" and "syncnode" fields are not yet +"bytesrecv" and "syncnode" fields are not yet implemented.`, "sendrawtransaction": ` NOTE: btcd does not currently support the "allowhighfees" parameter.`, @@ -1085,13 +1085,26 @@ func handleHelp(s *rpcServer, cmd btcjson.Cmd) (interface{}, error) { // search the main list of hanlders since we do not wish to provide help // for commands that are unimplemented or relate to wallet // functionality. - if _, ok := rpcHandlers[help.Command]; !ok { + if _, ok := rpcHandlers[help.Command]; !ok { return "", fmt.Errorf("help: unknown command: %s", help.Command) } return getHelpText(help.Command) } +// handlePing implements the ping command. +func handlePing(s *rpcServer, cmd btcjson.Cmd) (interface{}, error) { + // Ask server to ping \o_ + nonce, err := btcwire.RandomUint64() + if err != nil { + return nil, fmt.Errorf("Not sending ping - can not generate "+ + "nonce: %v", err) + } + s.server.BroadcastMessage(btcwire.NewMsgPing(nonce)) + + return nil, nil +} + // handleSendRawTransaction implements the sendrawtransaction command. func handleSendRawTransaction(s *rpcServer, cmd btcjson.Cmd) (interface{}, error) { c := cmd.(*btcjson.SendRawTransactionCmd) diff --git a/server.go b/server.go index ade3cb09..599b2320 100644 --- a/server.go +++ b/server.go @@ -266,6 +266,8 @@ type PeerInfo struct { LastRecv int64 `json:"lastrecv"` BytesSent int `json:"bytessent"` BytesRecv int `json:"bytesrecv"` + PingTime int64 `json:"pingtime"` + PingWait int64 `json:"pingwait,omitempty"` ConnTime int64 `json:"conntime"` Version uint32 `json:"version"` SubVer string `json:"subver"` @@ -333,6 +335,14 @@ func (s *server) handleQuery(querymsg interface{}, state *peerState) { BanScore: 0, SyncNode: false, // TODO(oga) for now. bm knows this. } + p.pingStatsMtx.Lock() + info.PingTime = p.lastPingMicros + if p.lastPingNonce != 0 { + wait := time.Now().Sub(p.lastPingTime).Nanoseconds() + // We actually want microseconds. + info.PingWait = wait / 1000 + } + p.pingStatsMtx.Unlock() infos = append(infos, info) }) msg.reply <- infos diff --git a/util/btcctl/btcctl.go b/util/btcctl/btcctl.go index 7fb82018..d73c56cf 100644 --- a/util/btcctl/btcctl.go +++ b/util/btcctl/btcctl.go @@ -64,9 +64,10 @@ var commandHandlers = map[string]*handlerData{ "getpeerinfo": &handlerData{0, 0, displayJSONDump, nil, makeGetPeerInfo, ""}, "getrawmempool": &handlerData{0, 1, displayJSONDump, []conversionHandler{toBool}, makeGetRawMempool, "[verbose=false]"}, "getrawtransaction": &handlerData{1, 1, displayJSONDump, []conversionHandler{nil, toBool}, makeGetRawTransaction, " [verbose=false]"}, - "help": &handlerData{0, 1, displayGeneric, nil, makeHelp, "[commandName]"}, + "help": &handlerData{0, 1, displayGeneric, nil, makeHelp, "[commandName]"}, "importprivkey": &handlerData{1, 2, displayGeneric, []conversionHandler{nil, nil, toBool}, makeImportPrivKey, " [label] [rescan=true]"}, "listtransactions": &handlerData{0, 3, displayJSONDump, []conversionHandler{nil, toInt, toInt}, makeListTransactions, "[account] [count=10] [from=0]"}, + "ping": &handlerData{0, 0, displayGeneric, nil, makePing, ""}, "verifychain": &handlerData{0, 2, displayJSONDump, []conversionHandler{toInt, toInt}, makeVerifyChain, "[level] [numblocks]"}, "sendrawtransaction": &handlerData{1, 0, displayGeneric, nil, makeSendRawTransaction, ""}, "stop": &handlerData{0, 0, displayGeneric, nil, makeStop, ""}, @@ -353,6 +354,11 @@ func makeListTransactions(args []interface{}) (btcjson.Cmd, error) { return btcjson.NewListTransactionsCmd("btcctl", optargs...) } +// makePing generates the cmd structure for ping commands. +func makePing(args []interface{}) (btcjson.Cmd, error) { + return btcjson.NewPingCmd("btcctl") +} + // makeSendRawTransaction generates the cmd structure for sendrawtransaction // commands. func makeSendRawTransaction(args []interface{}) (btcjson.Cmd, error) {