1// SPDX-License-Identifier: ISC 2// Copyright (c) 2014-2020 Bitmark Inc. 3// Use of this source code is governed by an ISC 4// license that can be found in the LICENSE file. 5 6package payment 7 8import ( 9 "encoding/hex" 10 "encoding/json" 11 "net/http" 12 "sync" 13 "time" 14 15 "github.com/bitmark-inc/bitmarkd/constants" 16 "github.com/bitmark-inc/bitmarkd/currency" 17 "github.com/bitmark-inc/bitmarkd/currency/satoshi" 18 "github.com/bitmark-inc/bitmarkd/pay" 19 "github.com/bitmark-inc/bitmarkd/reservoir" 20 "github.com/bitmark-inc/bitmarkd/util" 21 "github.com/bitmark-inc/logger" 22) 23 24const ( 25 bitcoinOPReturnHexCode = "6a30" // op code with 48 byte parameter 26 bitcoinOPReturnPrefixLength = len(bitcoinOPReturnHexCode) 27 bitcoinOPReturnPayIDOffset = bitcoinOPReturnPrefixLength 28 bitcoinOPReturnRecordLength = bitcoinOPReturnPrefixLength + 2*48 29) 30 31type bitcoinScriptPubKey struct { 32 Hex string `json:"hex"` 33 Addresses []string `json:"addresses"` 34} 35 36type bitcoinVout struct { 37 Value json.RawMessage `json:"value"` 38 ScriptPubKey bitcoinScriptPubKey `json:"scriptPubKey"` 39} 40 41type bitcoinTransaction struct { 42 TxId string `json:"txid"` 43 Vout []bitcoinVout `json:"vout"` 44} 45 46type bitcoinBlock struct { 47 Hash string `json:"hash"` 48 Confirmations uint64 `json:"confirmations"` 49 Height uint64 `json:"height"` 50 Tx []bitcoinTransaction `json:"tx"` 51 Time int64 `json:"time"` 52 PreviousBlockHash string `json:"previousblockhash"` 53 NextBlockHash string `json:"nextblockhash"` 54} 55 56type bitcoinBlockHeader struct { 57 Hash string `json:"hash"` 58 Confirmations uint64 `json:"confirmations"` 59 Height uint64 `json:"height"` 60 Time int64 `json:"time"` 61 PreviousBlockHash string `json:"previousblockhash"` 62 NextBlockHash string `json:"nextblockhash"` 63} 64 65type bitcoinChainInfo struct { 66 Blocks uint64 `json:"blocks"` 67 Hash string `json:"bestblockhash"` 68} 69 70// bitcoinHandler implements the currencyHandler interface for Bitcoin 71type bitcoinHandler struct { 72 log *logger.L 73 state *bitcoinState 74} 75 76func newBitcoinHandler(conf *currencyConfiguration) (*bitcoinHandler, error) { 77 log := logger.New("bitcoin") 78 79 state, err := newBitcoinState(conf.URL) 80 if err != nil { 81 return nil, err 82 } 83 return &bitcoinHandler{log, state}, nil 84} 85 86func (h *bitcoinHandler) processPastTxs(dat []byte) { 87 txs := make([]bitcoinTransaction, 0) 88 if err := json.Unmarshal(dat, &txs); err != nil { 89 h.log.Errorf("unable to unmarshal txs: %v", err) 90 return 91 } 92 93 for _, tx := range txs { 94 h.log.Debugf("old possible payment tx received: %s\n", tx.TxId) 95 inspectBitcoinTx(h.log, &tx) 96 } 97} 98 99func (h *bitcoinHandler) processIncomingTx(dat []byte) { 100 var tx bitcoinTransaction 101 if err := json.Unmarshal(dat, &tx); err != nil { 102 h.log.Errorf("unable to unmarshal tx: %v", err) 103 return 104 } 105 106 h.log.Debugf("new possible payment tx received: %s\n", tx.TxId) 107 inspectBitcoinTx(h.log, &tx) 108} 109 110func (h *bitcoinHandler) checkLatestBlock(wg *sync.WaitGroup) { 111 defer wg.Done() 112 113 var headers []bitcoinBlockHeader 114 if err := util.FetchJSON(h.state.client, h.state.url+"/headers/1/"+h.state.latestBlockHash+".json", &headers); err != nil { 115 h.log.Errorf("headers: error: %s", err) 116 return 117 } 118 119 if len(headers) < 1 { 120 return 121 } 122 123 h.log.Infof("block number: %d confirmations: %d", headers[0].Height, headers[0].Confirmations) 124 125 if h.state.forward && headers[0].Confirmations <= requiredConfirmations { 126 return 127 } 128 129 h.state.process(h.log) 130} 131 132// bitcoinState maintains the block state and extracts possible payment txs from bitcoin blocks 133type bitcoinState struct { 134 // connection to bitcoind 135 client *http.Client 136 url string 137 138 // latest block info 139 latestBlockNumber uint64 140 latestBlockHash string 141 142 // scanning direction 143 forward bool 144} 145 146func newBitcoinState(url string) (*bitcoinState, error) { 147 client := &http.Client{} 148 149 var chain bitcoinChainInfo 150 if err := util.FetchJSON(client, url+"/chaininfo.json", &chain); err != nil { 151 return nil, err 152 } 153 154 return &bitcoinState{ 155 client: client, 156 url: url, 157 latestBlockNumber: chain.Blocks, 158 latestBlockHash: chain.Hash, 159 forward: false, 160 }, nil 161} 162 163func (state *bitcoinState) process(log *logger.L) { 164 counter := 0 // number of blocks processed 165 startTime := time.Now() // used to calculate the elapsed time of the process 166 traceStopTime := time.Now().Add(-constants.ReservoirTimeout) // reverse scan stops when the block is older than traceStopTime 167 168 hash := state.latestBlockHash 169 170process_blocks: 171 for { 172 var block bitcoinBlock 173 if err := util.FetchJSON(state.client, state.url+"/block/"+hash+".json", &block); err != nil { 174 log.Errorf("failed to get the block by hash: %s", hash) 175 return 176 } 177 log.Infof("height: %d hash: %q number of txs: %d", block.Height, block.Hash, len(block.Tx)) 178 log.Tracef("block: %#v", block) 179 180 if block.Confirmations <= requiredConfirmations { 181 if !state.forward { 182 hash = block.PreviousBlockHash 183 state.latestBlockHash = hash 184 continue process_blocks 185 } 186 state.latestBlockHash = hash 187 break process_blocks 188 } 189 190 // extract possible payment txs from the block 191 transactionCount := len(block.Tx) // ignore the first tx (coinbase tx) 192 if transactionCount > 1 { 193 for _, tx := range block.Tx[1:] { 194 inspectBitcoinTx(log, &tx) 195 } 196 } 197 198 // throttle the sync speed 199 counter++ 200 if counter > 10 { 201 timeTaken := time.Since(startTime) 202 rate := float64(counter) / timeTaken.Seconds() 203 if rate > maximumBlockRate { 204 log.Infof("the current rate %f exceeds the limit %f", rate, maximumBlockRate) 205 time.Sleep(2 * time.Second) 206 } 207 } 208 209 // move to the next block 210 if state.forward { 211 hash = block.NextBlockHash 212 } else { 213 blockTime := time.Unix(block.Time, 0) 214 if blockTime.Before(traceStopTime) { 215 state.forward = true 216 break process_blocks 217 } 218 hash = block.PreviousBlockHash 219 } 220 } 221} 222 223func inspectBitcoinTx(log *logger.L, tx *bitcoinTransaction) { 224 _, err := hex.DecodeString(tx.TxId) 225 if err != nil { 226 log.Errorf("invalid tx id: %s", tx.TxId) 227 return 228 } 229 230 var payId pay.PayId 231 amounts := make(map[string]uint64) 232 found := false 233 234scan_vouts: 235 for _, vout := range tx.Vout { 236 if len(vout.ScriptPubKey.Hex) == bitcoinOPReturnRecordLength && vout.ScriptPubKey.Hex[0:4] == bitcoinOPReturnHexCode { 237 pid := vout.ScriptPubKey.Hex[bitcoinOPReturnPayIDOffset:] 238 if err := payId.UnmarshalText([]byte(pid)); err != nil { 239 log.Errorf("invalid pay id: %s", pid) 240 return 241 } 242 243 found = true 244 continue scan_vouts 245 } 246 247 if len(vout.ScriptPubKey.Addresses) == 1 { 248 amounts[vout.ScriptPubKey.Addresses[0]] += satoshi.FromByteString(vout.Value) 249 } 250 } 251 252 if !found { 253 return 254 } 255 256 if len(amounts) == 0 { 257 log.Warnf("found pay id but no payments in tx id: %s", tx.TxId) 258 return 259 } 260 261 reservoir.SetTransferVerified( 262 payId, 263 &reservoir.PaymentDetail{ 264 Currency: currency.Bitcoin, 265 TxID: tx.TxId, 266 Amounts: amounts, 267 }, 268 ) 269} 270