decred.org/dcrdex@v1.0.3/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 c.connected.Store(true) 178 // estimatesmartfee 1 returns extremely high rates on DCR. 179 estimateFeeResult, err := c.cl.EstimateSmartFee(ctx, 2, chainjson.EstimateSmartFeeConservative) 180 if err != nil { 181 c.log.Errorf("estimatesmartfee error: %w", err) 182 continue 183 } 184 atomsPerKB, err := btcutil.NewAmount(estimateFeeResult.FeeRate) 185 if err != nil { 186 c.log.Errorf("NewAmount error: %w", err) 187 continue 188 } 189 atomsPerB := uint64(math.Round(float64(atomsPerKB) / 1000)) 190 if atomsPerB == 0 { 191 atomsPerB = 1 192 } 193 select { 194 case c.fees <- atomsPerB: 195 case <-time.After(time.Second * 5): 196 c.log.Errorf("fee channel is blocking") 197 } 198 199 } 200 201 } 202 203 func (c *bitcoinChain) callHashGetter(ctx context.Context, method string, args []any) (*chainhash.Hash, error) { 204 var txid string 205 err := c.call(ctx, method, args, &txid) 206 if err != nil { 207 return nil, err 208 } 209 return chainhash.NewHashFromStr(txid) 210 } 211 212 // GetBestBlockHash returns the hash of the best block in the longest block 213 // chain. 214 func (c *bitcoinChain) getBestBlockHash(ctx context.Context) (*chainhash.Hash, error) { 215 return c.callHashGetter(ctx, "getbestblockhash", nil) 216 } 217 218 // checkTxIndex checks if bitcoind transaction index is enabled. 219 func (c *bitcoinChain) checkTxIndex(ctx context.Context) (bool, error) { 220 var res struct { 221 TxIndex *struct{} `json:"txindex"` 222 } 223 err := c.call(ctx, "getindexinfo", []any{"txindex"}, &res) 224 if err == nil { 225 // Return early if there is no error. bitcoind returns an empty json 226 // object if txindex is not enabled. It is safe to conclude txindex is 227 // enabled if res.Txindex is not nil. 228 return res.TxIndex != nil, nil 229 } 230 231 if !isMethodNotFoundErr(err) { 232 return false, err 233 } 234 235 // Using block at index 5 to retrieve a coinbase transaction and ensure 236 // txindex is enabled for pre 0.21 versions of bitcoind. 237 const blockIndex = 5 238 blockHash, err := c.getBlockHash(ctx, blockIndex) 239 if err != nil { 240 return false, err 241 } 242 243 blockInfo, err := c.getBlockVerbose(ctx, blockHash) 244 if err != nil { 245 return false, err 246 } 247 248 if len(blockInfo.Tx) == 0 { 249 return false, fmt.Errorf("block %d does not have a coinbase transaction", blockIndex) 250 } 251 252 txHash, err := chainhash.NewHashFromStr(blockInfo.Tx[0]) 253 if err != nil { 254 return false, err 255 } 256 257 // Retrieve coinbase transaction information. 258 txBytes, err := c.getRawTransaction(ctx, txHash) 259 if err != nil { 260 return false, err 261 } 262 263 return len(txBytes) != 0, nil 264 } 265 266 func (c *bitcoinChain) getBlockHash(ctx context.Context, index int64) (*chainhash.Hash, error) { 267 var blockHashStr string 268 if err := c.call(ctx, "getblockhash", []any{index}, &blockHashStr); err != nil { 269 return nil, err 270 } 271 return chainhash.NewHashFromStr(blockHashStr) 272 } 273 274 type getBlockVerboseResult struct { 275 Hash string `json:"hash"` 276 Confirmations int64 `json:"confirmations"` 277 Height int64 `json:"height"` 278 Tx []string `json:"tx,omitempty"` 279 PreviousHash string `json:"previousblockhash"` 280 } 281 282 func (c *bitcoinChain) getBlockVerbose(ctx context.Context, blockHash *chainhash.Hash) (*getBlockVerboseResult, error) { 283 var res getBlockVerboseResult 284 return &res, c.call(ctx, "getblock", []any{blockHash.String(), []any{1}}, res) 285 } 286 287 func (c *bitcoinChain) getRawTransaction(ctx context.Context, txHash *chainhash.Hash) ([]byte, error) { 288 var txB dex.Bytes 289 return txB, c.call(ctx, "getrawtransaction", []any{txHash.String(), false}, &txB) 290 } 291 292 func (c *bitcoinChain) call(ctx context.Context, method string, args []any, thing any) error { 293 params := make([]json.RawMessage, 0, len(args)) 294 for i := range args { 295 p, err := json.Marshal(args[i]) 296 if err != nil { 297 return err 298 } 299 params = append(params, p) 300 } 301 b, err := c.cl.RawRequest(ctx, method, params) 302 if err != nil { 303 return fmt.Errorf("rawrequest error: %w", err) 304 } 305 306 if thing != nil { 307 return json.Unmarshal(b, thing) 308 } 309 return nil 310 } 311 312 func (c *bitcoinChain) CheckBond(b *tanka.Bond) error { 313 314 // TODO: Validate bond 315 316 return nil 317 } 318 319 func (c *bitcoinChain) AuditHTLC(*tanka.HTLCAudit) (bool, error) { 320 321 // TODO: Perform the audit 322 323 return true, nil 324 } 325 326 // isMethodNotFoundErr will return true if the error indicates that the RPC 327 // method was not found by the RPC server. The error must be dcrjson.RPCError 328 // with a numeric code equal to btcjson.ErrRPCMethodNotFound.Code or a message 329 // containing "method not found". 330 func isMethodNotFoundErr(err error) bool { 331 var errRPCMethodNotFound = int(btcjson.ErrRPCMethodNotFound.Code) 332 var rpcErr *btcjson.RPCError 333 return errors.As(err, &rpcErr) && 334 (int(rpcErr.Code) == errRPCMethodNotFound || 335 strings.Contains(strings.ToLower(rpcErr.Message), "method not found")) 336 }