mirror of
https://github.com/btcsuite/btcd.git
synced 2025-03-10 17:26:07 +01:00
It is now possible to save and restore the state of the FeeEstimator
and the server searches the database for a previous state to load when the program is turned on.
This commit is contained in:
parent
4fd446028f
commit
47113d428c
4 changed files with 318 additions and 8 deletions
|
@ -5,11 +5,15 @@
|
||||||
package mempool
|
package mempool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
|
@ -20,8 +24,6 @@ import (
|
||||||
// TODO incorporate Alex Morcos' modifications to Gavin's initial model
|
// TODO incorporate Alex Morcos' modifications to Gavin's initial model
|
||||||
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2014-October/006824.html
|
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2014-October/006824.html
|
||||||
|
|
||||||
// TODO store and restore the FeeEstimator state in the database.
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// estimateFeeDepth is the maximum number of blocks before a transaction
|
// estimateFeeDepth is the maximum number of blocks before a transaction
|
||||||
// is confirmed that we want to track.
|
// is confirmed that we want to track.
|
||||||
|
@ -48,6 +50,12 @@ const (
|
||||||
btcPerSatoshi = 1E-8
|
btcPerSatoshi = 1E-8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// EstimateFeeDatabaseKey is the key that we use to
|
||||||
|
// store the fee estimator in the database.
|
||||||
|
EstimateFeeDatabaseKey = []byte("estimatefee")
|
||||||
|
)
|
||||||
|
|
||||||
// SatoshiPerByte is number with units of satoshis per byte.
|
// SatoshiPerByte is number with units of satoshis per byte.
|
||||||
type SatoshiPerByte float64
|
type SatoshiPerByte float64
|
||||||
|
|
||||||
|
@ -99,6 +107,29 @@ type observedTransaction struct {
|
||||||
mined int32
|
mined int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *observedTransaction) Serialize(w io.Writer) {
|
||||||
|
binary.Write(w, binary.BigEndian, o.hash)
|
||||||
|
binary.Write(w, binary.BigEndian, o.feeRate)
|
||||||
|
binary.Write(w, binary.BigEndian, o.observed)
|
||||||
|
binary.Write(w, binary.BigEndian, o.mined)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserializeObservedTransaction(r io.Reader) (*observedTransaction, error) {
|
||||||
|
ot := observedTransaction{}
|
||||||
|
|
||||||
|
// The first 32 bytes should be a hash.
|
||||||
|
binary.Read(r, binary.BigEndian, &ot.hash)
|
||||||
|
|
||||||
|
// The next 8 are SatoshiPerByte
|
||||||
|
binary.Read(r, binary.BigEndian, &ot.feeRate)
|
||||||
|
|
||||||
|
// And next there are two uint32's.
|
||||||
|
binary.Read(r, binary.BigEndian, &ot.observed)
|
||||||
|
binary.Read(r, binary.BigEndian, &ot.mined)
|
||||||
|
|
||||||
|
return &ot, nil
|
||||||
|
}
|
||||||
|
|
||||||
// registeredBlock has the hash of a block and the list of transactions
|
// registeredBlock has the hash of a block and the list of transactions
|
||||||
// it mined which had been previously observed by the FeeEstimator. It
|
// it mined which had been previously observed by the FeeEstimator. It
|
||||||
// is used if Rollback is called to reverse the effect of registering
|
// is used if Rollback is called to reverse the effect of registering
|
||||||
|
@ -108,6 +139,15 @@ type registeredBlock struct {
|
||||||
transactions []*observedTransaction
|
transactions []*observedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rb *registeredBlock) serialize(w io.Writer, txs map[*observedTransaction]uint32) {
|
||||||
|
binary.Write(w, binary.BigEndian, rb.hash)
|
||||||
|
|
||||||
|
binary.Write(w, binary.BigEndian, uint32(len(rb.transactions)))
|
||||||
|
for _, o := range rb.transactions {
|
||||||
|
binary.Write(w, binary.BigEndian, txs[o])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FeeEstimator manages the data necessary to create
|
// FeeEstimator manages the data necessary to create
|
||||||
// fee estimations. It is safe for concurrent access.
|
// fee estimations. It is safe for concurrent access.
|
||||||
type FeeEstimator struct {
|
type FeeEstimator struct {
|
||||||
|
@ -533,3 +573,177 @@ func (ef *FeeEstimator) EstimateFee(numBlocks uint32) (BtcPerKilobyte, error) {
|
||||||
|
|
||||||
return ef.cached[int(numBlocks)-1].ToBtcPerKb(), nil
|
return ef.cached[int(numBlocks)-1].ToBtcPerKb(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case the format for the serialized version of the FeeEstimator changes,
|
||||||
|
// we use a version number. If the version number changes, it does not make
|
||||||
|
// sense to try to upgrade a previous version to a new version. Instead, just
|
||||||
|
// start fee estimation over.
|
||||||
|
const estimateFeeSaveVersion = 1
|
||||||
|
|
||||||
|
func deserializeRegisteredBlock(r io.Reader, txs map[uint32]*observedTransaction) (*registeredBlock, error) {
|
||||||
|
var lenTransactions uint32
|
||||||
|
|
||||||
|
rb := ®isteredBlock{}
|
||||||
|
binary.Read(r, binary.BigEndian, &rb.hash)
|
||||||
|
binary.Read(r, binary.BigEndian, &lenTransactions)
|
||||||
|
|
||||||
|
rb.transactions = make([]*observedTransaction, lenTransactions)
|
||||||
|
|
||||||
|
for i := uint32(0); i < lenTransactions; i++ {
|
||||||
|
var index uint32
|
||||||
|
binary.Read(r, binary.BigEndian, &index)
|
||||||
|
rb.transactions[i] = txs[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
return rb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeeEstimatorState represents a saved FeeEstimator that can be
|
||||||
|
// restored with data from an earlier session of the program.
|
||||||
|
type FeeEstimatorState []byte
|
||||||
|
|
||||||
|
// observedTxSet is a set of txs that can that is sorted
|
||||||
|
// by hash. It exists for serialization purposes so that
|
||||||
|
// a serialized state always comes out the same.
|
||||||
|
type observedTxSet []*observedTransaction
|
||||||
|
|
||||||
|
func (q observedTxSet) Len() int { return len(q) }
|
||||||
|
|
||||||
|
func (q observedTxSet) Less(i, j int) bool {
|
||||||
|
return strings.Compare(q[i].hash.String(), q[j].hash.String()) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q observedTxSet) Swap(i, j int) {
|
||||||
|
q[i], q[j] = q[j], q[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save records the current state of the FeeEstimator to a []byte that
|
||||||
|
// can be restored later.
|
||||||
|
func (ef *FeeEstimator) Save() FeeEstimatorState {
|
||||||
|
ef.mtx.Lock()
|
||||||
|
defer ef.mtx.Unlock()
|
||||||
|
|
||||||
|
// TODO figure out what the capacity should be.
|
||||||
|
w := bytes.NewBuffer(make([]byte, 0))
|
||||||
|
|
||||||
|
binary.Write(w, binary.BigEndian, uint32(estimateFeeSaveVersion))
|
||||||
|
|
||||||
|
// Insert basic parameters.
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.maxRollback)
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.binSize)
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.maxReplacements)
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.minRegisteredBlocks)
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.lastKnownHeight)
|
||||||
|
binary.Write(w, binary.BigEndian, &ef.numBlocksRegistered)
|
||||||
|
|
||||||
|
// Put all the observed transactions in a sorted list.
|
||||||
|
var txCount uint32
|
||||||
|
ots := make([]*observedTransaction, len(ef.observed))
|
||||||
|
for hash := range ef.observed {
|
||||||
|
ots[txCount] = ef.observed[hash]
|
||||||
|
txCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(observedTxSet(ots))
|
||||||
|
|
||||||
|
txCount = 0
|
||||||
|
observed := make(map[*observedTransaction]uint32)
|
||||||
|
binary.Write(w, binary.BigEndian, uint32(len(ef.observed)))
|
||||||
|
for _, ot := range ots {
|
||||||
|
ot.Serialize(w)
|
||||||
|
observed[ot] = txCount
|
||||||
|
txCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all the right bins.
|
||||||
|
for _, list := range ef.bin {
|
||||||
|
|
||||||
|
binary.Write(w, binary.BigEndian, uint32(len(list)))
|
||||||
|
|
||||||
|
for _, o := range list {
|
||||||
|
binary.Write(w, binary.BigEndian, observed[o])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropped transactions.
|
||||||
|
binary.Write(w, binary.BigEndian, uint32(len(ef.dropped)))
|
||||||
|
for _, registered := range ef.dropped {
|
||||||
|
registered.serialize(w, observed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the tx and return.
|
||||||
|
return FeeEstimatorState(w.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreFeeEstimator takes a FeeEstimatorState that was previously
|
||||||
|
// returned by Save and restores it to a FeeEstimator
|
||||||
|
func RestoreFeeEstimator(data FeeEstimatorState) (*FeeEstimator, error) {
|
||||||
|
r := bytes.NewReader([]byte(data))
|
||||||
|
|
||||||
|
// Check version
|
||||||
|
var version uint32
|
||||||
|
err := binary.Read(r, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if version != estimateFeeSaveVersion {
|
||||||
|
return nil, fmt.Errorf("Incorrect version: expected %d found %d", estimateFeeSaveVersion, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
ef := &FeeEstimator{
|
||||||
|
observed: make(map[chainhash.Hash]*observedTransaction),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read basic parameters.
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.maxRollback)
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.binSize)
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.maxReplacements)
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.minRegisteredBlocks)
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.lastKnownHeight)
|
||||||
|
binary.Read(r, binary.BigEndian, &ef.numBlocksRegistered)
|
||||||
|
|
||||||
|
// Read transactions.
|
||||||
|
var numObserved uint32
|
||||||
|
observed := make(map[uint32]*observedTransaction)
|
||||||
|
binary.Read(r, binary.BigEndian, &numObserved)
|
||||||
|
for i := uint32(0); i < numObserved; i++ {
|
||||||
|
ot, err := deserializeObservedTransaction(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
observed[i] = ot
|
||||||
|
ef.observed[ot.hash] = ot
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read bins.
|
||||||
|
for i := 0; i < estimateFeeDepth; i++ {
|
||||||
|
var numTransactions uint32
|
||||||
|
binary.Read(r, binary.BigEndian, &numTransactions)
|
||||||
|
bin := make([]*observedTransaction, numTransactions)
|
||||||
|
for j := uint32(0); j < numTransactions; j++ {
|
||||||
|
var index uint32
|
||||||
|
binary.Read(r, binary.BigEndian, &index)
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
bin[j], exists = observed[index]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("Invalid transaction reference %d", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ef.bin[i] = bin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read dropped transactions.
|
||||||
|
var numDropped uint32
|
||||||
|
binary.Read(r, binary.BigEndian, &numDropped)
|
||||||
|
ef.dropped = make([]*registeredBlock, numDropped)
|
||||||
|
for i := uint32(0); i < numDropped; i++ {
|
||||||
|
var err error
|
||||||
|
ef.dropped[int(i)], err = deserializeRegisteredBlock(r, observed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ef, nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package mempool
|
package mempool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -364,3 +365,60 @@ func TestEstimateFeeRollback(t *testing.T) {
|
||||||
estimateHistory = estimateHistory[0 : len(estimateHistory)-stepsBack]
|
estimateHistory = estimateHistory[0 : len(estimateHistory)-stepsBack]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (eft *estimateFeeTester) checkSaveAndRestore(
|
||||||
|
previousEstimates [estimateFeeDepth]BtcPerKilobyte) {
|
||||||
|
|
||||||
|
// Get the save state.
|
||||||
|
save := eft.ef.Save()
|
||||||
|
|
||||||
|
// Save and restore database.
|
||||||
|
var err error
|
||||||
|
eft.ef, err = RestoreFeeEstimator(save)
|
||||||
|
if err != nil {
|
||||||
|
eft.t.Fatalf("Could not restore database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save again and check that it matches the previous one.
|
||||||
|
redo := eft.ef.Save()
|
||||||
|
if !bytes.Equal(save, redo) {
|
||||||
|
eft.t.Fatalf("Restored states do not match: %v %v", save, redo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the results match.
|
||||||
|
newEstimates := eft.estimates()
|
||||||
|
|
||||||
|
for i, prev := range previousEstimates {
|
||||||
|
if prev != newEstimates[i] {
|
||||||
|
eft.t.Error("Mismatch in estimate ", i, " after restore; got ", newEstimates[i], " but expected ", prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSave tests saving and restoring to a []byte.
|
||||||
|
func TestDatabase(t *testing.T) {
|
||||||
|
|
||||||
|
txPerRound := uint32(7)
|
||||||
|
txPerBlock := uint32(5)
|
||||||
|
binSize := uint32(6)
|
||||||
|
maxReplacements := uint32(4)
|
||||||
|
rounds := 8
|
||||||
|
|
||||||
|
eft := estimateFeeTester{ef: newTestFeeEstimator(binSize, maxReplacements, uint32(rounds)+1), t: t}
|
||||||
|
var txHistory [][]*TxDesc
|
||||||
|
estimateHistory := [][estimateFeeDepth]BtcPerKilobyte{eft.estimates()}
|
||||||
|
|
||||||
|
for round := 0; round < rounds; round++ {
|
||||||
|
eft.checkSaveAndRestore(estimateHistory[len(estimateHistory)-1])
|
||||||
|
|
||||||
|
// Go forward one step.
|
||||||
|
txHistory, estimateHistory =
|
||||||
|
eft.round(txHistory, estimateHistory, txPerRound, txPerBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse the process and try again.
|
||||||
|
for round := 1; round <= rounds; round++ {
|
||||||
|
eft.rollback()
|
||||||
|
eft.checkSaveAndRestore(estimateHistory[len(estimateHistory)-round-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -873,7 +873,7 @@ func handleEstimateFee(s *rpcServer, cmd interface{}, closeChan <-chan struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to satoshis per kb.
|
// Convert to satoshis per kb.
|
||||||
return float64(feeRate.ToSatoshiPerKb()), nil
|
return float64(feeRate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGenerate handles generate commands.
|
// handleGenerate handles generate commands.
|
||||||
|
|
44
server.go
44
server.go
|
@ -230,6 +230,10 @@ type server struct {
|
||||||
txIndex *indexers.TxIndex
|
txIndex *indexers.TxIndex
|
||||||
addrIndex *indexers.AddrIndex
|
addrIndex *indexers.AddrIndex
|
||||||
cfIndex *indexers.CfIndex
|
cfIndex *indexers.CfIndex
|
||||||
|
|
||||||
|
// The fee estimator keeps track of how long transactions are left in
|
||||||
|
// the mempool before they are mined into blocks.
|
||||||
|
feeEstimator *mempool.FeeEstimator
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverPeer extends the peer to maintain state shared by the server and
|
// serverPeer extends the peer to maintain state shared by the server and
|
||||||
|
@ -2107,6 +2111,14 @@ func (s *server) Stop() error {
|
||||||
s.rpcServer.Stop()
|
s.rpcServer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save fee estimator state in the database.
|
||||||
|
s.db.Update(func(tx database.Tx) error {
|
||||||
|
metadata := tx.Metadata()
|
||||||
|
metadata.Put(mempool.EstimateFeeDatabaseKey, s.feeEstimator.Save())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Signal the remaining goroutines to quit.
|
// Signal the remaining goroutines to quit.
|
||||||
close(s.quit)
|
close(s.quit)
|
||||||
return nil
|
return nil
|
||||||
|
@ -2411,9 +2423,35 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
feeEstimator := mempool.NewFeeEstimator(
|
// Search for a FeeEstimator state in the database. If none can be found
|
||||||
|
// or if it cannot be loaded, create a new one.
|
||||||
|
db.Update(func(tx database.Tx) error {
|
||||||
|
metadata := tx.Metadata()
|
||||||
|
feeEstimationData := metadata.Get(mempool.EstimateFeeDatabaseKey)
|
||||||
|
if feeEstimationData != nil {
|
||||||
|
// delete it from the database so that we don't try to restore the
|
||||||
|
// same thing again somehow.
|
||||||
|
metadata.Delete(mempool.EstimateFeeDatabaseKey)
|
||||||
|
|
||||||
|
// If there is an error, log it and make a new fee estimator.
|
||||||
|
var err error
|
||||||
|
s.feeEstimator, err = mempool.RestoreFeeEstimator(feeEstimationData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
peerLog.Errorf("Failed to restore fee estimator %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// If no feeEstimator has been found, or if the one that has been found
|
||||||
|
// is behind somehow, create a new one and start over.
|
||||||
|
if s.feeEstimator == nil || s.feeEstimator.LastKnownHeight() != s.chain.BestSnapshot().Height {
|
||||||
|
s.feeEstimator = mempool.NewFeeEstimator(
|
||||||
mempool.DefaultEstimateFeeMaxRollback,
|
mempool.DefaultEstimateFeeMaxRollback,
|
||||||
mempool.DefaultEstimateFeeMinRegisteredBlocks)
|
mempool.DefaultEstimateFeeMinRegisteredBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
txC := mempool.Config{
|
txC := mempool.Config{
|
||||||
Policy: mempool.Policy{
|
Policy: mempool.Policy{
|
||||||
|
@ -2437,7 +2475,7 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param
|
||||||
SigCache: s.sigCache,
|
SigCache: s.sigCache,
|
||||||
HashCache: s.hashCache,
|
HashCache: s.hashCache,
|
||||||
AddrIndex: s.addrIndex,
|
AddrIndex: s.addrIndex,
|
||||||
FeeEstimator: feeEstimator,
|
FeeEstimator: s.feeEstimator,
|
||||||
}
|
}
|
||||||
s.txMemPool = mempool.New(&txC)
|
s.txMemPool = mempool.New(&txC)
|
||||||
|
|
||||||
|
@ -2586,7 +2624,7 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param
|
||||||
TxIndex: s.txIndex,
|
TxIndex: s.txIndex,
|
||||||
AddrIndex: s.addrIndex,
|
AddrIndex: s.addrIndex,
|
||||||
CfIndex: s.cfIndex,
|
CfIndex: s.cfIndex,
|
||||||
FeeEstimator: feeEstimator,
|
FeeEstimator: s.feeEstimator,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Add table
Reference in a new issue