mempool: Implement orphan expiration.
This implements orphan expiration in the mempool such that any orphans that have not had their ancestors materialize within 15 minutes of their initial arrival time will be evicted which in turn will remove any other orphans that attempted to redeem it. In order to perform the evictions with reasonable efficiency, an opportunistic scan interval of 5 minutes is used. That is to say that there is not a hard deadline on the scan interval and instead it runs when a new orphan is added to the pool if enough time has passed. The following is an example of running this code against the main network for around an hour while intentionally generating orphans to test: 2019-10-28 01:44:36.272 [DBG] TXMP: Expired 2 orphans (remaining: 46) 2019-10-28 01:49:39.915 [DBG] TXMP: Expired 17 orphans (remaining: 49) 2019-10-28 01:54:47.704 [DBG] TXMP: Expired 15 orphans (remaining: 43) 2019-10-28 01:59:53.414 [DBG] TXMP: Expired 16 orphans (remaining: 56) 2019-10-28 02:05:09.858 [DBG] TXMP: Expired 16 orphans (remaining: 68) 2019-10-28 02:10:10.567 [DBG] TXMP: Expired 16 orphans (remaining: 78) 2019-10-28 02:15:11.736 [DBG] TXMP: Expired 26 orphans (remaining: 82) 2019-10-28 02:48:34.352 [DBG] TXMP: Expired 85 orphans (remaining: 0) As can be seen from the above, without orphan expiration on this data set, the orphan pool would have grown an additional 193 entries.
This commit is contained in:
parent
a195642ec7
commit
93efe6bfe3
@ -29,3 +29,12 @@ func DisableLog() {
|
||||
func UseLogger(logger slog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
|
||||
// pickNoun returns the singular or plural form of a noun depending
|
||||
// on the count n.
|
||||
func pickNoun(n int, singular, plural string) string {
|
||||
if n == 1 {
|
||||
return singular
|
||||
}
|
||||
return plural
|
||||
}
|
||||
|
||||
@ -56,6 +56,15 @@ const (
|
||||
// maxNullDataOutputs is the maximum number of OP_RETURN null data
|
||||
// pushes in a transaction, after which it is considered non-standard.
|
||||
maxNullDataOutputs = 4
|
||||
|
||||
// orphanTTL is the maximum amount of time an orphan is allowed to
|
||||
// stay in the orphan pool before it expires and is evicted during the
|
||||
// next scan.
|
||||
orphanTTL = time.Minute * 15
|
||||
|
||||
// orphanExpireScanInterval is the minimum amount of time in between
|
||||
// scans of the orphan pool to evict expired transactions.
|
||||
orphanExpireScanInterval = time.Minute * 5
|
||||
)
|
||||
|
||||
// Config is a descriptor containing the memory pool configuration.
|
||||
@ -217,6 +226,14 @@ type VerboseTxDesc struct {
|
||||
Depends []*TxDesc
|
||||
}
|
||||
|
||||
// orphanTx is a 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.
|
||||
type orphanTx struct {
|
||||
tx *dcrutil.Tx
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -224,10 +241,11 @@ type TxPool struct {
|
||||
// The following variables must only be used atomically.
|
||||
lastUpdated int64 // last time pool was updated.
|
||||
|
||||
mtx sync.RWMutex
|
||||
cfg Config
|
||||
pool map[chainhash.Hash]*TxDesc
|
||||
orphans map[chainhash.Hash]*dcrutil.Tx
|
||||
mtx sync.RWMutex
|
||||
cfg Config
|
||||
pool map[chainhash.Hash]*TxDesc
|
||||
|
||||
orphans map[chainhash.Hash]*orphanTx
|
||||
orphansByPrev map[wire.OutPoint]map[chainhash.Hash]*dcrutil.Tx
|
||||
outpoints map[wire.OutPoint]*dcrutil.Tx
|
||||
|
||||
@ -237,6 +255,12 @@ type TxPool struct {
|
||||
|
||||
pennyTotal float64 // exponentially decaying total for penny spends.
|
||||
lastPennyUnix int64 // unix time of last ``penny spend''
|
||||
|
||||
// nextExpireScan is the time after which the orphan pool will be
|
||||
// scanned in order to evict orphans. This is NOT a hard deadline as
|
||||
// the scan will only run when an orphan is added to the pool as opposed
|
||||
// to on an unconditional timer.
|
||||
nextExpireScan time.Time
|
||||
}
|
||||
|
||||
// insertVote inserts a vote into the map of block votes.
|
||||
@ -329,10 +353,9 @@ var _ mining.TxSource = (*TxPool)(nil)
|
||||
//
|
||||
// This function MUST be called with the mempool lock held (for writes).
|
||||
func (mp *TxPool) removeOrphan(tx *dcrutil.Tx, removeRedeemers bool) {
|
||||
txHash := tx.Hash()
|
||||
|
||||
// Nothing to do if passed tx is not an orphan.
|
||||
tx, exists := mp.orphans[*txHash]
|
||||
txHash := tx.Hash()
|
||||
otx, exists := mp.orphans[*txHash]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
@ -340,7 +363,7 @@ func (mp *TxPool) removeOrphan(tx *dcrutil.Tx, removeRedeemers bool) {
|
||||
log.Tracef("Removing orphan transaction %v", txHash)
|
||||
|
||||
// Remove the reference from the previous orphan index.
|
||||
for _, txIn := range tx.MsgTx().TxIn {
|
||||
for _, txIn := range otx.tx.MsgTx().TxIn {
|
||||
orphans, exists := mp.orphansByPrev[txIn.PreviousOutPoint]
|
||||
if exists {
|
||||
delete(orphans, *txHash)
|
||||
@ -389,6 +412,32 @@ func (mp *TxPool) RemoveOrphan(tx *dcrutil.Tx) {
|
||||
//
|
||||
// This function MUST be called with the mempool lock held (for writes).
|
||||
func (mp *TxPool) limitNumOrphans() {
|
||||
// Scan through the orphan pool and remove any expired orphans when it's
|
||||
// time. This is done for efficiency so the scan only happens periodically
|
||||
// instead of on every orphan added to the pool.
|
||||
if now := time.Now(); now.After(mp.nextExpireScan) {
|
||||
origNumOrphans := len(mp.orphans)
|
||||
for _, otx := range mp.orphans {
|
||||
if now.After(otx.expiration) {
|
||||
// Remove redeemers too because the missing parents are very
|
||||
// unlikely to ever materialize since the orphan has already
|
||||
// been around more than long enough for them to be delivered.
|
||||
mp.removeOrphan(otx.tx, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Set next expiration scan to occur after the scan interval.
|
||||
mp.nextExpireScan = now.Add(orphanExpireScanInterval)
|
||||
|
||||
numOrphans := len(mp.orphans)
|
||||
if numExpired := origNumOrphans - numOrphans; numExpired > 0 {
|
||||
log.Debugf("Expired %d %s (remaining: %d)", numExpired,
|
||||
pickNoun(numExpired, "orphan", "orphans"), numOrphans)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do if adding another orphan will not cause the pool to
|
||||
// exceed the limit.
|
||||
if len(mp.orphans)+1 <= mp.cfg.Policy.MaxOrphanTxs {
|
||||
return
|
||||
}
|
||||
@ -399,8 +448,10 @@ func (mp *TxPool) limitNumOrphans() {
|
||||
// is not important here because an adversary would have to be
|
||||
// able to pull off preimage attacks on the hashing function in
|
||||
// order to target eviction of specific entries anyways.
|
||||
for _, tx := range mp.orphans {
|
||||
mp.removeOrphan(tx, false)
|
||||
for _, otx := range mp.orphans {
|
||||
// Don't remove redeemers in the case of a random eviction since
|
||||
// it is quite possible it might be needed again shortly.
|
||||
mp.removeOrphan(otx.tx, false)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -414,11 +465,15 @@ func (mp *TxPool) addOrphan(tx *dcrutil.Tx) {
|
||||
return
|
||||
}
|
||||
|
||||
// Limit the number orphan transactions to prevent memory exhaustion. A
|
||||
// random orphan is evicted to make room if needed.
|
||||
// Limit the number orphan transactions to prevent memory exhaustion.
|
||||
// This will periodically remove any expired orphans and evict a random
|
||||
// orphan if space is still needed.
|
||||
mp.limitNumOrphans()
|
||||
|
||||
mp.orphans[*tx.Hash()] = tx
|
||||
mp.orphans[*tx.Hash()] = &orphanTx{
|
||||
tx: tx,
|
||||
expiration: time.Now().Add(orphanTTL),
|
||||
}
|
||||
for _, txIn := range tx.MsgTx().TxIn {
|
||||
if _, exists := mp.orphansByPrev[txIn.PreviousOutPoint]; !exists {
|
||||
mp.orphansByPrev[txIn.PreviousOutPoint] =
|
||||
@ -1695,11 +1750,12 @@ func (mp *TxPool) LastUpdated() time.Time {
|
||||
// transactions until they are mined into a block.
|
||||
func New(cfg *Config) *TxPool {
|
||||
return &TxPool{
|
||||
cfg: *cfg,
|
||||
pool: make(map[chainhash.Hash]*TxDesc),
|
||||
orphans: make(map[chainhash.Hash]*dcrutil.Tx),
|
||||
orphansByPrev: make(map[wire.OutPoint]map[chainhash.Hash]*dcrutil.Tx),
|
||||
outpoints: make(map[wire.OutPoint]*dcrutil.Tx),
|
||||
votes: make(map[chainhash.Hash][]mining.VoteDesc),
|
||||
cfg: *cfg,
|
||||
pool: make(map[chainhash.Hash]*TxDesc),
|
||||
orphans: make(map[chainhash.Hash]*orphanTx),
|
||||
orphansByPrev: make(map[wire.OutPoint]map[chainhash.Hash]*dcrutil.Tx),
|
||||
outpoints: make(map[wire.OutPoint]*dcrutil.Tx),
|
||||
votes: make(map[chainhash.Hash][]mining.VoteDesc),
|
||||
nextExpireScan: time.Now().Add(orphanExpireScanInterval),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user