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:
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
Transactions []TemplateTx `json:"transactions"`
|
||||
}
|
||||
|
||||
// GetBlockTemplate fetches the next block template with segwit rules.
|
||||
|
||||
@@ -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 — 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)} /
|
||||
|
||||
Reference in New Issue
Block a user