Fix accelerator revenue impact always showing zero

The old approach compared coinbasevalue from two getblocktemplate
calls, but new mempool arrivals between calls masked the displacement
loss. Now uses a single template snapshot to find the marginal
(lowest fee-rate) transaction that would be displaced and reports
its fee as the revenue cost.
This commit is contained in:
satoshi
2026-05-27 14:23:05 +03:00
parent 8242eec265
commit 83d70b2636
3 changed files with 73 additions and 29 deletions
+59 -23
View File
@@ -43,6 +43,54 @@ type AccelerateResult struct {
FeeLostError string `json:"fee_lost_error,omitempty"`
}
// marginalFeeLost estimates the fee revenue lost by inserting a boosted
// transaction into the template. It finds the lowest fee-rate transaction
// in the current template (the one that would be displaced) and returns
// the difference: displaced_fee - boosted_tx_real_fee. If the boosted tx
// is already in the template or the template has room, returns 0.
func marginalFeeLost(tpl *bitcoind.BlockTemplate, txid string, txVsize int64) int64 {
if len(tpl.Transactions) == 0 {
return 0
}
// Check if the tx is already in the template (nothing displaced).
for _, tx := range tpl.Transactions {
if tx.Txid == txid {
return 0
}
}
// Find the marginal transaction: lowest fee-rate in the template.
var marginal *bitcoind.TemplateTx
var marginalRate float64 = math.MaxFloat64
for i := range tpl.Transactions {
tx := &tpl.Transactions[i]
// Weight to vsize: ceil(weight/4)
vsize := (tx.Weight + 3) / 4
if vsize <= 0 {
continue
}
rate := float64(tx.Fee) / float64(vsize)
if rate < marginalRate {
marginalRate = rate
marginal = tx
}
}
if marginal == nil {
return 0
}
// The displaced tx's fee is the revenue we lose. We don't add the
// boosted tx's real fee back because the pool never had it — the tx
// wasn't in the template before the boost.
lost := marginal.Fee
if lost < 0 {
lost = 0
}
return lost
}
// Accelerate boosts a transaction to the target feerate (sat/vB).
func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB float64) (*AccelerateResult, error) {
if targetFeerateVB > MaxFeerateVB {
@@ -68,38 +116,26 @@ func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB f
targetFeerateVB, modifiedFeeSats/float64(entry.Vsize))
}
// Snapshot coinbasevalue BEFORE the boost to measure actual impact.
var coinbaseBefore int64
// Estimate revenue impact from the pre-boost template. The boosted tx
// will displace the marginal (lowest fee-rate) transaction in the
// template. We compute this from the single template snapshot to avoid
// a race with new mempool arrivals between two getblocktemplate calls.
var feeLost int64
var tplErr string
tplBefore, err := s.RPC.GetBlockTemplate(ctx)
tpl, err := s.RPC.GetBlockTemplate(ctx)
if err != nil {
tplErr = fmt.Sprintf("getblocktemplate (before): %s", bitcoind.RPCErrorMessage(err))
s.Log.Warn("accelerate: getblocktemplate failed (before)", "err", err)
tplErr = fmt.Sprintf("getblocktemplate: %s", bitcoind.RPCErrorMessage(err))
s.Log.Warn("accelerate: getblocktemplate failed", "err", err)
} else {
coinbaseBefore = tplBefore.CoinbaseValue
feeLost = marginalFeeLost(tpl, txid, entry.Vsize)
s.Log.Info("accelerate: fee impact estimated",
"marginal_fee_lost", feeLost, "boosted_tx_fee", int64(baseFeeSats))
}
if err := s.RPC.PrioritiseTransaction(ctx, txid, delta); err != nil {
return nil, fmt.Errorf("prioritisetransaction: %w", err)
}
// Measure coinbasevalue AFTER — the difference is the real fee lost.
var feeLost int64
if coinbaseBefore > 0 {
tplAfter, err := s.RPC.GetBlockTemplate(ctx)
if err != nil {
tplErr = fmt.Sprintf("getblocktemplate (after): %s", bitcoind.RPCErrorMessage(err))
s.Log.Warn("accelerate: getblocktemplate failed (after)", "err", err)
} else {
feeLost = coinbaseBefore - tplAfter.CoinbaseValue
if feeLost < 0 {
feeLost = 0 // template improved (new tx arrived between calls)
}
s.Log.Info("accelerate: fee impact measured",
"before", coinbaseBefore, "after", tplAfter.CoinbaseValue, "lost", feeLost)
}
}
rec := store.BoostedTx{
Txid: txid,
OriginalFeerate: currentFeerate,