From 47b5478cfc62ab7f2388678e13738d859e2d3835 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2017 18:39:46 -0700 Subject: [PATCH] rpcserver: implement `rescanblocks` command backported from dcrd --- rpcserver.go | 5 +- rpcserverhelp.go | 10 ++++ rpcwebsocket.go | 137 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 146 insertions(+), 6 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 9b4da54f..af077268 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -44,9 +44,9 @@ import ( // API version constants const ( - jsonrpcSemverString = "1.2.0" + jsonrpcSemverString = "1.3.0" jsonrpcSemverMajor = 1 - jsonrpcSemverMinor = 2 + jsonrpcSemverMinor = 3 jsonrpcSemverPatch = 0 ) @@ -238,6 +238,7 @@ var rpcLimited = map[string]struct{}{ "notifyreceived": {}, "notifyspent": {}, "rescan": {}, + "rescanblocks": {}, "session": {}, // Websockets AND HTTP/S commands diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 7e92e32c..c090413c 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -606,6 +606,15 @@ var helpDescsEnUS = map[string]string{ "rescan-outpoints": "List of transaction outpoints to include in the rescan", "rescan-endblock": "Hash of final block to rescan", + // RescanBlocks help. + "rescanblocks--synopsis": "Rescan blocks for transactions matching the loaded transaction filter.", + "rescanblocks-blockhashes": "List of hashes to rescan. Each next block must be a child of the previous.", + "rescanblocks--result0": "List of matching blocks.", + + // RescannedBlock help. + "rescannedblock-hash": "Hash of the matching block.", + "rescannedblock-transactions": "List of matching transactions, serialized and hex-encoded.", + // Version help. "version--synopsis": "Returns the JSON-RPC API version (semver)", "version--result0--desc": "Version objects keyed by the program or API name", @@ -680,6 +689,7 @@ var rpcResultTypes = map[string][]interface{}{ "notifyspent": nil, "stopnotifyspent": nil, "rescan": nil, + "rescanblocks": {(*[]btcjson.RescannedBlock)(nil)}, } // helpCacher provides a concurrent safe type that provides help and usage for diff --git a/rpcwebsocket.go b/rpcwebsocket.go index b56a1e14..5c892bb6 100644 --- a/rpcwebsocket.go +++ b/rpcwebsocket.go @@ -74,6 +74,7 @@ var wsHandlersBeforeInit = map[string]wsCommandHandler{ "stopnotifyspent": handleStopNotifySpent, "stopnotifyreceived": handleStopNotifyReceived, "rescan": handleRescan, + "rescanblocks": handleRescanBlocks, } // WebsocketHandler handles a new websocket client by creating a new wsClient, @@ -245,8 +246,7 @@ func (m *wsNotificationManager) NotifyMempoolTx(tx *btcutil.Tx, isNew bool) { } // wsClientFilter tracks relevant addresses for each websocket client for -// the future `rescanblocks` extension. It is modified by the `loadtxfilter` -// command. +// the `rescanblocks` extension. It is modified by the `loadtxfilter` command. // // NOTE: This extension was ported from github.com/decred/dcrd type wsClientFilter struct { @@ -759,7 +759,6 @@ func (m *wsNotificationManager) notifyFilteredBlockConnected(clients map[chan st } } for quitChan, wsc := range clients { - // Add all discovered transactions for this client. For clients // that have no new-style filter, add the empty string slice. ntfn.SubscribedTxs = subscribedTxs[quitChan] @@ -1273,7 +1272,7 @@ type wsClient struct { // filterData is the new generation transaction filter backported from // github.com/decred/dcrd for the new backported `loadtxfilter` and - // future `rescanblocks` methods. + // `rescanblocks` methods. filterData *wsClientFilter // Networking infrastructure. @@ -2119,6 +2118,136 @@ func rescanBlock(wsc *wsClient, lookups *rescanKeys, blk *btcutil.Block) { } } +// rescanBlockFilter rescans a block for any relevant transactions for the +// passed lookup keys. Any discovered transactions are returned hex encoded as +// a string slice. +// +// NOTE: This extension is ported from github.com/decred/dcrd +func rescanBlockFilter(filter *wsClientFilter, block *btcutil.Block) []string { + var transactions []string + + filter.mu.Lock() + for _, tx := range block.Transactions() { + msgTx := tx.MsgTx() + + // Keep track of whether the transaction has already been added + // to the result. It shouldn't be added twice. + added := false + + // Scan inputs if not a coinbase transaction. + if !blockchain.IsCoinBaseTx(msgTx) { + for _, input := range msgTx.TxIn { + if !filter.existsUnspentOutPoint(&input.PreviousOutPoint) { + continue + } + if !added { + transactions = append( + transactions, + txHexString(msgTx)) + added = true + } + } + } + + // Scan outputs. + for i, output := range msgTx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, + activeNetParams.Params) + if err != nil { + continue + } + for _, a := range addrs { + if !filter.existsAddress(a) { + continue + } + + op := wire.OutPoint{ + Hash: *tx.Hash(), + Index: uint32(i), + } + filter.addUnspentOutPoint(&op) + + if !added { + transactions = append( + transactions, + txHexString(msgTx)) + added = true + } + } + } + } + filter.mu.Unlock() + + return transactions +} + +// handleRescanBlocks implements the rescanblocks command extension for +// websocket connections. +// +// NOTE: This extension is ported from github.com/decred/dcrd +func handleRescanBlocks(wsc *wsClient, icmd interface{}) (interface{}, error) { + cmd, ok := icmd.(*btcjson.RescanBlocksCmd) + if !ok { + return nil, btcjson.ErrRPCInternal + } + + // Load client's transaction filter. Must exist in order to continue. + wsc.Lock() + filter := wsc.filterData + wsc.Unlock() + if filter == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Transaction filter must be loaded before rescanning", + } + } + + blockHashes := make([]*chainhash.Hash, len(cmd.BlockHashes)) + + for i := range cmd.BlockHashes { + hash, err := chainhash.NewHashFromStr(cmd.BlockHashes[i]) + if err != nil { + return nil, err + } + blockHashes[i] = hash + } + + discoveredData := make([]btcjson.RescannedBlock, 0, len(blockHashes)) + + // Iterate over each block in the request and rescan. When a block + // contains relevant transactions, add it to the response. + bc := wsc.server.server.blockManager.chain + var lastBlockHash *chainhash.Hash + for i := range blockHashes { + block, err := bc.BlockByHash(blockHashes[i]) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Failed to fetch block: " + err.Error(), + } + } + if lastBlockHash != nil && block.MsgBlock().Header.PrevBlock != *lastBlockHash { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: fmt.Sprintf("Block %v is not a child of %v", + blockHashes[i], lastBlockHash), + } + } + lastBlockHash = blockHashes[i] + + transactions := rescanBlockFilter(filter, block) + if len(transactions) != 0 { + discoveredData = append(discoveredData, btcjson.RescannedBlock{ + Hash: cmd.BlockHashes[i], + Transactions: transactions, + }) + } + } + + return &discoveredData, nil +} + // recoverFromReorg attempts to recover from a detected reorganize during a // rescan. It fetches a new range of block shas from the database and // verifies that the new range of blocks is on the same fork as a previous