From 7c44b6472f58088c8b119ba18915e961966d42d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 16 Dec 2016 16:59:25 -0700 Subject: [PATCH] Port `getheaders` JSON-RPC command from dcrd --- btcjson/btcdextcmds.go | 22 ++++++ btcjson/btcdextcmds_test.go | 40 ++++++++++ btcjson/btcdextresults.go | 4 +- btcjson/btcdextresults_test.go | 2 +- chaincfg/chainhash/hash.go | 66 ++++++++++------- docs/json_rpc_api.md | 14 ++++ rpcserver.go | 69 +++++++++++++++++- rpcserverhelp.go | 11 ++- server.go | 129 ++++++++++++++++----------------- 9 files changed, 254 insertions(+), 103 deletions(-) diff --git a/btcjson/btcdextcmds.go b/btcjson/btcdextcmds.go index 25af8d21..963ccb3a 100644 --- a/btcjson/btcdextcmds.go +++ b/btcjson/btcdextcmds.go @@ -90,6 +90,27 @@ func NewGetCurrentNetCmd() *GetCurrentNetCmd { return &GetCurrentNetCmd{} } +// GetHeadersCmd defines the getheaders JSON-RPC command. +// +// NOTE: This is a btcsuite extension ported from +// github.com/decred/dcrd/dcrjson. +type GetHeadersCmd struct { + BlockLocators []string `json:"blocklocators"` + HashStop string `json:"hashstop"` +} + +// NewGetHeadersCmd returns a new instance which can be used to issue a +// getheaders JSON-RPC command. +// +// NOTE: This is a btcsuite extension ported from +// github.com/decred/dcrd/dcrjson. +func NewGetHeadersCmd(blockLocators []string, hashStop string) *GetHeadersCmd { + return &GetHeadersCmd{ + BlockLocators: blockLocators, + HashStop: hashStop, + } +} + // VersionCmd defines the version JSON-RPC command. // // NOTE: This is a btcsuite extension ported from @@ -112,5 +133,6 @@ func init() { MustRegisterCmd("generate", (*GenerateCmd)(nil), flags) MustRegisterCmd("getbestblock", (*GetBestBlockCmd)(nil), flags) MustRegisterCmd("getcurrentnet", (*GetCurrentNetCmd)(nil), flags) + MustRegisterCmd("getheaders", (*GetHeadersCmd)(nil), flags) MustRegisterCmd("version", (*VersionCmd)(nil), flags) } diff --git a/btcjson/btcdextcmds_test.go b/btcjson/btcdextcmds_test.go index 1d87f7c4..10e6da38 100644 --- a/btcjson/btcdextcmds_test.go +++ b/btcjson/btcdextcmds_test.go @@ -136,6 +136,46 @@ func TestBtcdExtCmds(t *testing.T) { marshalled: `{"jsonrpc":"1.0","method":"getcurrentnet","params":[],"id":1}`, unmarshalled: &btcjson.GetCurrentNetCmd{}, }, + { + name: "getheaders", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getheaders", []string{}, "") + }, + staticCmd: func() interface{} { + return btcjson.NewGetHeadersCmd( + []string{}, + "", + ) + }, + marshalled: `{"jsonrpc":"1.0","method":"getheaders","params":[[],""],"id":1}`, + unmarshalled: &btcjson.GetHeadersCmd{ + BlockLocators: []string{}, + HashStop: "", + }, + }, + { + name: "getheaders - with arguments", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("getheaders", []string{"000000000000000001f1739002418e2f9a84c47a4fd2a0eb7a787a6b7dc12f16", "0000000000000000026f4b7f56eef057b32167eb5ad9ff62006f1807b7336d10"}, "000000000000000000ba33b33e1fad70b69e234fc24414dd47113bff38f523f7") + }, + staticCmd: func() interface{} { + return btcjson.NewGetHeadersCmd( + []string{ + "000000000000000001f1739002418e2f9a84c47a4fd2a0eb7a787a6b7dc12f16", + "0000000000000000026f4b7f56eef057b32167eb5ad9ff62006f1807b7336d10", + }, + "000000000000000000ba33b33e1fad70b69e234fc24414dd47113bff38f523f7", + ) + }, + marshalled: `{"jsonrpc":"1.0","method":"getheaders","params":[["000000000000000001f1739002418e2f9a84c47a4fd2a0eb7a787a6b7dc12f16","0000000000000000026f4b7f56eef057b32167eb5ad9ff62006f1807b7336d10"],"000000000000000000ba33b33e1fad70b69e234fc24414dd47113bff38f523f7"],"id":1}`, + unmarshalled: &btcjson.GetHeadersCmd{ + BlockLocators: []string{ + "000000000000000001f1739002418e2f9a84c47a4fd2a0eb7a787a6b7dc12f16", + "0000000000000000026f4b7f56eef057b32167eb5ad9ff62006f1807b7336d10", + }, + HashStop: "000000000000000000ba33b33e1fad70b69e234fc24414dd47113bff38f523f7", + }, + }, { name: "version", newCmd: func() (interface{}, error) { diff --git a/btcjson/btcdextresults.go b/btcjson/btcdextresults.go index 86cda258..0f7bc704 100644 --- a/btcjson/btcdextresults.go +++ b/btcjson/btcdextresults.go @@ -1,5 +1,5 @@ -// Copyright (c) 2016 The btcsuite developers -// Copyright (c) 2015-2016 The Decred developers +// Copyright (c) 2016-2017 The btcsuite developers +// Copyright (c) 2015-2017 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. diff --git a/btcjson/btcdextresults_test.go b/btcjson/btcdextresults_test.go index c4d7462b..478f088c 100644 --- a/btcjson/btcdextresults_test.go +++ b/btcjson/btcdextresults_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2017 The btcsuite developers // Copyright (c) 2015-2016 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. diff --git a/chaincfg/chainhash/hash.go b/chaincfg/chainhash/hash.go index 83004d50..2b1cec02 100644 --- a/chaincfg/chainhash/hash.go +++ b/chaincfg/chainhash/hash.go @@ -84,35 +84,45 @@ func NewHash(newHash []byte) (*Hash, error) { // the hexadecimal string of a byte-reversed hash, but any missing characters // result in zero padding at the end of the Hash. func NewHashFromStr(hash string) (*Hash, error) { - // Return error if hash string is too long. - if len(hash) > MaxHashStringSize { - return nil, ErrHashStrSize - } - - // Hex decoder expects the hash to be a multiple of two. - if len(hash)%2 != 0 { - hash = "0" + hash - } - - // Convert string hash to bytes. - buf, err := hex.DecodeString(hash) + ret := new(Hash) + err := Decode(ret, hash) if err != nil { return nil, err } - - // Un-reverse the decoded bytes, copying into in leading bytes of a - // Hash. There is no need to explicitly pad the result as any - // missing (when len(buf) < HashSize) bytes from the decoded hex string - // will remain zeros at the end of the Hash. - var ret Hash - blen := len(buf) - mid := blen / 2 - if blen%2 != 0 { - mid++ - } - blen-- - for i, b := range buf[:mid] { - ret[i], ret[blen-i] = buf[blen-i], b - } - return &ret, nil + return ret, nil +} + +// Decode decodes the byte-reversed hexadecimal string encoding of a Hash to a +// destination. +func Decode(dst *Hash, src string) error { + // Return error if hash string is too long. + if len(src) > MaxHashStringSize { + return ErrHashStrSize + } + + // Hex decoder expects the hash to be a multiple of two. When not, pad + // with a leading zero. + var srcBytes []byte + if len(src)%2 == 0 { + srcBytes = []byte(src) + } else { + srcBytes = make([]byte, 1+len(src)) + srcBytes[0] = '0' + copy(srcBytes[1:], src) + } + + // Hex decode the source bytes to a temporary destination. + var reversedHash Hash + _, err := hex.Decode(reversedHash[HashSize-hex.DecodedLen(len(srcBytes)):], srcBytes) + if err != nil { + return err + } + + // Reverse copy from the temporary hash to destination. Because the + // temporary was zeroed, the written result will be correctly padded. + for i, b := range reversedHash[:HashSize/2] { + dst[i], dst[HashSize-1-i] = reversedHash[HashSize-1-i], b + } + + return nil } diff --git a/docs/json_rpc_api.md b/docs/json_rpc_api.md index 27d36caf..2e2cbd5d 100644 --- a/docs/json_rpc_api.md +++ b/docs/json_rpc_api.md @@ -584,6 +584,7 @@ The following is an overview of the RPC methods which are implemented by btcd, b |5|[node](#node)|N|Attempts to add or remove a peer. |None| |6|[generate](#generate)|N|When in simnet or regtest mode, generate a set number of blocks. |None| |7|[version](#version)|Y|Returns the JSON-RPC API version.| +|8|[getheaders](#getheaders)|Y|Returns block headers starting with the first known block hash from the request.| @@ -678,6 +679,19 @@ The following is an overview of the RPC methods which are implemented by btcd, b *** + + +| | | +|---|---| +|Method|getheaders| +|Parameters|1. Block Locators (JSON array, required)
 `[ (json array of strings)`
  `"blocklocator", (string) the known block hash`
  `...`
 `]`
2. hashstop (string) - last desired block's hash| +|Description|Returns block headers starting with the first known block hash from the request.| +|Returns|`[ (json array of strings)`
  `"blockheader",`
  `...`
`]`| +|Example Return|`[`
  `"0000002099417930b2ae09feda10e38b58c0f6bb44b4d60fa33f0e000000000000000000d53...",`
  `"000000203ba25a173bfd24d09e0c76002a910b685ca297bd09a17b020000000000000000702..."`
`]`| +[Return to Overview](#MethodOverview)
+ +*** +
### 7. Websocket Extension Methods (Websocket-specific) diff --git a/rpcserver.go b/rpcserver.go index c34af5c1..5581699a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1,5 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2016 The Decred developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2017 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -45,9 +45,9 @@ import ( // API version constants const ( - jsonrpcSemverString = "1.0.0" + jsonrpcSemverString = "1.1.0" jsonrpcSemverMajor = 1 - jsonrpcSemverMinor = 0 + jsonrpcSemverMinor = 1 jsonrpcSemverPatch = 0 ) @@ -163,6 +163,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getdifficulty": handleGetDifficulty, "getgenerate": handleGetGenerate, "gethashespersec": handleGetHashesPerSec, + "getheaders": handleGetHeaders, "getinfo": handleGetInfo, "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, @@ -270,6 +271,7 @@ var rpcLimited = map[string]struct{}{ "getblockhash": {}, "getcurrentnet": {}, "getdifficulty": {}, + "getheaders": {}, "getinfo": {}, "getnettotals": {}, "getnetworkhashps": {}, @@ -2148,6 +2150,65 @@ func handleGetHashesPerSec(s *rpcServer, cmd interface{}, closeChan <-chan struc return int64(s.server.cpuMiner.HashesPerSecond()), nil } +// handleGetHeaders implements the getheaders command. +// +// NOTE: This is a btcsuite extension ported from +// github.com/decred/dcrd. +func handleGetHeaders(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.GetHeadersCmd) + blockLocators := make([]*chainhash.Hash, len(c.BlockLocators)) + for i := range c.BlockLocators { + blockLocator, err := chainhash.NewHashFromStr(c.BlockLocators[i]) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Failed to decode block locator: " + + err.Error(), + } + } + blockLocators[i] = blockLocator + } + var hashStop chainhash.Hash + if c.HashStop != "" { + err := chainhash.Decode(&hashStop, c.HashStop) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Failed to decode hashstop: " + err.Error(), + } + } + } + blockHashes, err := s.server.locateBlocks(blockLocators, &hashStop) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Failed to fetch hashes of block " + + "headers: " + err.Error(), + } + } + blockHeaders, err := fetchHeaders(s.server.db, blockHashes) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDatabase, + Message: "Failed to fetch headers of located blocks: " + + err.Error(), + } + } + + hexBlockHeaders := make([]string, len(blockHeaders)) + var buf bytes.Buffer + for i, h := range blockHeaders { + err := h.Serialize(&buf) + if err != nil { + return nil, internalRPCError(err.Error(), + "Failed to serialize block header") + } + hexBlockHeaders[i] = hex.EncodeToString(buf.Bytes()) + buf.Reset() + } + return hexBlockHeaders, nil +} + // handleGetInfo implements the getinfo command. We only return the fields // that are not related to wallet functionality. func handleGetInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 80dfb3a5..4c136f47 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -1,5 +1,5 @@ -// Copyright (c) 2015-2016 The btcsuite developers -// Copyright (c) 2015-2016 The Decred developers +// Copyright (c) 2015-2017 The btcsuite developers +// Copyright (c) 2015-2017 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -360,6 +360,12 @@ var helpDescsEnUS = map[string]string{ "infowalletresult-relayfee": "The minimum relay fee for non-free transactions in BTC/KB", "infowalletresult-errors": "Any current errors", + // GetHeadersCmd help. + "getheaders--synopsis": "Returns block headers starting with the first known block hash from the request", + "getheaders-blocklocators": "JSON array of hex-encoded hashes of blocks. Headers are returned starting from the first known hash in this list", + "getheaders-hashstop": "Block hash to stop including block headers for; if not found, all headers to the latest known block are returned.", + "getheaders--result0": "Serialized block headers of all located blocks, limited to some arbitrary maximum number of hashes (currently 2000, which matches the wire protocol headers message, but this is not guaranteed)", + // GetInfoCmd help. "getinfo--synopsis": "Returns a JSON object containing various state info.", @@ -646,6 +652,7 @@ var rpcResultTypes = map[string][]interface{}{ "getdifficulty": {(*float64)(nil)}, "getgenerate": {(*bool)(nil)}, "gethashespersec": {(*float64)(nil)}, + "getheaders": {(*[]string)(nil)}, "getinfo": {(*btcjson.InfoChainResult)(nil)}, "getmempoolinfo": {(*btcjson.GetMempoolInfoResult)(nil)}, "getmininginfo": {(*btcjson.GetMiningInfoResult)(nil)}, diff --git a/server.go b/server.go index efa5011a..352233fb 100644 --- a/server.go +++ b/server.go @@ -1,4 +1,5 @@ -// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2017 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -640,58 +641,30 @@ func (sp *serverPeer) OnGetBlocks(_ *peer.Peer, msg *wire.MsgGetBlocks) { } } -// OnGetHeaders is invoked when a peer receives a getheaders bitcoin -// message. -func (sp *serverPeer) OnGetHeaders(_ *peer.Peer, msg *wire.MsgGetHeaders) { - // Ignore getheaders requests if not in sync. - if !sp.server.blockManager.IsCurrent() { - return - } - +// locateBlocks returns the hashes of the blocks after the first known block in +// locators, until hashStop is reached, or up to a max of +// wire.MaxBlockHeadersPerMsg block hashes. This implements the search +// algorithm used by getheaders. +func (s *server) locateBlocks(locators []*chainhash.Hash, hashStop *chainhash.Hash) ([]chainhash.Hash, error) { // Attempt to look up the height of the provided stop hash. - chain := sp.server.blockManager.chain + chain := s.blockManager.chain endIdx := int32(math.MaxInt32) - height, err := chain.BlockHeightByHash(&msg.HashStop) + height, err := chain.BlockHeightByHash(hashStop) if err == nil { endIdx = height + 1 } // There are no block locators so a specific header is being requested // as identified by the stop hash. - if len(msg.BlockLocatorHashes) == 0 { + if len(locators) == 0 { // No blocks with the stop hash were found so there is nothing // to do. Just return. This behavior mirrors the reference // implementation. if endIdx == math.MaxInt32 { - return + return nil, nil } - // Fetch the raw block header bytes from the database. - var headerBytes []byte - err := sp.server.db.View(func(dbTx database.Tx) error { - var err error - headerBytes, err = dbTx.FetchBlockHeader(&msg.HashStop) - return err - }) - if err != nil { - peerLog.Warnf("Lookup of known block hash failed: %v", - err) - return - } - - // Deserialize the block header. - var header wire.BlockHeader - err = header.Deserialize(bytes.NewReader(headerBytes)) - if err != nil { - peerLog.Warnf("Block header deserialize failed: %v", - err) - return - } - - headersMsg := wire.NewMsgHeaders() - headersMsg.AddBlockHeader(&header) - sp.QueueMessage(headersMsg, nil) - return + return []chainhash.Hash{*hashStop}, nil } // Find the most recent known block based on the block locator. @@ -700,8 +673,8 @@ func (sp *serverPeer) OnGetHeaders(_ *peer.Peer, msg *wire.MsgGetHeaders) { // over with the genesis block if unknown block locators are provided. // This mirrors the behavior in the reference implementation. startIdx := int32(1) - for _, hash := range msg.BlockLocatorHashes { - height, err := chain.BlockHeightByHash(hash) + for _, loc := range locators { + height, err := chain.BlockHeightByHash(loc) if err == nil { // Start with the next hash since we know this one. startIdx = height + 1 @@ -709,43 +682,67 @@ func (sp *serverPeer) OnGetHeaders(_ *peer.Peer, msg *wire.MsgGetHeaders) { } } - // Don't attempt to fetch more than we can put into a single message. + // Don't attempt to fetch more than we can put into a single wire + // message. if endIdx-startIdx > wire.MaxBlockHeadersPerMsg { endIdx = startIdx + wire.MaxBlockHeadersPerMsg } // Fetch the inventory from the block database. - hashList, err := chain.HeightRange(startIdx, endIdx) - if err != nil { - peerLog.Warnf("Header lookup failed: %v", err) - return - } + return chain.HeightRange(startIdx, endIdx) +} - // Generate headers message and send it. - headersMsg := wire.NewMsgHeaders() - err = sp.server.db.View(func(dbTx database.Tx) error { - for i := range hashList { - headerBytes, err := dbTx.FetchBlockHeader(&hashList[i]) - if err != nil { - return err - } - - var header wire.BlockHeader - err = header.Deserialize(bytes.NewReader(headerBytes)) - if err != nil { - return err - } - headersMsg.AddBlockHeader(&header) +// fetchHeaders fetches and decodes headers from the db for each hash in +// blockHashes. +func fetchHeaders(db database.DB, blockHashes []chainhash.Hash) ([]*wire.BlockHeader, error) { + headers := make([]*wire.BlockHeader, 0, len(blockHashes)) + err := db.View(func(dbTx database.Tx) error { + rawHeaders, err := dbTx.FetchBlockHeaders(blockHashes) + if err != nil { + return err + } + for _, headerBytes := range rawHeaders { + h := new(wire.BlockHeader) + err = h.Deserialize(bytes.NewReader(headerBytes)) + if err != nil { + return err + } + headers = append(headers, h) } - return nil }) - if err != nil { - peerLog.Warnf("Failed to build headers: %v", err) + return headers, err +} + +// OnGetHeaders is invoked when a peer receives a getheaders bitcoin +// message. +func (sp *serverPeer) OnGetHeaders(_ *peer.Peer, msg *wire.MsgGetHeaders) { + // Ignore getheaders requests if not in sync. + if !sp.server.blockManager.IsCurrent() { return } - sp.QueueMessage(headersMsg, nil) + blockHashes, err := sp.server.locateBlocks(msg.BlockLocatorHashes, + &msg.HashStop) + if err != nil { + peerLog.Errorf("OnGetHeaders: failed to fetch hashes: %v", err) + return + } + blockHeaders, err := fetchHeaders(sp.server.db, blockHashes) + if err != nil { + peerLog.Errorf("OnGetHeaders: failed to fetch block headers: "+ + "%v", err) + return + } + + if len(blockHeaders) > wire.MaxBlockHeadersPerMsg { + peerLog.Warnf("OnGetHeaders: fetched more block headers than " + + "allowed per message") + // Can still recover from this error, just slice off the extra + // headers and continue queing the message. + blockHeaders = blockHeaders[:wire.MaxBlockHeaderPayload] + } + sp.QueueMessage(&wire.MsgHeaders{Headers: blockHeaders}, nil) } // enforceNodeBloomFlag disconnects the peer if the server is not configured to