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 }