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,
+10 -2
View File
@@ -273,11 +273,19 @@ func (c *RPC) GetNetworkHashPS(ctx context.Context, blocks, height int) (float64
return out, nil
}
// TemplateTx is one transaction inside a getblocktemplate result.
type TemplateTx struct {
Txid string `json:"txid"`
Fee int64 `json:"fee"` // satoshis
Weight int64 `json:"weight"` // weight units
}
// BlockTemplate is the subset of getblocktemplate we care about: the
// coinbase value (subsidy + fees) and height of the next block.
type BlockTemplate struct {
CoinbaseValue int64 `json:"coinbasevalue"` // satoshis
Height int64 `json:"height"`
CoinbaseValue int64 `json:"coinbasevalue"` // satoshis
Height int64 `json:"height"`
Transactions []TemplateTx `json:"transactions"`
}
// GetBlockTemplate fetches the next block template with segwit rules.
+4 -4
View File
@@ -69,7 +69,7 @@
} else if (feeLostSats > 0) {
success = `Boosted from ${from} to ${to} sat/vB. Revenue impact: -${feeLostSats.toLocaleString()} sats (${(feeLostSats / 1e8).toFixed(8)} BTC) per block mined.`;
} else {
success = `Boosted from ${from} to ${to} sat/vB. No revenue impact — the displaced transaction's fee was equal or lower.`;
success = `Boosted from ${from} to ${to} sat/vB. No revenue impact — the transaction was already in the template or the mempool fits in one block.`;
}
txid = "";
feerateInput = "";
@@ -169,9 +169,9 @@
mempool isn't full (all transactions already fit), there is <em>zero cost</em>.
</p>
<p>
<strong>Revenue impact:</strong> After boosting, the server compares the block template's
coinbase value before and after. The difference (if any) is the actual fee revenue
lost per block mined. This is shown after each boost.
<strong>Revenue impact:</strong> The server inspects the current block template to find
the marginal transaction &mdash; the lowest fee-rate tx that would be displaced by the
boosted one. Its fee is the revenue you sacrifice per block mined, shown after each boost.
</p>
<p>
Your pool: {formatHashrate(poolHashrate)} /