From 83d70b26361116341c52f62e996cf1c2d0fc407a Mon Sep 17 00:00:00 2001
From: satoshi
Date: Wed, 27 May 2026 14:23:05 +0300
Subject: [PATCH] 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.
---
api/internal/accelerator/accelerator.go | 82 ++++++++++++++++++-------
api/internal/bitcoind/rpc.go | 12 +++-
ui/src/lib/AcceleratorPage.svelte | 8 +--
3 files changed, 73 insertions(+), 29 deletions(-)
diff --git a/api/internal/accelerator/accelerator.go b/api/internal/accelerator/accelerator.go
index 982aca5..032cccb 100644
--- a/api/internal/accelerator/accelerator.go
+++ b/api/internal/accelerator/accelerator.go
@@ -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,
diff --git a/api/internal/bitcoind/rpc.go b/api/internal/bitcoind/rpc.go
index de5042e..ccd7d61 100644
--- a/api/internal/bitcoind/rpc.go
+++ b/api/internal/bitcoind/rpc.go
@@ -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.
diff --git a/ui/src/lib/AcceleratorPage.svelte b/ui/src/lib/AcceleratorPage.svelte
index 362950a..fdf9892 100644
--- a/ui/src/lib/AcceleratorPage.svelte
+++ b/ui/src/lib/AcceleratorPage.svelte
@@ -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 zero cost.
- Revenue impact: 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.
+ Revenue impact: The server inspects the current block template to find
+ the marginal transaction — 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.
Your pool: {formatHashrate(poolHashrate)} /