From 13e31d033aa8db3afc00b127e00f5999d5071d3a Mon Sep 17 00:00:00 2001 From: Jonathan Moody <103143855+moodyjon@users.noreply.github.com> Date: Thu, 14 Jul 2022 17:44:39 -0400 Subject: [PATCH] [rpc mempool] Add support for usage, total_fee, mempoolminfee, minrelaytxfee to RPC getmempoolinfo. --- btcjson/chainsvrresults.go | 8 +++- mempool/mempool.go | 72 +++++++++++++++++++++++++++++++- mempool/memusage.go | 84 ++++++++++++++++++++++++++++++++++++++ rpcserver.go | 14 +------ rpcserverhelp.go | 8 +++- 5 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 mempool/memusage.go diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index ffa0a103..261cdcf4 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -337,8 +337,12 @@ type GetChainTipsResult struct { // GetMempoolInfoResult models the data returned from the getmempoolinfo // command. type GetMempoolInfoResult struct { - Size int64 `json:"size"` - Bytes int64 `json:"bytes"` + Size int64 `json:"size"` // Current tx count + Bytes int64 `json:"bytes"` // Sum of all virtual transaction sizes as defined in BIP 141. Differs from actual serialized size because witness data is discounted + Usage int64 `json:"usage"` // Total memory usage for the mempool + TotalFee float64 `json:"total_fee"` // Total fees for the mempool in BTC, ignoring modified fees through prioritizetransaction + MemPoolMinFee float64 `json:"mempoolminfee"` // Minimum fee rate in BTC/kvB for tx to be accepted. Is the maximum of minrelaytxfee and minimum mempool fee + MinRelayTxFee float64 `json:"minrelaytxfee"` // Current minimum relay fee for transactions } // NetworksResult models the networks data from the getnetworkinfo command. diff --git a/mempool/mempool.go b/mempool/mempool.go index 45cf602b..9f2e4f28 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -8,6 +8,7 @@ import ( "container/list" "fmt" "math" + "reflect" "sync" "sync/atomic" "time" @@ -156,6 +157,15 @@ type Policy struct { RejectReplacement bool } +// aggregateInfo tracks aggregated serialized size, memory usage, and fees +// for TxDesc in the mempool. +type aggregateInfo struct { + totalCount int64 + totalBytes int64 + totalMem int64 + totalFee int64 +} + // TxDesc is a descriptor containing a transaction in the mempool along with // additional metadata. type TxDesc struct { @@ -166,6 +176,20 @@ type TxDesc struct { StartingPriority float64 } +func (txD *TxDesc) incr(info *aggregateInfo) { + info.totalCount += 1 + info.totalBytes += int64(txD.Tx.MsgTx().SerializeSize()) + info.totalMem += int64(dynamicMemUsage(reflect.ValueOf(txD), false, 0)) + info.totalFee += txD.Fee +} + +func (txD *TxDesc) decr(info *aggregateInfo) { + info.totalCount -= 1 + info.totalBytes -= int64(txD.Tx.MsgTx().SerializeSize()) + info.totalMem -= int64(dynamicMemUsage(reflect.ValueOf(txD), false, 0)) + info.totalFee -= txD.Fee +} + // orphanTx is normal transaction that references an ancestor transaction // that is not yet available. It also contains additional information related // to it such as an expiration time to help prevent caching the orphan forever. @@ -175,6 +199,18 @@ type orphanTx struct { expiration time.Time } +func (otx *orphanTx) incr(info *aggregateInfo) { + info.totalCount += 1 + info.totalBytes += int64(otx.tx.MsgTx().SerializeSize()) + info.totalMem += int64(dynamicMemUsage(reflect.ValueOf(otx), true, 0)) +} + +func (otx *orphanTx) decr(info *aggregateInfo) { + info.totalCount -= 1 + info.totalBytes -= int64(otx.tx.MsgTx().SerializeSize()) + info.totalMem -= int64(dynamicMemUsage(reflect.ValueOf(otx), false, 0)) +} + // TxPool is used as a source of transactions that need to be mined into blocks // and relayed to other peers. It is safe for concurrent access from multiple // peers. @@ -196,6 +232,9 @@ type TxPool struct { // the scan will only run when an orphan is added to the pool as opposed // to on an unconditional timer. nextExpireScan time.Time + + // stats are aggregated over pool, orphans, etc. + stats aggregateInfo } // Ensure the TxPool type implements the mining.TxSource interface. @@ -240,6 +279,9 @@ func (mp *TxPool) removeOrphan(tx *btcutil.Tx, removeRedeemers bool) { // Remove the transaction from the orphan pool. delete(mp.orphans, *txHash) + + // Update stats. + otx.decr(&mp.stats) } // RemoveOrphan removes the passed orphan transaction from the orphan pool and @@ -336,11 +378,12 @@ func (mp *TxPool) addOrphan(tx *btcutil.Tx, tag Tag) { // orphan if space is still needed. mp.limitNumOrphans() - mp.orphans[*tx.Hash()] = &orphanTx{ + otx := &orphanTx{ tx: tx, tag: tag, expiration: time.Now().Add(orphanTTL), } + mp.orphans[*tx.Hash()] = otx for _, txIn := range tx.MsgTx().TxIn { if _, exists := mp.orphansByPrev[txIn.PreviousOutPoint]; !exists { mp.orphansByPrev[txIn.PreviousOutPoint] = @@ -349,6 +392,9 @@ func (mp *TxPool) addOrphan(tx *btcutil.Tx, tag Tag) { mp.orphansByPrev[txIn.PreviousOutPoint][*tx.Hash()] = tx } + // Update stats. + otx.incr(&mp.stats) + log.Debugf("Stored orphan transaction %v (total: %d)", tx.Hash(), len(mp.orphans)) } @@ -498,6 +544,9 @@ func (mp *TxPool) removeTransaction(tx *btcutil.Tx, removeRedeemers bool) { } delete(mp.pool, *txHash) + // Update stats. + txDesc.decr(&mp.stats) + // Inform associated fee estimator that the transaction has been removed // from the mempool if mp.cfg.RemoveTxFromFeeEstimation != nil { @@ -579,6 +628,9 @@ func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *btcutil mp.cfg.AddTxToFeeEstimation(txD.Tx.Hash(), txD.Fee, size) } + // Update stats. + txD.incr(&mp.stats) + return txD } @@ -1503,6 +1555,24 @@ func (mp *TxPool) MiningDescs() []*mining.TxDesc { return descs } +func (mp *TxPool) MempoolInfo() *btcjson.GetMempoolInfoResult { + mp.mtx.RLock() + policy := mp.cfg.Policy + stats := mp.stats + mp.mtx.RUnlock() + + ret := &btcjson.GetMempoolInfoResult{ + Size: stats.totalCount, + Usage: stats.totalMem, + Bytes: stats.totalBytes, + TotalFee: btcutil.Amount(stats.totalFee).ToBTC(), + MemPoolMinFee: btcutil.Amount(calcMinRequiredTxRelayFee(1000, policy.MinRelayTxFee)).ToBTC(), + MinRelayTxFee: policy.MinRelayTxFee.ToBTC(), + } + + return ret +} + // RawMempoolVerbose returns all the entries in the mempool as a fully // populated btcjson result. // diff --git a/mempool/memusage.go b/mempool/memusage.go new file mode 100644 index 00000000..f6a546a0 --- /dev/null +++ b/mempool/memusage.go @@ -0,0 +1,84 @@ +// Copyright (c) 2013-2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mempool + +import ( + "reflect" +) + +func dynamicMemUsage(v reflect.Value, debug bool, level int) uintptr { + t := v.Type() + bytes := t.Size() + if debug { + println("[", level, "]", t.Kind().String(), "(", t.String(), ") ->", t.Size()) + } + + // For complex types, we need to peek inside slices/arrays/structs/maps and chase pointers. + switch t.Kind() { + case reflect.Pointer, reflect.Interface: + if !v.IsNil() { + bytes += dynamicMemUsage(v.Elem(), debug, level+1) + } + case reflect.Array, reflect.Slice: + for j := 0; j < v.Len(); j++ { + vi := v.Index(j) + k := vi.Type().Kind() + if debug { + println("[", level, "] index:", j, "kind:", k.String()) + } + elemB := uintptr(0) + if t.Kind() == reflect.Array { + if (k == reflect.Pointer || k == reflect.Interface) && !vi.IsNil() { + elemB += dynamicMemUsage(vi.Elem(), debug, level+1) + } + } else { // slice + elemB += dynamicMemUsage(vi, debug, level+1) + } + if k == reflect.Uint8 { + // short circuit for byte slice/array + bytes += elemB * uintptr(v.Len()) + if debug { + println("...", v.Len(), "elements") + } + break + } + bytes += elemB + } + case reflect.Map: + iter := v.MapRange() + for iter.Next() { + vk := iter.Key() + vv := iter.Value() + if debug { + println("[", level, "] key:", vk.Type().Kind().String()) + } + bytes += dynamicMemUsage(vk, debug, level+1) + if debug { + println("[", level, "] value:", vv.Type().Kind().String()) + } + bytes += dynamicMemUsage(vv, debug, level+1) + if debug { + println("...", v.Len(), "map elements") + } + debug = false + } + case reflect.Struct: + for _, f := range reflect.VisibleFields(t) { + vf := v.FieldByIndex(f.Index) + k := vf.Type().Kind() + if debug { + println("[", level, "] field:", f.Name, "kind:", k.String()) + } + if (k == reflect.Pointer || k == reflect.Interface) && !vf.IsNil() { + bytes += dynamicMemUsage(vf.Elem(), debug, level+1) + } else if k == reflect.Array || k == reflect.Slice { + bytes -= vf.Type().Size() + bytes += dynamicMemUsage(vf, debug, level+1) + } + } + } + + return bytes +} diff --git a/rpcserver.go b/rpcserver.go index 1e37f458..e2f21981 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2466,19 +2466,7 @@ func handleGetInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in // handleGetMempoolInfo implements the getmempoolinfo command. func handleGetMempoolInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { - mempoolTxns := s.cfg.TxMemPool.TxDescs() - - var numBytes int64 - for _, txD := range mempoolTxns { - numBytes += int64(txD.Tx.MsgTx().SerializeSize()) - } - - ret := &btcjson.GetMempoolInfoResult{ - Size: int64(len(mempoolTxns)), - Bytes: numBytes, - } - - return ret, nil + return s.cfg.TxMemPool.MempoolInfo(), nil } // handleGetMempoolEntry implements the getmempoolentry command. diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 4cfde4d3..8c339855 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -453,8 +453,12 @@ var helpDescsEnUS = map[string]string{ "getmempoolinfo--synopsis": "Returns memory pool information", // GetMempoolInfoResult help. - "getmempoolinforesult-bytes": "Size in bytes of the mempool", - "getmempoolinforesult-size": "Number of transactions in the mempool", + "getmempoolinforesult-bytes": "Size in bytes of the mempool", + "getmempoolinforesult-size": "Number of transactions in the mempool", + "getmempoolinforesult-usage": "Total memory usage for the mempool", + "getmempoolinforesult-total_fee": "Total fees for the mempool in LBC, ignoring modified fees through prioritizetransaction", + "getmempoolinforesult-mempoolminfee": "Minimum fee rate in LBC/kvB for tx to be accepted. Is the maximum of minrelaytxfee and minimum mempool fee", + "getmempoolinforesult-minrelaytxfee": "Current minimum relay fee for transactions", // GetMiningInfoResult help. "getmininginforesult-blocks": "Height of the latest best block",