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