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.
|
||||
// Pointers are used instead of values to allow for optional fields.
|
||||
type GetBlockStatsResult struct {
|
||||
AverageFee int64 `json:"avgfee"`
|
||||
AverageFeeRate int64 `json:"avgfeerate"`
|
||||
AverageTxSize int64 `json:"avgtxsize"`
|
||||
FeeratePercentiles []int64 `json:"feerate_percentiles"`
|
||||
Hash string `json:"blockhash"`
|
||||
Height int64 `json:"height"`
|
||||
Ins int64 `json:"ins"`
|
||||
MaxFee int64 `json:"maxfee"`
|
||||
MaxFeeRate int64 `json:"maxfeerate"`
|
||||
MaxTxSize int64 `json:"maxtxsize"`
|
||||
MedianFee int64 `json:"medianfee"`
|
||||
MedianTime int64 `json:"mediantime"`
|
||||
MedianTxSize int64 `json:"mediantxsize"`
|
||||
MinFee int64 `json:"minfee"`
|
||||
MinFeeRate int64 `json:"minfeerate"`
|
||||
MinTxSize int64 `json:"mintxsize"`
|
||||
Outs int64 `json:"outs"`
|
||||
SegWitTotalSize int64 `json:"swtotal_size"`
|
||||
SegWitTotalWeight int64 `json:"swtotal_weight"`
|
||||
SegWitTxs int64 `json:"swtxs"`
|
||||
Subsidy int64 `json:"subsidy"`
|
||||
Time int64 `json:"time"`
|
||||
TotalOut int64 `json:"total_out"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
TotalWeight int64 `json:"total_weight"`
|
||||
Txs int64 `json:"txs"`
|
||||
UTXOIncrease int64 `json:"utxo_increase"`
|
||||
UTXOSizeIncrease int64 `json:"utxo_size_inc"`
|
||||
AverageFee *int64 `json:"avgfee,omitempty"`
|
||||
AverageFeeRate *int64 `json:"avgfeerate,omitempty"`
|
||||
AverageTxSize *int64 `json:"avgtxsize,omitempty"`
|
||||
FeeratePercentiles *[]int64 `json:"feerate_percentiles,omitempty"`
|
||||
Hash *string `json:"blockhash,omitempty"`
|
||||
Height *int64 `json:"height,omitempty"`
|
||||
Ins *int64 `json:"ins,omitempty"`
|
||||
MaxFee *int64 `json:"maxfee,omitempty"`
|
||||
MaxFeeRate *int64 `json:"maxfeerate,omitempty"`
|
||||
MaxTxSize *int64 `json:"maxtxsize,omitempty"`
|
||||
MedianFee *int64 `json:"medianfee,omitempty"`
|
||||
MedianTime *int64 `json:"mediantime,omitempty"`
|
||||
MedianTxSize *int64 `json:"mediantxsize,omitempty"`
|
||||
MinFee *int64 `json:"minfee,omitempty"`
|
||||
MinFeeRate *int64 `json:"minfeerate,omitempty"`
|
||||
MinTxSize *int64 `json:"mintxsize,omitempty"`
|
||||
Outs *int64 `json:"outs,omitempty"`
|
||||
SegWitTotalSize *int64 `json:"swtotal_size,omitempty"`
|
||||
SegWitTotalWeight *int64 `json:"swtotal_weight,omitempty"`
|
||||
SegWitTxs *int64 `json:"swtxs,omitempty"`
|
||||
Subsidy *int64 `json:"subsidy,omitempty"`
|
||||
Time *int64 `json:"time,omitempty"`
|
||||
TotalOut *int64 `json:"total_out,omitempty"`
|
||||
TotalSize *int64 `json:"total_size,omitempty"`
|
||||
TotalWeight *int64 `json:"total_weight,omitempty"`
|
||||
TotalFee *int64 `json:"totalfee,omitempty"`
|
||||
Txs *int64 `json:"txs,omitempty"`
|
||||
UTXOIncrease *int64 `json:"utxo_increase,omitempty"`
|
||||
UTXOSizeIncrease *int64 `json:"utxo_size_inc,omitempty"`
|
||||
}
|
||||
|
||||
type GetBlockVerboseResultBase struct {
|
||||
|
|
|
@ -13,7 +13,9 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lbryio/lbcd/chaincfg"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
testGetBestBlock,
|
||||
testGetBlockCount,
|
||||
testGetBlockHash,
|
||||
testGetBlockStats,
|
||||
testBulkClient,
|
||||
}
|
||||
|
||||
|
@ -151,7 +418,8 @@ func TestMain(m *testing.M) {
|
|||
// In order to properly test scenarios on as if we were on mainnet,
|
||||
// ensure that non-standard transactions aren't accepted into the
|
||||
// mempool or relayed.
|
||||
btcdCfg := []string{"--rejectnonstd"}
|
||||
// Enable transaction index to be able to fully test GetBlockStats
|
||||
btcdCfg := []string{"--rejectnonstd", "--txindex"}
|
||||
primaryHarness, err = rpctest.New(
|
||||
&chaincfg.SimNetParams, nil, btcdCfg, "",
|
||||
)
|
||||
|
|
398
rpcserver.go
398
rpcserver.go
|
@ -21,6 +21,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -151,6 +152,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{
|
|||
"getblockcount": handleGetBlockCount,
|
||||
"getblockhash": handleGetBlockHash,
|
||||
"getblockheader": handleGetBlockHeader,
|
||||
"getblockstats": handleGetBlockStats,
|
||||
"getblocktemplate": handleGetBlockTemplate,
|
||||
"getcfilter": handleGetCFilter,
|
||||
"getcfilterheader": handleGetCFilterHeader,
|
||||
|
@ -1578,6 +1580,402 @@ func handleGetChainTips(s *rpcServer, cmd interface{}, closeChan <-chan struct{}
|
|||
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
|
||||
// uniquely identify a block template.
|
||||
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-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-reject": "The current activation status of the softfork",
|
||||
"softforkdescription-version": "The block version that signals enforcement of this softfork",
|
||||
|
@ -917,6 +954,7 @@ var rpcResultTypes = map[string][]interface{}{
|
|||
"getblockcount": {(*int64)(nil)},
|
||||
"getblockhash": {(*string)(nil)},
|
||||
"getblockheader": {(*string)(nil), (*btcjson.GetBlockHeaderVerboseResult)(nil)},
|
||||
"getblockstats": {(*btcjson.GetBlockStatsResult)(nil)},
|
||||
"getblocktemplate": {(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil},
|
||||
"getcfilter": {(*string)(nil)},
|
||||
"getcfilterheader": {(*string)(nil)},
|
||||
|
|
Loading…
Reference in a new issue