From 266851e329e2089fd2e0f082ddc6002c920be879 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 29 Oct 2019 18:50:46 -0700 Subject: [PATCH] btcjson+rpcclient: support new unified softfork bitcoind format --- btcjson/chainsvrresults.go | 48 ++++++++++++++----- integration/bip0009_test.go | 2 +- rpcclient/chain.go | 75 ++++++++++++++++++++++++++---- rpcclient/chain_test.go | 92 +++++++++++++++++++++++++++++++++++++ rpcserver.go | 8 ++-- rpcserverhelp.go | 40 ++++++++++------ 6 files changed, 224 insertions(+), 41 deletions(-) create mode 100644 rpcclient/chain_test.go diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index cc9c0855..257a6c90 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -97,21 +97,45 @@ type Bip9SoftForkDescription struct { Since int32 `json:"since"` } +// SoftForks describes the current softforks enabled by the backend. Softforks +// activated through BIP9 are grouped together separate from any other softforks +// with different activation types. +type SoftForks struct { + SoftForks []*SoftForkDescription `json:"softforks"` + Bip9SoftForks map[string]*Bip9SoftForkDescription `json:"bip9_softforks"` +} + +// UnifiedSoftForks describes a softforks in a general manner, irrespective of +// its activation type. This was a format introduced by bitcoind v0.19.0 +type UnifiedSoftFork struct { + Type string `json:"type"` + BIP9SoftForkDescription *Bip9SoftForkDescription `json:"bip9"` + Height int32 `json:"height"` + Active bool `json:"active"` +} + +// UnifiedSoftForks describes the current softforks enabled the by the backend +// in a unified manner, i.e, softforks with different activation types are +// grouped together. This was a format introduced by bitcoind v0.19.0 +type UnifiedSoftForks struct { + SoftForks map[string]*UnifiedSoftFork `json:"softforks"` +} + // GetBlockChainInfoResult models the data returned from the getblockchaininfo // command. type GetBlockChainInfoResult struct { - Chain string `json:"chain"` - Blocks int32 `json:"blocks"` - Headers int32 `json:"headers"` - BestBlockHash string `json:"bestblockhash"` - Difficulty float64 `json:"difficulty"` - MedianTime int64 `json:"mediantime"` - VerificationProgress float64 `json:"verificationprogress,omitempty"` - Pruned bool `json:"pruned"` - PruneHeight int32 `json:"pruneheight,omitempty"` - ChainWork string `json:"chainwork,omitempty"` - SoftForks []*SoftForkDescription `json:"softforks"` - Bip9SoftForks map[string]*Bip9SoftForkDescription `json:"bip9_softforks"` + Chain string `json:"chain"` + Blocks int32 `json:"blocks"` + Headers int32 `json:"headers"` + BestBlockHash string `json:"bestblockhash"` + Difficulty float64 `json:"difficulty"` + MedianTime int64 `json:"mediantime"` + VerificationProgress float64 `json:"verificationprogress,omitempty"` + Pruned bool `json:"pruned"` + PruneHeight int32 `json:"pruneheight,omitempty"` + ChainWork string `json:"chainwork,omitempty"` + *SoftForks + *UnifiedSoftForks } // GetBlockTemplateResultTx models the transactions field of the diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 181c8983..df3721b1 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -102,7 +102,7 @@ func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, stat } // Ensure the key is available. - desc, ok := info.Bip9SoftForks[forkKey] + desc, ok := info.SoftForks.Bip9SoftForks[forkKey] if !ok { _, _, line, _ := runtime.Caller(1) t.Fatalf("assertion failed at line %d: softfork status for %q "+ diff --git a/rpcclient/chain.go b/rpcclient/chain.go index c2166891..996d8045 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -253,16 +253,15 @@ func (c *Client) GetDifficulty() (float64, error) { // FutureGetBlockChainInfoResult is a promise to deliver the result of a // GetBlockChainInfoAsync RPC invocation (or an applicable error). -type FutureGetBlockChainInfoResult chan *response - -// Receive waits for the response promised by the future and returns chain info -// result provided by the server. -func (r FutureGetBlockChainInfoResult) Receive() (*btcjson.GetBlockChainInfoResult, error) { - res, err := receiveFuture(r) - if err != nil { - return nil, err - } +type FutureGetBlockChainInfoResult struct { + client *Client + Response chan *response +} +// unmarshalPartialGetBlockChainInfoResult unmarshals the response into an +// instance of GetBlockChainInfoResult without populating the SoftForks and +// UnifiedSoftForks fields. +func unmarshalPartialGetBlockChainInfoResult(res []byte) (*btcjson.GetBlockChainInfoResult, error) { var chainInfo btcjson.GetBlockChainInfoResult if err := json.Unmarshal(res, &chainInfo); err != nil { return nil, err @@ -270,6 +269,59 @@ func (r FutureGetBlockChainInfoResult) Receive() (*btcjson.GetBlockChainInfoResu return &chainInfo, nil } +// unmarshalGetBlockChainInfoResultSoftForks properly unmarshals the softforks +// related fields into the GetBlockChainInfoResult instance. +func unmarshalGetBlockChainInfoResultSoftForks(chainInfo *btcjson.GetBlockChainInfoResult, + version BackendVersion, res []byte) error { + + switch version { + // Versions of bitcoind on or after v0.19.0 use the unified format. + case BitcoindPost19: + var softForks btcjson.UnifiedSoftForks + if err := json.Unmarshal(res, &softForks); err != nil { + return err + } + chainInfo.UnifiedSoftForks = &softForks + + // All other versions use the original format. + default: + var softForks btcjson.SoftForks + if err := json.Unmarshal(res, &softForks); err != nil { + return err + } + chainInfo.SoftForks = &softForks + } + + return nil +} + +// Receive waits for the response promised by the future and returns chain info +// result provided by the server. +func (r FutureGetBlockChainInfoResult) Receive() (*btcjson.GetBlockChainInfoResult, error) { + res, err := receiveFuture(r.Response) + if err != nil { + return nil, err + } + chainInfo, err := unmarshalPartialGetBlockChainInfoResult(res) + if err != nil { + return nil, err + } + + // Inspect the version to determine how we'll need to parse the + // softforks from the response. + version, err := r.client.BackendVersion() + if err != nil { + return nil, err + } + + err = unmarshalGetBlockChainInfoResultSoftForks(chainInfo, version, res) + if err != nil { + return nil, err + } + + return chainInfo, nil +} + // GetBlockChainInfoAsync returns an instance of a type that can be used to get // the result of the RPC at some future time by invoking the Receive function // on the returned instance. @@ -277,7 +329,10 @@ func (r FutureGetBlockChainInfoResult) Receive() (*btcjson.GetBlockChainInfoResu // See GetBlockChainInfo for the blocking version and more details. func (c *Client) GetBlockChainInfoAsync() FutureGetBlockChainInfoResult { cmd := btcjson.NewGetBlockChainInfoCmd() - return c.sendCmd(cmd) + return FutureGetBlockChainInfoResult{ + client: c, + Response: c.sendCmd(cmd), + } } // GetBlockChainInfo returns information related to the processing state of diff --git a/rpcclient/chain_test.go b/rpcclient/chain_test.go new file mode 100644 index 00000000..e32d547c --- /dev/null +++ b/rpcclient/chain_test.go @@ -0,0 +1,92 @@ +package rpcclient + +import "testing" + +// TestUnmarshalGetBlockChainInfoResult ensures that the SoftForks and +// UnifiedSoftForks fields of GetBlockChainInfoResult are properly unmarshaled +// when using the expected backend version. +func TestUnmarshalGetBlockChainInfoResultSoftForks(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + version BackendVersion + res []byte + compatible bool + }{ + { + name: "bitcoind < 0.19.0 with separate softforks", + version: BitcoindPre19, + res: []byte(`{"softforks": [{"version": 2}]}`), + compatible: true, + }, + { + name: "bitcoind >= 0.19.0 with separate softforks", + version: BitcoindPost19, + res: []byte(`{"softforks": [{"version": 2}]}`), + compatible: false, + }, + { + name: "bitcoind < 0.19.0 with unified softforks", + version: BitcoindPre19, + res: []byte(`{"softforks": {"segwit": {"type": "bip9"}}}`), + compatible: false, + }, + { + name: "bitcoind >= 0.19.0 with unified softforks", + version: BitcoindPost19, + res: []byte(`{"softforks": {"segwit": {"type": "bip9"}}}`), + compatible: true, + }, + } + + for _, test := range tests { + success := t.Run(test.name, func(t *testing.T) { + // We'll start by unmarshaling the JSON into a struct. + // The SoftForks and UnifiedSoftForks field should not + // be set yet, as they are unmarshaled within a + // different function. + info, err := unmarshalPartialGetBlockChainInfoResult(test.res) + if err != nil { + t.Fatal(err) + } + if info.SoftForks != nil { + t.Fatal("expected SoftForks to be empty") + } + if info.UnifiedSoftForks != nil { + t.Fatal("expected UnifiedSoftForks to be empty") + } + + // Proceed to unmarshal the softforks of the response + // with the expected version. If the version is + // incompatible with the response, then this should + // fail. + err = unmarshalGetBlockChainInfoResultSoftForks( + info, test.version, test.res, + ) + if test.compatible && err != nil { + t.Fatalf("unable to unmarshal softforks: %v", err) + } + if !test.compatible && err == nil { + t.Fatal("expected to not unmarshal softforks") + } + if !test.compatible { + return + } + + // If the version is compatible with the response, we + // should expect to see the proper softforks field set. + if test.version == BitcoindPost19 && + info.SoftForks != nil { + t.Fatal("expected SoftForks to be empty") + } + if test.version == BitcoindPre19 && + info.UnifiedSoftForks != nil { + t.Fatal("expected UnifiedSoftForks to be empty") + } + }) + if !success { + return + } + } +} diff --git a/rpcserver.go b/rpcserver.go index 097ba2ec..0a7ab8ed 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1198,14 +1198,16 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str Difficulty: getDifficultyRatio(chainSnapshot.Bits, params), MedianTime: chainSnapshot.MedianTime.Unix(), Pruned: false, - Bip9SoftForks: make(map[string]*btcjson.Bip9SoftForkDescription), + SoftForks: &btcjson.SoftForks{ + Bip9SoftForks: make(map[string]*btcjson.Bip9SoftForkDescription), + }, } // Next, populate the response with information describing the current // status of soft-forks deployed via the super-majority block // signalling mechanism. height := chainSnapshot.Height - chainInfo.SoftForks = []*btcjson.SoftForkDescription{ + chainInfo.SoftForks.SoftForks = []*btcjson.SoftForkDescription{ { ID: "bip34", Version: 2, @@ -1281,7 +1283,7 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str // Finally, populate the soft-fork description with all the // information gathered above. - chainInfo.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{ + chainInfo.SoftForks.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{ Status: strings.ToLower(statusString), Bit: deploymentDetails.BitNumber, StartTime: int64(deploymentDetails.StartTime), diff --git a/rpcserverhelp.go b/rpcserverhelp.go index e7637db6..7da266ea 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -172,21 +172,18 @@ var helpDescsEnUS = map[string]string{ "getblockchaininfo--synopsis": "Returns information about the current blockchain state and the status of any active soft-fork deployments.", // GetBlockChainInfoResult help. - "getblockchaininforesult-chain": "The name of the chain the daemon is on (testnet, mainnet, etc)", - "getblockchaininforesult-blocks": "The number of blocks in the best known chain", - "getblockchaininforesult-headers": "The number of headers that we've gathered for in the best known chain", - "getblockchaininforesult-bestblockhash": "The block hash for the latest block in the main chain", - "getblockchaininforesult-difficulty": "The current chain difficulty", - "getblockchaininforesult-mediantime": "The median time from the PoV of the best block in the chain", - "getblockchaininforesult-verificationprogress": "An estimate for how much of the best chain we've verified", - "getblockchaininforesult-pruned": "A bool that indicates if the node is pruned or not", - "getblockchaininforesult-pruneheight": "The lowest block retained in the current pruned chain", - "getblockchaininforesult-chainwork": "The total cumulative work in the best chain", - "getblockchaininforesult-softforks": "The status of the super-majority soft-forks", - "getblockchaininforesult-bip9_softforks": "JSON object describing active BIP0009 deployments", - "getblockchaininforesult-bip9_softforks--key": "bip9_softforks", - "getblockchaininforesult-bip9_softforks--value": "An object describing a particular BIP009 deployment", - "getblockchaininforesult-bip9_softforks--desc": "The status of any defined BIP0009 soft-fork deployments", + "getblockchaininforesult-chain": "The name of the chain the daemon is on (testnet, mainnet, etc)", + "getblockchaininforesult-blocks": "The number of blocks in the best known chain", + "getblockchaininforesult-headers": "The number of headers that we've gathered for in the best known chain", + "getblockchaininforesult-bestblockhash": "The block hash for the latest block in the main chain", + "getblockchaininforesult-difficulty": "The current chain difficulty", + "getblockchaininforesult-mediantime": "The median time from the PoV of the best block in the chain", + "getblockchaininforesult-verificationprogress": "An estimate for how much of the best chain we've verified", + "getblockchaininforesult-pruned": "A bool that indicates if the node is pruned or not", + "getblockchaininforesult-pruneheight": "The lowest block retained in the current pruned chain", + "getblockchaininforesult-chainwork": "The total cumulative work in the best chain", + "getblockchaininforesult-softforks": "The status of the super-majority soft-forks", + "getblockchaininforesult-unifiedsoftforks": "The status of the super-majority soft-forks used by bitcoind on or after v0.19.0", // SoftForkDescription help. "softforkdescription-reject": "The current activation status of the softfork", @@ -194,6 +191,19 @@ var helpDescsEnUS = map[string]string{ "softforkdescription-id": "The string identifier for the soft fork", "-status": "A bool which indicates if the soft fork is active", + // SoftForks help. + "softforks-softforks": "The status of the super-majority soft-forks", + "softforks-bip9_softforks": "JSON object describing active BIP0009 deployments", + "softforks-bip9_softforks--key": "bip9_softforks", + "softforks-bip9_softforks--value": "An object describing a particular BIP009 deployment", + "softforks-bip9_softforks--desc": "The status of any defined BIP0009 soft-fork deployments", + + // UnifiedSoftForks help. + "unifiedsoftforks-softforks": "The status of the super-majority soft-forks used by bitcoind on or after v0.19.0", + "unifiedsoftforks-softforks--key": "softforks", + "unifiedsoftforks-softforks--value": "An object describing an active softfork deployment used by bitcoind on or after v0.19.0", + "unifiedsoftforks-softforks--desc": "JSON object describing an active softfork deployment used by bitcoind on or after v0.19.0", + // TxRawResult help. "txrawresult-hex": "Hex-encoded transaction", "txrawresult-txid": "The hash of the transaction",