rpcserver: add GetBlockStats
This commit is contained in:
parent
c5193e74ac
commit
5acfa4c81b
4 changed files with 735 additions and 29 deletions
|
@ -35,35 +35,37 @@ type GetBlockHeaderVerboseResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockStatsResult models the data from the getblockstats command.
|
// GetBlockStatsResult models the data from the getblockstats command.
|
||||||
|
// Pointers are used instead of values to allow for optional fields.
|
||||||
type GetBlockStatsResult struct {
|
type GetBlockStatsResult struct {
|
||||||
AverageFee int64 `json:"avgfee"`
|
AverageFee *int64 `json:"avgfee,omitempty"`
|
||||||
AverageFeeRate int64 `json:"avgfeerate"`
|
AverageFeeRate *int64 `json:"avgfeerate,omitempty"`
|
||||||
AverageTxSize int64 `json:"avgtxsize"`
|
AverageTxSize *int64 `json:"avgtxsize,omitempty"`
|
||||||
FeeratePercentiles []int64 `json:"feerate_percentiles"`
|
FeeratePercentiles *[]int64 `json:"feerate_percentiles,omitempty"`
|
||||||
Hash string `json:"blockhash"`
|
Hash *string `json:"blockhash,omitempty"`
|
||||||
Height int64 `json:"height"`
|
Height *int64 `json:"height,omitempty"`
|
||||||
Ins int64 `json:"ins"`
|
Ins *int64 `json:"ins,omitempty"`
|
||||||
MaxFee int64 `json:"maxfee"`
|
MaxFee *int64 `json:"maxfee,omitempty"`
|
||||||
MaxFeeRate int64 `json:"maxfeerate"`
|
MaxFeeRate *int64 `json:"maxfeerate,omitempty"`
|
||||||
MaxTxSize int64 `json:"maxtxsize"`
|
MaxTxSize *int64 `json:"maxtxsize,omitempty"`
|
||||||
MedianFee int64 `json:"medianfee"`
|
MedianFee *int64 `json:"medianfee,omitempty"`
|
||||||
MedianTime int64 `json:"mediantime"`
|
MedianTime *int64 `json:"mediantime,omitempty"`
|
||||||
MedianTxSize int64 `json:"mediantxsize"`
|
MedianTxSize *int64 `json:"mediantxsize,omitempty"`
|
||||||
MinFee int64 `json:"minfee"`
|
MinFee *int64 `json:"minfee,omitempty"`
|
||||||
MinFeeRate int64 `json:"minfeerate"`
|
MinFeeRate *int64 `json:"minfeerate,omitempty"`
|
||||||
MinTxSize int64 `json:"mintxsize"`
|
MinTxSize *int64 `json:"mintxsize,omitempty"`
|
||||||
Outs int64 `json:"outs"`
|
Outs *int64 `json:"outs,omitempty"`
|
||||||
SegWitTotalSize int64 `json:"swtotal_size"`
|
SegWitTotalSize *int64 `json:"swtotal_size,omitempty"`
|
||||||
SegWitTotalWeight int64 `json:"swtotal_weight"`
|
SegWitTotalWeight *int64 `json:"swtotal_weight,omitempty"`
|
||||||
SegWitTxs int64 `json:"swtxs"`
|
SegWitTxs *int64 `json:"swtxs,omitempty"`
|
||||||
Subsidy int64 `json:"subsidy"`
|
Subsidy *int64 `json:"subsidy,omitempty"`
|
||||||
Time int64 `json:"time"`
|
Time *int64 `json:"time,omitempty"`
|
||||||
TotalOut int64 `json:"total_out"`
|
TotalOut *int64 `json:"total_out,omitempty"`
|
||||||
TotalSize int64 `json:"total_size"`
|
TotalSize *int64 `json:"total_size,omitempty"`
|
||||||
TotalWeight int64 `json:"total_weight"`
|
TotalWeight *int64 `json:"total_weight,omitempty"`
|
||||||
Txs int64 `json:"txs"`
|
TotalFee *int64 `json:"totalfee,omitempty"`
|
||||||
UTXOIncrease int64 `json:"utxo_increase"`
|
Txs *int64 `json:"txs,omitempty"`
|
||||||
UTXOSizeIncrease int64 `json:"utxo_size_inc"`
|
UTXOIncrease *int64 `json:"utxo_increase,omitempty"`
|
||||||
|
UTXOSizeIncrease *int64 `json:"utxo_size_inc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetBlockVerboseResultBase struct {
|
type GetBlockVerboseResultBase struct {
|
||||||
|
|
|
@ -13,7 +13,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lbryio/lbcd/chaincfg"
|
"github.com/lbryio/lbcd/chaincfg"
|
||||||
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
||||||
|
@ -133,13 +135,278 @@ func testBulkClient(r *rpctest.Harness, t *testing.T) {
|
||||||
t.Fatalf("expected hash %s to be in generated hash list", blockHash)
|
t.Fatalf("expected hash %s to be in generated hash list", blockHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetBlockStats(r *rpctest.Harness, t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
baseFeeRate := int64(10)
|
||||||
|
txValue := int64(50000000)
|
||||||
|
txQuantity := 10
|
||||||
|
txs := make([]*btcutil.Tx, txQuantity)
|
||||||
|
fees := make([]int64, txQuantity)
|
||||||
|
sizes := make([]int64, txQuantity)
|
||||||
|
feeRates := make([]int64, txQuantity)
|
||||||
|
var outputCount int
|
||||||
|
|
||||||
|
// Generate test sample.
|
||||||
|
for i := 0; i < txQuantity; i++ {
|
||||||
|
address, err := r.NewAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to generate address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := txscript.PayToAddrScript(address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to generate PKScript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This feerate is not the actual feerate. See comment below.
|
||||||
|
feeRate := baseFeeRate * int64(i)
|
||||||
|
|
||||||
|
tx, err := r.CreateTransaction([]*wire.TxOut{wire.NewTxOut(txValue, pkScript)}, btcutil.Amount(feeRate), true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to generate segwit transaction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txs[i] = btcutil.NewTx(tx)
|
||||||
|
sizes[i] = int64(tx.SerializeSize())
|
||||||
|
|
||||||
|
// memWallet.fundTx makes some assumptions when calculating fees.
|
||||||
|
// For instance, it assumes the signature script has exactly 108 bytes
|
||||||
|
// and it does not account for the size of the change output.
|
||||||
|
// This needs to be taken into account when getting the true feerate.
|
||||||
|
scriptSigOffset := 108 - len(tx.TxIn[0].SignatureScript)
|
||||||
|
changeOutputSize := tx.TxOut[len(tx.TxOut)-1].SerializeSize()
|
||||||
|
fees[i] = (sizes[i] + int64(scriptSigOffset) - int64(changeOutputSize)) * feeRate
|
||||||
|
feeRates[i] = fees[i] / sizes[i]
|
||||||
|
|
||||||
|
outputCount += len(tx.TxOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := func(slice []int64) (int64, int64, int64, int64, int64) {
|
||||||
|
var total, average, min, max, median int64
|
||||||
|
min = slice[0]
|
||||||
|
length := len(slice)
|
||||||
|
for _, item := range slice {
|
||||||
|
if min > item {
|
||||||
|
min = item
|
||||||
|
}
|
||||||
|
if max < item {
|
||||||
|
max = item
|
||||||
|
}
|
||||||
|
total += item
|
||||||
|
}
|
||||||
|
average = total / int64(length)
|
||||||
|
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
|
||||||
|
if length == 0 {
|
||||||
|
median = 0
|
||||||
|
} else if length%2 == 0 {
|
||||||
|
median = (slice[length/2-1] + slice[length/2]) / 2
|
||||||
|
} else {
|
||||||
|
median = slice[length/2]
|
||||||
|
}
|
||||||
|
return total, average, min, max, median
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFee, avgFee, minFee, maxFee, medianFee := stats(fees)
|
||||||
|
totalSize, avgSize, minSize, maxSize, medianSize := stats(sizes)
|
||||||
|
_, avgFeeRate, minFeeRate, maxFeeRate, _ := stats(feeRates)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
txs []*btcutil.Tx
|
||||||
|
stats []string
|
||||||
|
expectedResults map[string]interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty block",
|
||||||
|
txs: []*btcutil.Tx{},
|
||||||
|
stats: []string{},
|
||||||
|
expectedResults: map[string]interface{}{
|
||||||
|
"avgfee": int64(0),
|
||||||
|
"avgfeerate": int64(0),
|
||||||
|
"avgtxsize": int64(0),
|
||||||
|
"feerate_percentiles": []int64{0, 0, 0, 0, 0},
|
||||||
|
"ins": int64(0),
|
||||||
|
"maxfee": int64(0),
|
||||||
|
"maxfeerate": int64(0),
|
||||||
|
"maxtxsize": int64(0),
|
||||||
|
"medianfee": int64(0),
|
||||||
|
"mediantxsize": int64(0),
|
||||||
|
"minfee": int64(0),
|
||||||
|
"mintxsize": int64(0),
|
||||||
|
"outs": int64(1),
|
||||||
|
"swtotal_size": int64(0),
|
||||||
|
"swtotal_weight": int64(0),
|
||||||
|
"swtxs": int64(0),
|
||||||
|
"total_out": int64(0),
|
||||||
|
"total_size": int64(0),
|
||||||
|
"total_weight": int64(0),
|
||||||
|
"txs": int64(1),
|
||||||
|
"utxo_increase": int64(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "block with 10 transactions + coinbase",
|
||||||
|
txs: txs,
|
||||||
|
stats: []string{"avgfee", "avgfeerate", "avgtxsize", "feerate_percentiles",
|
||||||
|
"ins", "maxfee", "maxfeerate", "maxtxsize", "medianfee", "mediantxsize",
|
||||||
|
"minfee", "minfeerate", "mintxsize", "outs", "subsidy", "swtxs",
|
||||||
|
"total_size", "total_weight", "totalfee", "txs", "utxo_increase"},
|
||||||
|
expectedResults: map[string]interface{}{
|
||||||
|
"avgfee": avgFee,
|
||||||
|
"avgfeerate": avgFeeRate,
|
||||||
|
"avgtxsize": avgSize,
|
||||||
|
"feerate_percentiles": []int64{feeRates[0], feeRates[2],
|
||||||
|
feeRates[4], feeRates[7], feeRates[8]},
|
||||||
|
"ins": int64(txQuantity),
|
||||||
|
"maxfee": maxFee,
|
||||||
|
"maxfeerate": maxFeeRate,
|
||||||
|
"maxtxsize": maxSize,
|
||||||
|
"medianfee": medianFee,
|
||||||
|
"mediantxsize": medianSize,
|
||||||
|
"minfee": minFee,
|
||||||
|
"minfeerate": minFeeRate,
|
||||||
|
"mintxsize": minSize,
|
||||||
|
"outs": int64(outputCount + 1), // Coinbase output also counts.
|
||||||
|
"subsidy": int64(5000000000),
|
||||||
|
"swtotal_weight": nil, // This stat was not selected, so it should be nil.
|
||||||
|
"swtxs": int64(0),
|
||||||
|
"total_size": totalSize,
|
||||||
|
"total_weight": totalSize * 4,
|
||||||
|
"totalfee": totalFee,
|
||||||
|
"txs": int64(txQuantity + 1), // Coinbase transaction also counts.
|
||||||
|
"utxo_increase": int64(outputCount + 1 - txQuantity),
|
||||||
|
"utxo_size_inc": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
// Submit a new block with the provided transactions.
|
||||||
|
block, err := r.GenerateAndSubmitBlock(test.txs, -1, time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to generate block: %v from test %s", err, test.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockStats, err := r.Node.GetBlockStats(block.Hash(), &test.stats)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Call to `getblockstats` on test %s failed: %v", test.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockStats.Height != (*int64)(nil) && *blockStats.Height != int64(block.Height()) {
|
||||||
|
t.Fatalf("Unexpected result in test %s, stat: %v, expected: %v, got: %v", test.name, "height", block.Height(), *blockStats.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
for stat, value := range test.expectedResults {
|
||||||
|
var result interface{}
|
||||||
|
switch stat {
|
||||||
|
case "avgfee":
|
||||||
|
result = blockStats.AverageFee
|
||||||
|
case "avgfeerate":
|
||||||
|
result = blockStats.AverageFeeRate
|
||||||
|
case "avgtxsize":
|
||||||
|
result = blockStats.AverageTxSize
|
||||||
|
case "feerate_percentiles":
|
||||||
|
result = blockStats.FeeratePercentiles
|
||||||
|
case "blockhash":
|
||||||
|
result = blockStats.Hash
|
||||||
|
case "height":
|
||||||
|
result = blockStats.Height
|
||||||
|
case "ins":
|
||||||
|
result = blockStats.Ins
|
||||||
|
case "maxfee":
|
||||||
|
result = blockStats.MaxFee
|
||||||
|
case "maxfeerate":
|
||||||
|
result = blockStats.MaxFeeRate
|
||||||
|
case "maxtxsize":
|
||||||
|
result = blockStats.MaxTxSize
|
||||||
|
case "medianfee":
|
||||||
|
result = blockStats.MedianFee
|
||||||
|
case "mediantime":
|
||||||
|
result = blockStats.MedianTime
|
||||||
|
case "mediantxsize":
|
||||||
|
result = blockStats.MedianTxSize
|
||||||
|
case "minfee":
|
||||||
|
result = blockStats.MinFee
|
||||||
|
case "minfeerate":
|
||||||
|
result = blockStats.MinFeeRate
|
||||||
|
case "mintxsize":
|
||||||
|
result = blockStats.MinTxSize
|
||||||
|
case "outs":
|
||||||
|
result = blockStats.Outs
|
||||||
|
case "swtotal_size":
|
||||||
|
result = blockStats.SegWitTotalSize
|
||||||
|
case "swtotal_weight":
|
||||||
|
result = blockStats.SegWitTotalWeight
|
||||||
|
case "swtxs":
|
||||||
|
result = blockStats.SegWitTxs
|
||||||
|
case "subsidy":
|
||||||
|
result = blockStats.Subsidy
|
||||||
|
case "time":
|
||||||
|
result = blockStats.Time
|
||||||
|
case "total_out":
|
||||||
|
result = blockStats.TotalOut
|
||||||
|
case "total_size":
|
||||||
|
result = blockStats.TotalSize
|
||||||
|
case "total_weight":
|
||||||
|
result = blockStats.TotalWeight
|
||||||
|
case "totalfee":
|
||||||
|
result = blockStats.TotalFee
|
||||||
|
case "txs":
|
||||||
|
result = blockStats.Txs
|
||||||
|
case "utxo_increase":
|
||||||
|
result = blockStats.UTXOIncrease
|
||||||
|
case "utxo_size_inc":
|
||||||
|
result = blockStats.UTXOSizeIncrease
|
||||||
|
}
|
||||||
|
|
||||||
|
var equality bool
|
||||||
|
|
||||||
|
// Check for nil equality.
|
||||||
|
if value == nil && result == (*int64)(nil) {
|
||||||
|
equality = true
|
||||||
|
break
|
||||||
|
} else if result == nil || value == nil {
|
||||||
|
equality = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultValue interface{}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int64:
|
||||||
|
resultValue = *result.(*int64)
|
||||||
|
equality = v == resultValue
|
||||||
|
case string:
|
||||||
|
resultValue = *result.(*string)
|
||||||
|
equality = v == resultValue
|
||||||
|
case []int64:
|
||||||
|
resultValue = *result.(*[]int64)
|
||||||
|
resultSlice := resultValue.([]int64)
|
||||||
|
equality = true
|
||||||
|
for i, item := range resultSlice {
|
||||||
|
if item != v[i] {
|
||||||
|
equality = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !equality {
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("Unexpected result in test %s, stat: %v, expected: %v, got: %v", test.name, stat, value, resultValue)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Unexpected result in test %s, stat: %v, expected: %v, got: %v", test.name, stat, value, "<nil>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rpcTestCases = []rpctest.HarnessTestCase{
|
var rpcTestCases = []rpctest.HarnessTestCase{
|
||||||
testGetBestBlock,
|
testGetBestBlock,
|
||||||
testGetBlockCount,
|
testGetBlockCount,
|
||||||
testGetBlockHash,
|
testGetBlockHash,
|
||||||
|
testGetBlockStats,
|
||||||
testBulkClient,
|
testBulkClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +418,8 @@ func TestMain(m *testing.M) {
|
||||||
// In order to properly test scenarios on as if we were on mainnet,
|
// In order to properly test scenarios on as if we were on mainnet,
|
||||||
// ensure that non-standard transactions aren't accepted into the
|
// ensure that non-standard transactions aren't accepted into the
|
||||||
// mempool or relayed.
|
// mempool or relayed.
|
||||||
btcdCfg := []string{"--rejectnonstd"}
|
// Enable transaction index to be able to fully test GetBlockStats
|
||||||
|
btcdCfg := []string{"--rejectnonstd", "--txindex"}
|
||||||
primaryHarness, err = rpctest.New(
|
primaryHarness, err = rpctest.New(
|
||||||
&chaincfg.SimNetParams, nil, btcdCfg, "",
|
&chaincfg.SimNetParams, nil, btcdCfg, "",
|
||||||
)
|
)
|
||||||
|
|
398
rpcserver.go
398
rpcserver.go
|
@ -21,6 +21,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -151,6 +152,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{
|
||||||
"getblockcount": handleGetBlockCount,
|
"getblockcount": handleGetBlockCount,
|
||||||
"getblockhash": handleGetBlockHash,
|
"getblockhash": handleGetBlockHash,
|
||||||
"getblockheader": handleGetBlockHeader,
|
"getblockheader": handleGetBlockHeader,
|
||||||
|
"getblockstats": handleGetBlockStats,
|
||||||
"getblocktemplate": handleGetBlockTemplate,
|
"getblocktemplate": handleGetBlockTemplate,
|
||||||
"getcfilter": handleGetCFilter,
|
"getcfilter": handleGetCFilter,
|
||||||
"getcfilterheader": handleGetCFilterHeader,
|
"getcfilterheader": handleGetCFilterHeader,
|
||||||
|
@ -1578,6 +1580,402 @@ func handleGetChainTips(s *rpcServer, cmd interface{}, closeChan <-chan struct{}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetBlockStats implements the getblockstats command.
|
||||||
|
func handleGetBlockStats(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {
|
||||||
|
c := cmd.(*btcjson.GetBlockStatsCmd)
|
||||||
|
|
||||||
|
// Check whether a block height or hash was provided.
|
||||||
|
blockHeight, ok := c.HashOrHeight.Value.(int)
|
||||||
|
var hash *chainhash.Hash
|
||||||
|
var err error
|
||||||
|
if ok {
|
||||||
|
// Block height was provided.
|
||||||
|
hash, err = s.cfg.Chain.BlockHashByHeight(int32(blockHeight))
|
||||||
|
if err != nil {
|
||||||
|
return nil, &btcjson.RPCError{
|
||||||
|
Code: btcjson.ErrRPCOutOfRange,
|
||||||
|
Message: "Block number out of range",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Block hash was provided.
|
||||||
|
hashString := c.HashOrHeight.Value.(string)
|
||||||
|
hash, err = chainhash.NewHashFromStr(hashString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, rpcDecodeHexError(hashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the block height from chain.
|
||||||
|
blockHeightByHash, err := s.cfg.Chain.BlockHeightByHash(hash)
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to obtain block height"
|
||||||
|
return nil, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
blockHeight = int(blockHeightByHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load block bytes from the database.
|
||||||
|
var blkBytes []byte
|
||||||
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
||||||
|
var err error
|
||||||
|
blkBytes, err = dbTx.FetchBlock(hash)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, &btcjson.RPCError{
|
||||||
|
Code: btcjson.ErrRPCBlockNotFound,
|
||||||
|
Message: "Block not found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the block.
|
||||||
|
blk, err := btcutil.NewBlockFromBytes(blkBytes)
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to deserialize block"
|
||||||
|
return nil, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedStats := c.Stats
|
||||||
|
|
||||||
|
// Create a set of selected stats to facilitate queries.
|
||||||
|
statsSet := make(map[string]bool)
|
||||||
|
for _, value := range *selectedStats {
|
||||||
|
statsSet[value] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all stats if an empty array was provided.
|
||||||
|
allStats := len(*selectedStats) == 0
|
||||||
|
calcFees := statsSet["avgfee"] || statsSet["avgfeerate"] || statsSet["maxfee"] || statsSet["maxfeerate"] ||
|
||||||
|
statsSet["medianfee"] || statsSet["totalfee"] || statsSet["feerate_percentiles"]
|
||||||
|
|
||||||
|
if calcFees && s.cfg.TxIndex == nil {
|
||||||
|
return nil, &btcjson.RPCError{
|
||||||
|
Code: btcjson.ErrRPCNoTxInfo,
|
||||||
|
Message: "The transaction index must be " +
|
||||||
|
"enabled to obtain fee statistics " +
|
||||||
|
"(specify --txindex)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txs := blk.Transactions()
|
||||||
|
txCount := len(txs)
|
||||||
|
var inputCount, outputCount int
|
||||||
|
var totalOutputValue int64
|
||||||
|
|
||||||
|
// Create a map of transaction statistics.
|
||||||
|
txStats := make([]map[string]interface{}, txCount)
|
||||||
|
for i, tx := range txs {
|
||||||
|
size := tx.MsgTx().SerializeSize()
|
||||||
|
witnessSize := size - tx.MsgTx().SerializeSizeStripped()
|
||||||
|
weight := int64(tx.MsgTx().SerializeSizeStripped()*4 + witnessSize)
|
||||||
|
|
||||||
|
var fee, feeRate int64
|
||||||
|
if (calcFees || allStats) && s.cfg.TxIndex != nil && !blockchain.IsCoinBaseTx(tx.MsgTx()) {
|
||||||
|
fee, err = calculateFee(tx, s.cfg.TxIndex, s.cfg.DB)
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to calculate fees"
|
||||||
|
return nil, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
if weight != 0 {
|
||||||
|
feeRate = fee * 4 / weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segwit := tx.HasWitness()
|
||||||
|
txStats[i] = map[string]interface{}{"tx": tx, "fee": fee, "size": int64(size),
|
||||||
|
"feeRate": feeRate, "weight": weight, "segwit": segwit}
|
||||||
|
inputCount += len(tx.MsgTx().TxIn)
|
||||||
|
outputCount += len(tx.MsgTx().TxOut)
|
||||||
|
|
||||||
|
// Coinbase is excluded from the total output.
|
||||||
|
if !blockchain.IsCoinBase(tx) {
|
||||||
|
for _, txOut := range tx.MsgTx().TxOut {
|
||||||
|
totalOutputValue += txOut.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalFees, minFee, maxFee, minFeeRate, maxFeeRate, segwitCount,
|
||||||
|
segwitWeight, totalWeight, totalSize, minSize, maxSize, segwitSize int64
|
||||||
|
if txCount > 1 {
|
||||||
|
minFee = txStats[1]["fee"].(int64)
|
||||||
|
minFeeRate = txStats[1]["feeRate"].(int64)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(txStats); i++ {
|
||||||
|
var fee, feeRate int64
|
||||||
|
tx := txStats[i]["tx"].(*btcutil.Tx)
|
||||||
|
if !blockchain.IsCoinBaseTx(tx.MsgTx()) {
|
||||||
|
// Fee statistics.
|
||||||
|
fee = txStats[i]["fee"].(int64)
|
||||||
|
feeRate = txStats[i]["feeRate"].(int64)
|
||||||
|
if minFee > fee {
|
||||||
|
minFee = fee
|
||||||
|
}
|
||||||
|
if maxFee < fee {
|
||||||
|
maxFee = fee
|
||||||
|
}
|
||||||
|
if minFeeRate > feeRate {
|
||||||
|
minFeeRate = feeRate
|
||||||
|
}
|
||||||
|
if maxFeeRate < feeRate {
|
||||||
|
maxFeeRate = feeRate
|
||||||
|
}
|
||||||
|
totalFees += txStats[i]["fee"].(int64)
|
||||||
|
|
||||||
|
// Segwit statistics.
|
||||||
|
if txStats[i]["segwit"].(bool) {
|
||||||
|
segwitCount++
|
||||||
|
segwitSize += txStats[i]["size"].(int64)
|
||||||
|
segwitWeight += txStats[i]["weight"].(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size statistics.
|
||||||
|
size := txStats[i]["size"].(int64)
|
||||||
|
if minSize == 0 {
|
||||||
|
minSize = size
|
||||||
|
}
|
||||||
|
if maxSize < size {
|
||||||
|
maxSize = size
|
||||||
|
} else if minSize > size {
|
||||||
|
minSize = size
|
||||||
|
}
|
||||||
|
totalSize += txStats[i]["size"].(int64)
|
||||||
|
|
||||||
|
totalWeight += txStats[i]["weight"].(int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var avgFee, avgFeeRate, avgSize int64
|
||||||
|
if txCount > 1 {
|
||||||
|
avgFee = totalFees / int64(txCount-1)
|
||||||
|
}
|
||||||
|
if totalWeight != 0 {
|
||||||
|
avgFeeRate = totalFees * 4 / totalWeight
|
||||||
|
}
|
||||||
|
if txCount > 1 {
|
||||||
|
avgSize = totalSize / int64(txCount-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
subsidy := blockchain.CalcBlockSubsidy(int32(blockHeight), s.cfg.ChainParams)
|
||||||
|
|
||||||
|
medianStat := func(stat string) int64 {
|
||||||
|
size := len(txStats) - 1
|
||||||
|
if size == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
statArray := make([]int64, size)
|
||||||
|
// Start with the second element to ignore entry associated with coinbase.
|
||||||
|
for i, stats := range txStats[1:] {
|
||||||
|
statArray[i] = stats[stat].(int64)
|
||||||
|
}
|
||||||
|
sort.Slice(statArray, func(i, j int) bool {
|
||||||
|
return statArray[i] < statArray[j]
|
||||||
|
})
|
||||||
|
if size%2 == 0 {
|
||||||
|
return (statArray[size/2-1] + statArray[size/2]) / 2
|
||||||
|
}
|
||||||
|
return statArray[size/2]
|
||||||
|
}
|
||||||
|
|
||||||
|
var medianFee int64
|
||||||
|
if totalFees > 0 {
|
||||||
|
medianFee = medianStat("fee")
|
||||||
|
} else {
|
||||||
|
medianFee = 0
|
||||||
|
}
|
||||||
|
medianSize := medianStat("size")
|
||||||
|
|
||||||
|
// Calculate feerate percentiles.
|
||||||
|
var feeratePercentiles []int64
|
||||||
|
if allStats || calcFees {
|
||||||
|
|
||||||
|
// Sort by feerate.
|
||||||
|
sort.Slice(txStats, func(i, j int) bool {
|
||||||
|
return txStats[i]["feeRate"].(int64) < txStats[j]["feeRate"].(int64)
|
||||||
|
})
|
||||||
|
totalWeight := float64(totalWeight)
|
||||||
|
|
||||||
|
// Find 10th, 25th, 50th, 75th and 90th percentile weight units.
|
||||||
|
weights := []float64{
|
||||||
|
totalWeight / 10, totalWeight / 4, totalWeight / 2,
|
||||||
|
(totalWeight * 3) / 4, (totalWeight * 9) / 10}
|
||||||
|
var cumulativeWeight int64
|
||||||
|
feeratePercentiles = make([]int64, len(weights))
|
||||||
|
nextPercentileIndex := 0
|
||||||
|
for i := 0; i < len(txStats); i++ {
|
||||||
|
cumulativeWeight += txStats[i]["weight"].(int64)
|
||||||
|
for nextPercentileIndex < len(weights) && float64(cumulativeWeight) >= weights[nextPercentileIndex] {
|
||||||
|
feeratePercentiles[nextPercentileIndex] = txStats[i]["feeRate"].(int64)
|
||||||
|
nextPercentileIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill any remaining percentiles with the last value.
|
||||||
|
for i := nextPercentileIndex; i < len(weights); i++ {
|
||||||
|
feeratePercentiles[i] = txStats[len(txStats)-1]["feeRate"].(int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockHash string
|
||||||
|
if allStats || statsSet["blockhash"] {
|
||||||
|
blockHash = blk.Hash().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
medianTime, err := medianBlockTime(blk.Hash(), s.cfg.Chain)
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to obtain block median time"
|
||||||
|
return nil, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultMap := map[string]int64{
|
||||||
|
"avgfee": avgFee,
|
||||||
|
"avgfeerate": avgFeeRate,
|
||||||
|
"avgtxsize": avgSize,
|
||||||
|
"height": int64(blockHeight),
|
||||||
|
"ins": int64(inputCount - 1), // Coinbase input is not included.
|
||||||
|
"maxfee": maxFee,
|
||||||
|
"maxfeerate": maxFeeRate,
|
||||||
|
"maxtxsize": maxSize,
|
||||||
|
"medianfee": medianFee,
|
||||||
|
"mediantime": medianTime.Unix(),
|
||||||
|
"mediantxsize": medianSize,
|
||||||
|
"minfee": minFee,
|
||||||
|
"minfeerate": minFeeRate,
|
||||||
|
"mintxsize": minSize,
|
||||||
|
"outs": int64(outputCount),
|
||||||
|
"swtotal_size": segwitSize,
|
||||||
|
"swtotal_weight": segwitWeight,
|
||||||
|
"swtxs": segwitCount,
|
||||||
|
"subsidy": subsidy,
|
||||||
|
"time": blk.MsgBlock().Header.Timestamp.Unix(),
|
||||||
|
"total_out": totalOutputValue,
|
||||||
|
"total_size": totalSize,
|
||||||
|
"total_weight": totalWeight,
|
||||||
|
"totalfee": totalFees,
|
||||||
|
"txs": int64(len(txs)),
|
||||||
|
"utxo_increase": int64(outputCount - (inputCount - 1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function determines whether a statistic goes into the
|
||||||
|
// final result, except for blockhash and feerate_percentiles
|
||||||
|
// which are handled separately.
|
||||||
|
resultFilter := func(stat string) *int64 {
|
||||||
|
if allStats && s.cfg.TxIndex == nil {
|
||||||
|
// There are no fee statistics to send.
|
||||||
|
excludedStats := []string{"avgfee", "avgfeerate", "maxfee", "maxfeerate", "medianfee", "minfee", "minfeerate"}
|
||||||
|
for _, excluded := range excludedStats {
|
||||||
|
if stat == excluded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allStats || statsSet[stat] {
|
||||||
|
if value, ok := resultMap[stat]; ok {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &btcjson.GetBlockStatsResult{
|
||||||
|
AverageFee: resultFilter("avgfee"),
|
||||||
|
AverageFeeRate: resultFilter("avgfeerate"),
|
||||||
|
AverageTxSize: resultFilter("avgtxsize"),
|
||||||
|
FeeratePercentiles: &feeratePercentiles,
|
||||||
|
Hash: &blockHash,
|
||||||
|
Height: resultFilter("height"),
|
||||||
|
Ins: resultFilter("ins"),
|
||||||
|
MaxFee: resultFilter("maxfee"),
|
||||||
|
MaxFeeRate: resultFilter("maxfeerate"),
|
||||||
|
MaxTxSize: resultFilter("maxtxsize"),
|
||||||
|
MedianFee: resultFilter("medianfee"),
|
||||||
|
MedianTime: resultFilter("mediantime"),
|
||||||
|
MedianTxSize: resultFilter("mediantxsize"),
|
||||||
|
MinFee: resultFilter("minfee"),
|
||||||
|
MinFeeRate: resultFilter("minfeerate"),
|
||||||
|
MinTxSize: resultFilter("mintxsize"),
|
||||||
|
Outs: resultFilter("outs"),
|
||||||
|
SegWitTotalSize: resultFilter("swtotal_size"),
|
||||||
|
SegWitTotalWeight: resultFilter("swtotal_weight"),
|
||||||
|
SegWitTxs: resultFilter("swtxs"),
|
||||||
|
Subsidy: resultFilter("subsidy"),
|
||||||
|
Time: resultFilter("time"),
|
||||||
|
TotalOut: resultFilter("total_out"),
|
||||||
|
TotalSize: resultFilter("total_size"),
|
||||||
|
TotalWeight: resultFilter("total_weight"),
|
||||||
|
TotalFee: resultFilter("totalfee"),
|
||||||
|
Txs: resultFilter("txs"),
|
||||||
|
UTXOIncrease: resultFilter("utxo_increase"),
|
||||||
|
UTXOSizeIncrease: resultFilter("utxo_size_inc"),
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateFee returns the fee of a transaction.
|
||||||
|
func calculateFee(tx *btcutil.Tx, txIndex *indexers.TxIndex, db database.DB) (int64, error) {
|
||||||
|
var inValue, outValue int64
|
||||||
|
for _, input := range tx.MsgTx().TxIn {
|
||||||
|
prevTxHash := input.PreviousOutPoint.Hash
|
||||||
|
// Look up the location of the previous transaction in the index.
|
||||||
|
blockRegion, err := txIndex.TxBlockRegion(&prevTxHash)
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to retrieve transaction location"
|
||||||
|
return 0, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
if blockRegion == nil {
|
||||||
|
return 0, rpcNoTxInfoError(&prevTxHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the raw transaction bytes from the database.
|
||||||
|
var txBytes []byte
|
||||||
|
err = db.View(func(dbTx database.Tx) error {
|
||||||
|
var err error
|
||||||
|
txBytes, err = dbTx.FetchBlockRegion(blockRegion)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, rpcNoTxInfoError(&prevTxHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msgTx wire.MsgTx
|
||||||
|
err = msgTx.Deserialize(bytes.NewReader(txBytes))
|
||||||
|
if err != nil {
|
||||||
|
context := "Failed to deserialize transaction"
|
||||||
|
return 0, internalRPCError(err.Error(), context)
|
||||||
|
}
|
||||||
|
prevOutValue := msgTx.TxOut[input.PreviousOutPoint.Index].Value
|
||||||
|
inValue += prevOutValue
|
||||||
|
}
|
||||||
|
for _, output := range tx.MsgTx().TxOut {
|
||||||
|
outValue += output.Value
|
||||||
|
}
|
||||||
|
fee := inValue - outValue
|
||||||
|
return fee, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// medianBlockTime returns the median time of a block and its 10 previous blocks
|
||||||
|
// as per BIP113.
|
||||||
|
func medianBlockTime(blockHash *chainhash.Hash, chain *blockchain.BlockChain) (*time.Time, error) {
|
||||||
|
blockTimes := make([]time.Time, 0)
|
||||||
|
currentHash := blockHash
|
||||||
|
for i := 0; i < 11; i++ {
|
||||||
|
header, err := chain.HeaderByHash(currentHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blockTimes = append(blockTimes, header.Timestamp)
|
||||||
|
genesisPrevBlock, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
if header.PrevBlock.IsEqual(genesisPrevBlock) {
|
||||||
|
// This is the genesis block so there's no need to iterate further.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currentHash = &header.PrevBlock
|
||||||
|
}
|
||||||
|
sort.Slice(blockTimes, func(i, j int) bool {
|
||||||
|
return blockTimes[i].Before(blockTimes[j])
|
||||||
|
})
|
||||||
|
return &blockTimes[len(blockTimes)/2], nil
|
||||||
|
}
|
||||||
|
|
||||||
// encodeTemplateID encodes the passed details into an ID that can be used to
|
// encodeTemplateID encodes the passed details into an ID that can be used to
|
||||||
// uniquely identify a block template.
|
// uniquely identify a block template.
|
||||||
func encodeTemplateID(prevHash *chainhash.Hash, lastGenerated time.Time) string {
|
func encodeTemplateID(prevHash *chainhash.Hash, lastGenerated time.Time) string {
|
||||||
|
|
|
@ -200,6 +200,43 @@ var helpDescsEnUS = map[string]string{
|
||||||
"getblockchaininforesult-softforks": "The status of the super-majority soft-forks",
|
"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",
|
"getblockchaininforesult-unifiedsoftforks": "The status of the super-majority soft-forks used by bitcoind on or after v0.19.0",
|
||||||
|
|
||||||
|
// GetBlockStatsCmd help.
|
||||||
|
"getblockstats--synopsis": "Returns statistics about a block given its hash or height. --txindex must be enabled for fee and feerate statistics.",
|
||||||
|
"getblockstats-hashorheight": "The hash or height of the block",
|
||||||
|
"hashorheight-value": "The hash or height of the block",
|
||||||
|
"getblockstats-stats": "Selected statistics",
|
||||||
|
|
||||||
|
// GetBlockStatsResult help.
|
||||||
|
"getblockstatsresult-avgfee": "The average fee in the block",
|
||||||
|
"getblockstatsresult-avgfeerate": "The average feerate in the block (in satoshis per virtual byte)",
|
||||||
|
"getblockstatsresult-avgtxsize": "The average transaction size in the block",
|
||||||
|
"getblockstatsresult-blockhash": "The block hash",
|
||||||
|
"getblockstatsresult-feerate_percentiles": "Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per virtual byte)",
|
||||||
|
"getblockstatsresult-height": "The block height",
|
||||||
|
"getblockstatsresult-ins": "The number of inputs (excluding coinbase)",
|
||||||
|
"getblockstatsresult-maxfee": "Maxium fee in the block",
|
||||||
|
"getblockstatsresult-maxfeerate": "Maximum feerate in the block (in satoshis per virtual byte)",
|
||||||
|
"getblockstatsresult-maxtxsize": "Maximum transaction size",
|
||||||
|
"getblockstatsresult-medianfee": "Truncated median fee",
|
||||||
|
"getblockstatsresult-mediantime": "The median time from the block and its previous 10 blocks (BIP113)",
|
||||||
|
"getblockstatsresult-mediantxsize": "Truncated median transaction size",
|
||||||
|
"getblockstatsresult-minfee": "Minimum fee in the block",
|
||||||
|
"getblockstatsresult-minfeerate": "Minimum feerate in the block (in satoshis per virtual byte)",
|
||||||
|
"getblockstatsresult-mintxsize": "Minimum transaction size",
|
||||||
|
"getblockstatsresult-outs": "The number of outputs",
|
||||||
|
"getblockstatsresult-subsidy": "The block subsidy",
|
||||||
|
"getblockstatsresult-swtotal_size": "Total size of all segwit transactions in the block (excluding coinbase)",
|
||||||
|
"getblockstatsresult-swtotal_weight": "Total weight of all segwit transactions in the block (excluding coinbase)",
|
||||||
|
"getblockstatsresult-swtxs": "The number of segwit transactions in the block (excluding coinbase)",
|
||||||
|
"getblockstatsresult-time": "The block time",
|
||||||
|
"getblockstatsresult-total_out": "Total amount in all outputs (excluding coinbase)",
|
||||||
|
"getblockstatsresult-total_size": "Total size of all transactions (excluding coinbase)",
|
||||||
|
"getblockstatsresult-total_weight": "Total weight of all transactions (excluding coinbase)",
|
||||||
|
"getblockstatsresult-totalfee": "The total of fees",
|
||||||
|
"getblockstatsresult-txs": "The number of transactions (excluding coinbase)",
|
||||||
|
"getblockstatsresult-utxo_increase": "The increase/decrease in the number of unspent outputs",
|
||||||
|
"getblockstatsresult-utxo_size_inc": "The increase/decrease in size for the utxo index",
|
||||||
|
|
||||||
// SoftForkDescription help.
|
// SoftForkDescription help.
|
||||||
"softforkdescription-reject": "The current activation status of the softfork",
|
"softforkdescription-reject": "The current activation status of the softfork",
|
||||||
"softforkdescription-version": "The block version that signals enforcement of this softfork",
|
"softforkdescription-version": "The block version that signals enforcement of this softfork",
|
||||||
|
@ -917,6 +954,7 @@ var rpcResultTypes = map[string][]interface{}{
|
||||||
"getblockcount": {(*int64)(nil)},
|
"getblockcount": {(*int64)(nil)},
|
||||||
"getblockhash": {(*string)(nil)},
|
"getblockhash": {(*string)(nil)},
|
||||||
"getblockheader": {(*string)(nil), (*btcjson.GetBlockHeaderVerboseResult)(nil)},
|
"getblockheader": {(*string)(nil), (*btcjson.GetBlockHeaderVerboseResult)(nil)},
|
||||||
|
"getblockstats": {(*btcjson.GetBlockStatsResult)(nil)},
|
||||||
"getblocktemplate": {(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil},
|
"getblocktemplate": {(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil},
|
||||||
"getcfilter": {(*string)(nil)},
|
"getcfilter": {(*string)(nil)},
|
||||||
"getcfilterheader": {(*string)(nil)},
|
"getcfilterheader": {(*string)(nil)},
|
||||||
|
|
Loading…
Reference in a new issue