Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
10 changes: 5 additions & 5 deletions bitcoin/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func UtxoPickerSingle(amount, fee btcutil.Amount, utxos []*tbcapi.UTXO) (*tbcapi
return nil, errors.New("no suitable utxo found")
}

func TransactionCreate(locktime uint32, amount, satsPerByte btcutil.Amount, address btcutil.Address, utxos []*tbcapi.UTXO, script []byte) (*wire.MsgTx, map[string][]byte, error) {
func TransactionCreate(locktime uint32, amount btcutil.Amount, satsPerByte float64, address btcutil.Address, utxos []*tbcapi.UTXO, script []byte) (*wire.MsgTx, map[string][]byte, error) {
// Create TxOut
payToScript, err := txscript.PayToAddrScript(address)
if err != nil {
Expand All @@ -70,7 +70,7 @@ func TransactionCreate(locktime uint32, amount, satsPerByte btcutil.Amount, addr

// Calculate fee for worst case input number and assume there is change
txSize := txsizes.EstimateSerializeSize(len(utxos), []*wire.TxOut{txOut}, true)
fee := btcutil.Amount(txSize) * satsPerByte
fee := btcutil.Amount(float64(txSize) * satsPerByte)

// Find utxo list that is big enough for entire transaction
utxoList, err := UtxoPickerMultiple(amount, fee, utxos)
Expand All @@ -80,7 +80,7 @@ func TransactionCreate(locktime uint32, amount, satsPerByte btcutil.Amount, addr

// Calculate fee for real input number and assume there is change
txSize = txsizes.EstimateSerializeSize(len(utxoList), []*wire.TxOut{txOut}, true)
fee = btcutil.Amount(txSize) * satsPerByte
fee = btcutil.Amount(float64(txSize) * satsPerByte)

// Assemble transaction
tx := wire.NewMsgTx(2) // Latest supported version
Expand All @@ -104,7 +104,7 @@ func TransactionCreate(locktime uint32, amount, satsPerByte btcutil.Amount, addr
return tx, prevOuts, nil
}

func PoPTransactionCreate(l2keystone *hemi.L2Keystone, locktime uint32, satsPerByte btcutil.Amount, utxos []*tbcapi.UTXO, script []byte) (*wire.MsgTx, map[string][]byte, error) {
func PoPTransactionCreate(l2keystone *hemi.L2Keystone, locktime uint32, satsPerByte float64, utxos []*tbcapi.UTXO, script []byte) (*wire.MsgTx, map[string][]byte, error) {
// Create OP_RETURN
aks := hemi.L2KeystoneAbbreviate(*l2keystone)
popTx := pop.TransactionL2{L2Keystone: aks}
Expand All @@ -116,7 +116,7 @@ func PoPTransactionCreate(l2keystone *hemi.L2Keystone, locktime uint32, satsPerB

// Calculate fee for 1 input and assume there is change
txSize := txsizes.EstimateSerializeSize(1, []*wire.TxOut{popTxOut}, true)
fee := btcutil.Amount(txSize) * satsPerByte
fee := btcutil.Amount(float64(txSize) * satsPerByte)

// Find utxo that is big enough for entire transaction
utxo, err := UtxoPickerSingle(0, fee, utxos) // no amount, just fees
Expand Down
4 changes: 2 additions & 2 deletions bitcoin/wallet/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func TestIntegration(t *testing.T) {
}

tx, prevOut, err := TransactionCreate(uint32(time.Now().Unix()),
btcutil.Amount(550), btcutil.Amount(feeEstimateForTx.SatsPerByte), addr, utxos, pkscript)
btcutil.Amount(550), feeEstimateForTx.SatsPerByte, addr, utxos, pkscript)
if err != nil {
t.Fatal(err)
}
Expand All @@ -198,7 +198,7 @@ func TestIntegration(t *testing.T) {
}

popTx, prevOut, err := PoPTransactionCreate(keystone, uint32(time.Now().Unix()),
btcutil.Amount(feeEstimateForPop.SatsPerByte+0.5), utxos, pkscript)
feeEstimateForPop.SatsPerByte+0.5, utxos, pkscript)
if err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/popmd/popmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ var (
Help: "the bitcoin url to connect to; it's either a tbc or blockstream url",
Print: config.PrintAll,
},
"POPM_STATIC_FEE": config.Config{
Value: &cfg.StaticFee,
DefaultValue: float64(0),
Help: "static fee amount in sats/byte; overrides fee estimation if greater than 0. Can be decimal (ex. 1.5 sats/byte)",
Print: config.PrintAll,
},
}
)

Expand Down
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ func Parse(c CfgMap) error {
}
reflect.ValueOf(v.Value).Elem().SetInt(evTyped)

case reflect.Float32, reflect.Float64:
evTyped, err := strconv.ParseFloat(envValue, 64)
if err != nil {
return fmt.Errorf("invalid float for %v: %w",
k, err)
}
reflect.ValueOf(v.Value).Elem().SetFloat(evTyped)

case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:

Expand Down
43 changes: 35 additions & 8 deletions service/popm/popm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"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 +52,8 @@ const (
defaultL2KeystoneMaxAge = 4 * time.Hour
defaultL2KeystonePollTimeout = 13 * time.Second
defaultL2KeystoneRetryTimeout = 15 * time.Second

minRelayFee = 1 // sats/byte
)

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

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

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

switch strings.ToLower(cfg.Network) {
case "mainnet":
s.params = &chaincfg.MainNetParams
Expand Down Expand Up @@ -259,14 +268,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)
feeAmount, err := s.estimateFee(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 nil, err
}

// Retrieve available UTXOs for the miner.
Expand All @@ -279,7 +283,7 @@ func (s *Server) createKeystoneTx(ctx context.Context, ks *hemi.L2Keystone) (*wi

// Build transaction.
popTx, prevOut, err := wallet.PoPTransactionCreate(ks, uint32(btcHeight),
btcutil.Amount(feeAmount.SatsPerByte), utxos, payToScript)
feeAmount.SatsPerByte, utxos, payToScript)
if err != nil {
return nil, fmt.Errorf("create transaction: %w", err)
}
Expand Down Expand Up @@ -334,6 +338,29 @@ func (s *Server) latestKeystones(ctx context.Context, count int) (*gethapi.L2Key
return &kr, nil
}

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

if s.cfg.StaticFee != 0 {
return &tbcapi.FeeEstimate{
Blocks: s.cfg.BitcoinConfirmations,
SatsPerByte: s.cfg.StaticFee,
}, nil
}
// 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
36 changes: 36 additions & 0 deletions service/popm/popm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,42 @@ 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 = 0.5

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

_, err := NewServer(cfg)
if err == nil {
t.Fatal("expected error")
}

cfg.StaticFee = 1.5
s, err := NewServer(cfg)
if err != nil {
t.Fatal(err)
}

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

if fee.SatsPerByte != 1.5 {
t.Fatalf("expected fee of 1.5 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
Loading