Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
popm: add static fee option
  • Loading branch information
AL-CT committed Jul 22, 2025
commit 09197a08b5f85744f09588ef5d49b50637987903
12 changes: 12 additions & 0 deletions cmd/popmd/popmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ var (
Help: "the bitcoin url to connect to; it's either a tbc or blockstream url",
Print: config.PrintAll,
},
"POPM_FEE_OVERRIDE": config.Config{
Value: &cfg.StaticFee,
DefaultValue: false,
Help: "if popm should override fee estimation and use a static fee",
Print: config.PrintAll,
},
"POPM_FEE_AMOUNT": config.Config{
Value: &cfg.StaticFeeAmount,
DefaultValue: uint(1),
Help: "static fee amount in sats/byte (requires POPM_FEE_OVERRIDE=true)",
Print: config.PrintAll,
},
}
)

Expand Down
45 changes: 38 additions & 7 deletions service/popm/popm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
btcmempool "github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/wire"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/juju/loggo"
"github.com/prometheus/client_golang/prometheus"

"github.com/hemilabs/heminetwork/api/gethapi"
"github.com/hemilabs/heminetwork/api/tbcapi"
"github.com/hemilabs/heminetwork/bitcoin/wallet"
"github.com/hemilabs/heminetwork/bitcoin/wallet/gozer"
"github.com/hemilabs/heminetwork/bitcoin/wallet/gozer/blockstream"
Expand Down Expand Up @@ -51,6 +53,9 @@ const (
defaultL2KeystoneMaxAge = 4 * time.Hour
defaultL2KeystonePollTimeout = 13 * time.Second
defaultL2KeystoneRetryTimeout = 15 * time.Second

// convert btcd minimum fee from sat/kB to sat/byte
minRelayFee = uint(btcmempool.DefaultMinRelayTxFee) / 1000
)

var log = loggo.GetLogger("popm")
Expand All @@ -73,6 +78,8 @@ type Config struct {
PrometheusListenAddress string
PrometheusNamespace string
RetryMineThreshold uint
StaticFee bool
StaticFeeAmount uint

// cooked settings, do not export
opgethReconnectTimeout time.Duration
Expand Down Expand Up @@ -164,6 +171,11 @@ func NewServer(cfg *Config) (*Server, error) {
workC: make(chan struct{}, 2),
}

if cfg.StaticFee && cfg.StaticFeeAmount < minRelayFee {
return nil, fmt.Errorf("static fee set to %v, minimum is %v",
cfg.StaticFeeAmount, minRelayFee)
}

switch strings.ToLower(cfg.Network) {
case "mainnet":
s.params = &chaincfg.MainNetParams
Expand Down Expand Up @@ -259,14 +271,9 @@ func (s *Server) createKeystoneTx(ctx context.Context, ks *hemi.L2Keystone) (*wi
}
scriptHash := vinzclortho.ScriptHashFromScript(payToScript)

// Estimate BTC fees.
feeEstimates, err := s.gozer.FeeEstimates(ctx)
if err != nil {
return nil, fmt.Errorf("fee estimates: %w", err)
}
feeAmount, err := gozer.FeeByConfirmations(s.cfg.BitcoinConfirmations, feeEstimates)
feeAmount, err := s.getFee(ctx)
if err != nil {
return nil, fmt.Errorf("fee by confirmations: %w", err)
return nil, err
}

// Retrieve available UTXOs for the miner.
Expand Down Expand Up @@ -334,6 +341,30 @@ func (s *Server) latestKeystones(ctx context.Context, count int) (*gethapi.L2Key
return &kr, nil
}

func (s *Server) getFee(ctx context.Context) (*tbcapi.FeeEstimate, error) {
log.Tracef("getFee")
defer log.Tracef("getFee exit")

var feeAmount *tbcapi.FeeEstimate
if s.cfg.StaticFee {
feeAmount = &tbcapi.FeeEstimate{
Blocks: s.cfg.BitcoinConfirmations,
SatsPerByte: float64(s.cfg.StaticFeeAmount),
}
} else {
// Estimate BTC fees.
feeEstimates, err := s.gozer.FeeEstimates(ctx)
if err != nil {
return nil, fmt.Errorf("fee estimates: %w", err)
}
feeAmount, err = gozer.FeeByConfirmations(s.cfg.BitcoinConfirmations, feeEstimates)
if err != nil {
return nil, fmt.Errorf("fee by confirmations: %w", err)
}
}
return feeAmount, nil
}

// reconcileKeystones generates a keystones map
func (s *Server) reconcileKeystones(ctx context.Context) (map[chainhash.Hash]*keystone, error) {
log.Tracef("reconcileKeystones")
Expand Down
39 changes: 39 additions & 0 deletions service/popm/popm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,45 @@ func TestDisconnectedOpgeth(t *testing.T) {
}
}

func TestStaticFee(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
defer cancel()

// Setup pop miner
cfg := NewDefaultConfig()
cfg.BitcoinSource = "tbc"
cfg.BitcoinSecret = "5e2deaa9f1bb2bcef294cc36513c591c5594d6b671fe83a104aa2708bc634c"
cfg.LogLevel = "popm=TRACE; mock=TRACE;"
cfg.StaticFee = true
cfg.StaticFeeAmount = 0

if err := loggo.ConfigureLoggers(cfg.LogLevel); err != nil {
t.Fatal(err)
}

// shouldn't allow static fee of 0
_, err := NewServer(cfg)
if err == nil {
t.Fatal("expected error")
}

cfg.StaticFeeAmount = 3
// shouldn't allow static fee of 0
s, err := NewServer(cfg)
if err != nil {
t.Fatal(err)
}

fee, err := s.getFee(ctx)
if err != nil {
t.Fatal(err)
}

if fee.SatsPerByte != 3 {
t.Fatalf("expected fee of 3 sats/byte, got %v sats/byte", fee.SatsPerByte)
}
}

func messageListener(t *testing.T, expected map[string]int, errCh chan error, msgCh chan string) error {
for {
select {
Expand Down