decred.org/dcrdex@v1.0.5/tatanka/chain/utxo/btc.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package utxo
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  	"time"
    16  
    17  	"decred.org/dcrdex/dex"
    18  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    19  	"decred.org/dcrdex/tatanka/chain"
    20  	"decred.org/dcrdex/tatanka/tanka"
    21  	"github.com/btcsuite/btcd/btcjson"
    22  	"github.com/btcsuite/btcd/btcutil"
    23  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    24  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    25  	"github.com/decred/dcrd/rpcclient/v8"
    26  )
    27  
    28  const (
    29  	BitcoinID      = 0
    30  	feeMonitorTick = time.Second * 10
    31  )
    32  
    33  func init() {
    34  	chain.RegisterChainConstructor(0, NewBitcoin)
    35  }
    36  
    37  type BitcoinConfigFile struct {
    38  	dexbtc.RPCConfig
    39  	NodeRelay string `json:"nodeRelay"`
    40  }
    41  
    42  type bitcoinChain struct {
    43  	cfg  *BitcoinConfigFile
    44  	net  dex.Network
    45  	log  dex.Logger
    46  	fees chan uint64
    47  	name string
    48  
    49  	cl        *rpcclient.Client
    50  	connected atomic.Bool
    51  }
    52  
    53  func NewBitcoin(rawConfig json.RawMessage, log dex.Logger, net dex.Network) (chain.Chain, error) {
    54  	var cfg BitcoinConfigFile
    55  	if err := json.Unmarshal(rawConfig, &cfg); err != nil {
    56  		return nil, fmt.Errorf("error parsing configuration: %w", err)
    57  	}
    58  	if cfg.NodeRelay != "" {
    59  		cfg.RPCBind = cfg.NodeRelay
    60  	}
    61  
    62  	if err := dexbtc.CheckRPCConfig(&cfg.RPCConfig, "Bitcoin", net, dexbtc.RPCPorts); err != nil {
    63  		return nil, fmt.Errorf("error validating RPC configuration: %v", err)
    64  	}
    65  
    66  	return &bitcoinChain{
    67  		cfg:  &cfg,
    68  		net:  net,
    69  		log:  log,
    70  		name: "Bitcoin",
    71  		fees: make(chan uint64, 1),
    72  	}, nil
    73  }
    74  
    75  func (c *bitcoinChain) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) {
    76  	cfg := c.cfg
    77  	host := cfg.RPCBind
    78  	if cfg.NodeRelay != "" {
    79  		host = cfg.NodeRelay
    80  	}
    81  	c.cl, err = connectLocalHTTP(host, cfg.RPCUser, cfg.RPCPass)
    82  	if err != nil {
    83  		return nil, fmt.Errorf("error connecting RPC client: %w", err)
    84  	}
    85  
    86  	if err = c.initialize(ctx); err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	c.connected.Store(true)
    91  	defer c.connected.Store(false)
    92  
    93  	var wg sync.WaitGroup
    94  	wg.Add(1)
    95  	go func() {
    96  		defer wg.Done()
    97  		c.monitorFees(ctx)
    98  	}()
    99  
   100  	return &wg, nil
   101  }
   102  
   103  func (c *bitcoinChain) initialize(ctx context.Context) error {
   104  	// TODO: Check min version
   105  
   106  	tip, err := c.getBestBlockHash(ctx)
   107  	if err != nil {
   108  		return fmt.Errorf("error getting best block from rpc: %w", err)
   109  	}
   110  	if tip == nil {
   111  		return fmt.Errorf("nil best block hash?")
   112  	}
   113  
   114  	txindex, err := c.checkTxIndex(ctx)
   115  	if err != nil {
   116  		c.log.Warnf(`Please ensure txindex is enabled in the node config and you might need to re-index if txindex was not previously enabled for %s`, c.name)
   117  		return fmt.Errorf("error checking txindex for %s: %w", c.name, err)
   118  	}
   119  	if !txindex {
   120  		return fmt.Errorf("%s transaction index is not enabled. Please enable txindex in the node config and you might need to re-index when you enable txindex", c.name)
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  // func (c *bitcoinChain) Query(ctx context.Context, rawQuery chain.Query) (chain.Result, error) {
   127  // 	var q query
   128  // 	if err := json.Unmarshal(rawQuery, &q); err != nil {
   129  // 		return nil, chain.BadQueryError(fmt.Errorf("error parsing raw query: %w", err))
   130  // 	}
   131  
   132  // 	if q.Method == "" {
   133  // 		return nil, chain.BadQueryError(errors.New("invalid query parameters. no method"))
   134  // 	}
   135  
   136  // 	res, err := c.cl.RawRequest(ctx, q.Method, q.Args)
   137  // 	if err != nil {
   138  // 		// Could potentially try to parse certain errors here
   139  
   140  // 		return nil, fmt.Errorf("error performing query: %w", err)
   141  // 	}
   142  
   143  // 	return chain.Result(res), nil
   144  // }
   145  
   146  func (c *bitcoinChain) Connected() bool {
   147  	return c.connected.Load()
   148  }
   149  
   150  func (c *bitcoinChain) FeeChannel() <-chan uint64 {
   151  	return c.fees
   152  }
   153  
   154  func (c *bitcoinChain) monitorFees(ctx context.Context) {
   155  	tick := time.NewTicker(feeMonitorTick)
   156  	var tip *chainhash.Hash
   157  	for {
   158  		select {
   159  		case <-tick.C:
   160  		case <-ctx.Done():
   161  			return
   162  		}
   163  
   164  		newTip, err := c.getBestBlockHash(ctx)
   165  		if err != nil {
   166  			c.connected.Store(false)
   167  			c.log.Errorf("Decred is not connected: %w", err)
   168  			continue
   169  		}
   170  		if newTip == nil { // sanity check
   171  			c.log.Error("nil tip hash?")
   172  			continue
   173  		}
   174  		if tip != nil && *tip == *newTip {
   175  			continue
   176  		}
   177  		tip = newTip
   178  		c.connected.Store(true)
   179  		// estimatesmartfee 1 returns extremely high rates on DCR.
   180  		estimateFeeResult, err := c.cl.EstimateSmartFee(ctx, 2, chainjson.EstimateSmartFeeConservative)
   181  		if err != nil {
   182  			c.log.Errorf("estimatesmartfee error: %w", err)
   183  			continue
   184  		}
   185  		atomsPerKB, err := btcutil.NewAmount(estimateFeeResult.FeeRate)
   186  		if err != nil {
   187  			c.log.Errorf("NewAmount error: %w", err)
   188  			continue
   189  		}
   190  		atomsPerB := uint64(math.Round(float64(atomsPerKB) / 1000))
   191  		if atomsPerB == 0 {
   192  			atomsPerB = 1
   193  		}
   194  		select {
   195  		case c.fees <- atomsPerB:
   196  		case <-time.After(time.Second * 5):
   197  			c.log.Errorf("fee channel is blocking")
   198  		}
   199  	}
   200  }
   201  
   202  func (c *bitcoinChain) callHashGetter(ctx context.Context, method string, args []any) (*chainhash.Hash, error) {
   203  	var txid string
   204  	err := c.call(ctx, method, args, &txid)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	return chainhash.NewHashFromStr(txid)
   209  }
   210  
   211  // GetBestBlockHash returns the hash of the best block in the longest block
   212  // chain.
   213  func (c *bitcoinChain) getBestBlockHash(ctx context.Context) (*chainhash.Hash, error) {
   214  	return c.callHashGetter(ctx, "getbestblockhash", nil)
   215  }
   216  
   217  // checkTxIndex checks if bitcoind transaction index is enabled.
   218  func (c *bitcoinChain) checkTxIndex(ctx context.Context) (bool, error) {
   219  	var res struct {
   220  		TxIndex *struct{} `json:"txindex"`
   221  	}
   222  	err := c.call(ctx, "getindexinfo", []any{"txindex"}, &res)
   223  	if err == nil {
   224  		// Return early if there is no error. bitcoind returns an empty json
   225  		// object if txindex is not enabled. It is safe to conclude txindex is
   226  		// enabled if res.Txindex is not nil.
   227  		return res.TxIndex != nil, nil
   228  	}
   229  
   230  	if !isMethodNotFoundErr(err) {
   231  		return false, err
   232  	}
   233  
   234  	// Using block at index 5 to retrieve a coinbase transaction and ensure
   235  	// txindex is enabled for pre 0.21 versions of bitcoind.
   236  	const blockIndex = 5
   237  	blockHash, err := c.getBlockHash(ctx, blockIndex)
   238  	if err != nil {
   239  		return false, err
   240  	}
   241  
   242  	blockInfo, err := c.getBlockVerbose(ctx, blockHash)
   243  	if err != nil {
   244  		return false, err
   245  	}
   246  
   247  	if len(blockInfo.Tx) == 0 {
   248  		return false, fmt.Errorf("block %d does not have a coinbase transaction", blockIndex)
   249  	}
   250  
   251  	txHash, err := chainhash.NewHashFromStr(blockInfo.Tx[0])
   252  	if err != nil {
   253  		return false, err
   254  	}
   255  
   256  	// Retrieve coinbase transaction information.
   257  	txBytes, err := c.getRawTransaction(ctx, txHash)
   258  	if err != nil {
   259  		return false, err
   260  	}
   261  
   262  	return len(txBytes) != 0, nil
   263  }
   264  
   265  func (c *bitcoinChain) getBlockHash(ctx context.Context, index int64) (*chainhash.Hash, error) {
   266  	var blockHashStr string
   267  	if err := c.call(ctx, "getblockhash", []any{index}, &blockHashStr); err != nil {
   268  		return nil, err
   269  	}
   270  	return chainhash.NewHashFromStr(blockHashStr)
   271  }
   272  
   273  type getBlockVerboseResult struct {
   274  	Hash          string   `json:"hash"`
   275  	Confirmations int64    `json:"confirmations"`
   276  	Height        int64    `json:"height"`
   277  	Tx            []string `json:"tx,omitempty"`
   278  	PreviousHash  string   `json:"previousblockhash"`
   279  }
   280  
   281  func (c *bitcoinChain) getBlockVerbose(ctx context.Context, blockHash *chainhash.Hash) (*getBlockVerboseResult, error) {
   282  	var res getBlockVerboseResult
   283  	return &res, c.call(ctx, "getblock", []any{blockHash.String(), []any{1}}, res)
   284  }
   285  
   286  func (c *bitcoinChain) getRawTransaction(ctx context.Context, txHash *chainhash.Hash) ([]byte, error) {
   287  	var txB dex.Bytes
   288  	return txB, c.call(ctx, "getrawtransaction", []any{txHash.String(), false}, &txB)
   289  }
   290  
   291  func (c *bitcoinChain) call(ctx context.Context, method string, args []any, thing any) error {
   292  	params := make([]json.RawMessage, 0, len(args))
   293  	for i := range args {
   294  		p, err := json.Marshal(args[i])
   295  		if err != nil {
   296  			return err
   297  		}
   298  		params = append(params, p)
   299  	}
   300  	b, err := c.cl.RawRequest(ctx, method, params)
   301  	if err != nil {
   302  		return fmt.Errorf("rawrequest error: %w", err)
   303  	}
   304  
   305  	if thing != nil {
   306  		return json.Unmarshal(b, thing)
   307  	}
   308  	return nil
   309  }
   310  
   311  func (c *bitcoinChain) CheckBond(b *tanka.Bond) error {
   312  
   313  	// TODO: Validate bond
   314  
   315  	return nil
   316  }
   317  
   318  func (c *bitcoinChain) AuditHTLC(*tanka.HTLCAudit) (bool, error) {
   319  
   320  	// TODO: Perform the audit
   321  
   322  	return true, nil
   323  }
   324  
   325  // isMethodNotFoundErr will return true if the error indicates that the RPC
   326  // method was not found by the RPC server. The error must be dcrjson.RPCError
   327  // with a numeric code equal to btcjson.ErrRPCMethodNotFound.Code or a message
   328  // containing "method not found".
   329  func isMethodNotFoundErr(err error) bool {
   330  	var errRPCMethodNotFound = int(btcjson.ErrRPCMethodNotFound.Code)
   331  	var rpcErr *btcjson.RPCError
   332  	return errors.As(err, &rpcErr) &&
   333  		(int(rpcErr.Code) == errRPCMethodNotFound ||
   334  			strings.Contains(strings.ToLower(rpcErr.Message), "method not found"))
   335  }