diff --git a/bchain/bitcoinrpc.go b/bchain/bitcoinrpc.go index 1a5bdc4a..09929fd5 100644 --- a/bchain/bitcoinrpc.go +++ b/bchain/bitcoinrpc.go @@ -2,6 +2,7 @@ package bchain import ( "bytes" + "encoding/binary" "encoding/hex" "encoding/json" "fmt" @@ -11,15 +12,75 @@ import ( "net/http" "time" + vlq "github.com/bsm/go-vlq" + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/golang/glog" "github.com/juju/errors" ) -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` +// BitcoinRPC is an interface to JSON-RPC bitcoind service. +type BitcoinRPC struct { + client http.Client + rpcURL string + user string + password string + parser *BitcoinBlockParser + testnet bool + network string + mempool *Mempool + parseBlocks bool +} + +// NewBitcoinRPC returns new BitcoinRPC instance. +func NewBitcoinRPC(url string, user string, password string, timeout time.Duration, parse bool) (BlockChain, error) { + transport := &http.Transport{ + Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, // necessary to not to deplete ports + } + s := &BitcoinRPC{ + client: http.Client{Timeout: timeout, Transport: transport}, + rpcURL: url, + user: user, + password: password, + parseBlocks: parse, + } + chainName, err := s.GetBlockChainInfo() + if err != nil { + return nil, err + } + + // always create parser + s.parser = &BitcoinBlockParser{ + Params: GetChainParams(chainName), + } + + // parameters for getInfo request + if s.parser.Params.Net == wire.MainNet { + s.testnet = false + s.network = "livenet" + } else { + s.testnet = true + s.network = "testnet" + } + + s.mempool = NewMempool(s) + + glog.Info("rpc: block chain ", s.parser.Params.Name) + return s, nil +} + +func (b *BitcoinRPC) IsTestnet() bool { + return b.testnet +} + +func (b *BitcoinRPC) GetNetworkName() string { + return b.network } func (e *RPCError) Error() string { @@ -184,52 +245,6 @@ type resSendRawTransaction struct { Result string `json:"result"` } -// BitcoinRPC is an interface to JSON-RPC bitcoind service. -type BitcoinRPC struct { - client http.Client - URL string - User string - Password string - Parser *BitcoinBlockParser - Testnet bool - Network string -} - -// NewBitcoinRPC returns new BitcoinRPC instance. -func NewBitcoinRPC(url string, user string, password string, timeout time.Duration, parse bool) (*BitcoinRPC, error) { - transport := &http.Transport{ - Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, // necessary to not to deplete ports - } - s := &BitcoinRPC{ - client: http.Client{Timeout: timeout, Transport: transport}, - URL: url, - User: user, - Password: password, - } - chain, err := s.GetBlockChainInfo() - if err != nil { - return nil, err - } - - // always create parser - s.Parser = &BitcoinBlockParser{ - Params: GetChainParams(chain), - } - - // parameters for getInfo request - if s.Parser.Params.Net == wire.MainNet { - s.Testnet = false - s.Network = "livenet" - } else { - s.Testnet = true - s.Network = "testnet" - } - glog.Info("rpc: block chain ", s.Parser.Params.Name) - return s, nil -} - // GetBestBlockHash returns hash of the tip of the best-block-chain. func (b *BitcoinRPC) GetBestBlockHash() (string, error) { @@ -321,7 +336,7 @@ func (b *BitcoinRPC) GetBlockHeader(hash string) (*BlockHeader, error) { // GetBlock returns block with given hash. func (b *BitcoinRPC) GetBlock(hash string) (*Block, error) { - if b.Parser == nil { + if !b.parseBlocks { return b.GetBlockFull(hash) } header, err := b.GetBlockHeader(hash) @@ -332,7 +347,7 @@ func (b *BitcoinRPC) GetBlock(hash string) (*Block, error) { if err != nil { return nil, err } - block, err := b.Parser.ParseBlock(data) + block, err := b.parser.ParseBlock(data) if err != nil { return nil, errors.Annotatef(err, "hash %v", hash) } @@ -343,14 +358,14 @@ func (b *BitcoinRPC) GetBlock(hash string) (*Block, error) { // GetBlockWithoutHeader is an optimization - it does not call GetBlockHeader to get prev, next hashes // instead it sets to header only block hash and height passed in parameters func (b *BitcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*Block, error) { - if b.Parser == nil { + if !b.parseBlocks { return b.GetBlockFull(hash) } data, err := b.GetBlockRaw(hash) if err != nil { return nil, err } - block, err := b.Parser.ParseBlock(data) + block, err := b.parser.ParseBlock(data) if err != nil { return nil, errors.Annotatef(err, "%v %v", height, hash) } @@ -466,6 +481,22 @@ func (b *BitcoinRPC) GetTransaction(txid string) (*Tx, error) { return &res.Result, nil } +// ResyncMempool gets mempool transactions and maps output scripts to transactions. +// ResyncMempool is not reentrant, it should be called from a single thread. +func (b *BitcoinRPC) ResyncMempool(onNewTxAddr func(txid string, addr string)) error { + return b.mempool.Resync(onNewTxAddr) +} + +// GetMempoolTransactions returns slice of mempool transactions for given output script. +func (b *BitcoinRPC) GetMempoolTransactions(outputScript []byte) ([]string, error) { + return b.mempool.GetTransactions(outputScript) +} + +// GetMempoolSpentOutput returns transaction in mempool which spends given outpoint +func (b *BitcoinRPC) GetMempoolSpentOutput(outputTxid string, vout uint32) string { + return b.mempool.GetSpentOutput(outputTxid, vout) +} + // EstimateSmartFee returns fee estimation. func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (float64, error) { glog.V(1).Info("rpc: estimatesmartfee ", blocks) @@ -512,11 +543,11 @@ func (b *BitcoinRPC) call(req interface{}, res interface{}) error { if err != nil { return err } - httpReq, err := http.NewRequest("POST", b.URL, bytes.NewBuffer(httpData)) + httpReq, err := http.NewRequest("POST", b.rpcURL, bytes.NewBuffer(httpData)) if err != nil { return err } - httpReq.SetBasicAuth(b.User, b.Password) + httpReq.SetBasicAuth(b.user, b.password) httpRes, err := b.client.Do(httpReq) // in some cases the httpRes can contain data even if it returns error // see http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/ @@ -531,3 +562,156 @@ func (b *BitcoinRPC) call(req interface{}, res interface{}) error { defer io.Copy(ioutil.Discard, httpRes.Body) return json.NewDecoder(httpRes.Body).Decode(&res) } + +// GetChainParser returns BlockChainParser +func (b *BitcoinRPC) GetChainParser() BlockChainParser { + return b.parser +} + +// bitcoinwire parsing + +type BitcoinBlockParser struct { + Params *chaincfg.Params +} + +// getChainParams contains network parameters for the main Bitcoin network, +// the regression test Bitcoin network, the test Bitcoin network and +// the simulation test Bitcoin network, in this order +func GetChainParams(chain string) *chaincfg.Params { + switch chain { + case "test": + return &chaincfg.TestNet3Params + case "regtest": + return &chaincfg.RegressionNetParams + } + return &chaincfg.MainNetParams +} + +// AddressToOutputScript converts bitcoin address to ScriptPubKey +func (p *BitcoinBlockParser) AddressToOutputScript(address string) ([]byte, error) { + da, err := btcutil.DecodeAddress(address, p.Params) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(da) + if err != nil { + return nil, err + } + return script, nil +} + +// OutputScriptToAddresses converts ScriptPubKey to bitcoin addresses +func (p *BitcoinBlockParser) OutputScriptToAddresses(script []byte) ([]string, error) { + _, addresses, _, err := txscript.ExtractPkScriptAddrs(script, p.Params) + if err != nil { + return nil, err + } + rv := make([]string, len(addresses)) + for i, a := range addresses { + rv[i] = a.EncodeAddress() + } + return rv, nil +} + +func (p *BitcoinBlockParser) txFromMsgTx(t *wire.MsgTx, parseAddresses bool) Tx { + vin := make([]Vin, len(t.TxIn)) + for i, in := range t.TxIn { + if blockchain.IsCoinBaseTx(t) { + vin[i] = Vin{ + Coinbase: hex.EncodeToString(in.SignatureScript), + Sequence: in.Sequence, + } + break + } + s := ScriptSig{ + Hex: hex.EncodeToString(in.SignatureScript), + // missing: Asm, + } + vin[i] = Vin{ + Txid: in.PreviousOutPoint.Hash.String(), + Vout: in.PreviousOutPoint.Index, + Sequence: in.Sequence, + ScriptSig: s, + } + } + vout := make([]Vout, len(t.TxOut)) + for i, out := range t.TxOut { + addrs := []string{} + if parseAddresses { + addrs, _ = p.OutputScriptToAddresses(out.PkScript) + } + s := ScriptPubKey{ + Hex: hex.EncodeToString(out.PkScript), + Addresses: addrs, + // missing: Asm, + // missing: Type, + } + vout[i] = Vout{ + Value: float64(out.Value) / 1E8, + N: uint32(i), + ScriptPubKey: s, + } + } + tx := Tx{ + Txid: t.TxHash().String(), + // skip: Version, + LockTime: t.LockTime, + Vin: vin, + Vout: vout, + // skip: BlockHash, + // skip: Confirmations, + // skip: Time, + // skip: Blocktime, + } + return tx +} + +// ParseTx parses byte array containing transaction and returns Tx struct +func (p *BitcoinBlockParser) ParseTx(b []byte) (*Tx, error) { + t := wire.MsgTx{} + r := bytes.NewReader(b) + if err := t.Deserialize(r); err != nil { + return nil, err + } + tx := p.txFromMsgTx(&t, true) + tx.Hex = hex.EncodeToString(b) + return &tx, nil +} + +// ParseBlock parses raw block to our Block struct +func (p *BitcoinBlockParser) ParseBlock(b []byte) (*Block, error) { + w := wire.MsgBlock{} + r := bytes.NewReader(b) + + if err := w.Deserialize(r); err != nil { + return nil, err + } + + txs := make([]Tx, len(w.Transactions)) + for ti, t := range w.Transactions { + txs[ti] = p.txFromMsgTx(t, false) + } + + return &Block{Txs: txs}, nil +} + +// PackTx packs transaction to byte array +func (p *BitcoinBlockParser) PackTx(tx *Tx, height uint32, blockTime int64) ([]byte, error) { + buf := make([]byte, 4+vlq.MaxLen64+len(tx.Hex)/2) + binary.BigEndian.PutUint32(buf[0:4], height) + vl := vlq.PutInt(buf[4:4+vlq.MaxLen64], blockTime) + hl, err := hex.Decode(buf[4+vl:], []byte(tx.Hex)) + return buf[0 : 4+vl+hl], err +} + +// UnpackTx unpacks transaction from byte array +func (p *BitcoinBlockParser) UnpackTx(buf []byte) (*Tx, uint32, error) { + height := binary.BigEndian.Uint32(buf) + bt, l := vlq.Int(buf[4:]) + tx, err := p.ParseTx(buf[4+l:]) + if err != nil { + return nil, 0, err + } + tx.Blocktime = bt + return tx, height, nil +} diff --git a/db/rocksdb_test.go b/bchain/bitcoinrpc_test.go similarity index 54% rename from db/rocksdb_test.go rename to bchain/bitcoinrpc_test.go index cedf4e33..1ddec9c7 100644 --- a/db/rocksdb_test.go +++ b/bchain/bitcoinrpc_test.go @@ -1,20 +1,122 @@ -package db +package bchain import ( - "blockbook/bchain" "encoding/hex" "reflect" "testing" ) -var testTx1 = bchain.Tx{ +func TestAddressToOutputScript(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PKH", + args: args{address: "1JKgN43B9SyLuZH19H5ECvr4KcfrbVHzZ6"}, + want: "76a914be027bf3eac907bd4ac8cb9c5293b6f37662722088ac", + wantErr: false, + }, + { + name: "P2SH", + args: args{address: "321x69Cb9HZLWwAWGiUBT1U81r1zPLnEjL"}, + want: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87", + wantErr: false, + }, + { + name: "P2WPKH", + args: args{address: "bc1qrsf2l34jvqnq0lduyz0j5pfu2nkd93nnq0qggn"}, + want: "00141c12afc6b2602607fdbc209f2a053c54ecd2c673", + wantErr: false, + }, + { + name: "P2WSH", + args: args{address: "bc1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355sw5exgr"}, + want: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29", + wantErr: false, + }, + } + parser := &BitcoinBlockParser{Params: GetChainParams("main")} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.AddressToOutputScript(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("AddressToOutputScript() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("AddressToOutputScript() = %v, want %v", h, tt.want) + } + }) + } +} + +func TestOutputScriptToAddresses(t *testing.T) { + type args struct { + script string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "P2PKH", + args: args{script: "76a914be027bf3eac907bd4ac8cb9c5293b6f37662722088ac"}, + want: []string{"1JKgN43B9SyLuZH19H5ECvr4KcfrbVHzZ6"}, + wantErr: false, + }, + { + name: "P2SH", + args: args{script: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87"}, + want: []string{"321x69Cb9HZLWwAWGiUBT1U81r1zPLnEjL"}, + wantErr: false, + }, + { + name: "P2WPKH", + args: args{script: "00141c12afc6b2602607fdbc209f2a053c54ecd2c673"}, + want: []string{"bc1qrsf2l34jvqnq0lduyz0j5pfu2nkd93nnq0qggn"}, + wantErr: false, + }, + { + name: "P2WSH", + args: args{script: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29"}, + want: []string{"bc1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355sw5exgr"}, + wantErr: false, + }, + } + parser := &BitcoinBlockParser{Params: GetChainParams("main")} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.script) + got, err := parser.OutputScriptToAddresses(b) + if (err != nil) != tt.wantErr { + t.Errorf("OutputScriptToAddresses() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("OutputScriptToAddresses() = %v, want %v", got, tt.want) + } + }) + } +} + +var testTx1 = Tx{ Hex: "01000000017f9a22c9cbf54bd902400df746f138f37bcf5b4d93eb755820e974ba43ed5f42040000006a4730440220037f4ed5427cde81d55b9b6a2fd08c8a25090c2c2fff3a75c1a57625ca8a7118022076c702fe55969fa08137f71afd4851c48e31082dd3c40c919c92cdbc826758d30121029f6da5623c9f9b68a9baf9c1bc7511df88fa34c6c2f71f7c62f2f03ff48dca80feffffff019c9700000000000017a9146144d57c8aff48492c9dfb914e120b20bad72d6f8773d00700", Blocktime: 1519053802, Txid: "056e3d82e5ffd0e915fb9b62797d76263508c34fe3e5dbed30dd3e943930f204", LockTime: 512115, - Vin: []bchain.Vin{ + Vin: []Vin{ { - ScriptSig: bchain.ScriptSig{ + ScriptSig: ScriptSig{ Hex: "4730440220037f4ed5427cde81d55b9b6a2fd08c8a25090c2c2fff3a75c1a57625ca8a7118022076c702fe55969fa08137f71afd4851c48e31082dd3c40c919c92cdbc826758d30121029f6da5623c9f9b68a9baf9c1bc7511df88fa34c6c2f71f7c62f2f03ff48dca80", }, Txid: "425fed43ba74e9205875eb934d5bcf7bf338f146f70d4002d94bf5cbc9229a7f", @@ -22,11 +124,11 @@ var testTx1 = bchain.Tx{ Sequence: 4294967294, }, }, - Vout: []bchain.Vout{ + Vout: []Vout{ { Value: 0.00038812, N: 0, - ScriptPubKey: bchain.ScriptPubKey{ + ScriptPubKey: ScriptPubKey{ Hex: "a9146144d57c8aff48492c9dfb914e120b20bad72d6f87", Addresses: []string{ "3AZKvpKhSh1o8t1QrX3UeXG9d2BhCRnbcK", @@ -37,14 +139,14 @@ var testTx1 = bchain.Tx{ } var testTxPacked1 = "0001e2408ba8d7af5401000000017f9a22c9cbf54bd902400df746f138f37bcf5b4d93eb755820e974ba43ed5f42040000006a4730440220037f4ed5427cde81d55b9b6a2fd08c8a25090c2c2fff3a75c1a57625ca8a7118022076c702fe55969fa08137f71afd4851c48e31082dd3c40c919c92cdbc826758d30121029f6da5623c9f9b68a9baf9c1bc7511df88fa34c6c2f71f7c62f2f03ff48dca80feffffff019c9700000000000017a9146144d57c8aff48492c9dfb914e120b20bad72d6f8773d00700" -var testTx2 = bchain.Tx{ +var testTx2 = Tx{ Hex: "010000000001019d64f0c72a0d206001decbffaa722eb1044534c74eee7a5df8318e42a4323ec10000000017160014550da1f5d25a9dae2eafd6902b4194c4c6500af6ffffffff02809698000000000017a914cd668d781ece600efa4b2404dc91fd26b8b8aed8870553d7360000000017a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a8702473044022076aba4ad559616905fa51d4ddd357fc1fdb428d40cb388e042cdd1da4a1b7357022011916f90c712ead9a66d5f058252efd280439ad8956a967e95d437d246710bc9012102a80a5964c5612bb769ef73147b2cf3c149bc0fd4ecb02f8097629c94ab013ffd00000000", Blocktime: 1235678901, Txid: "474e6795760ebe81cb4023dc227e5a0efe340e1771c89a0035276361ed733de7", LockTime: 0, - Vin: []bchain.Vin{ + Vin: []Vin{ { - ScriptSig: bchain.ScriptSig{ + ScriptSig: ScriptSig{ Hex: "160014550da1f5d25a9dae2eafd6902b4194c4c6500af6", }, Txid: "c13e32a4428e31f85d7aee4ec7344504b12e72aaffcbde0160200d2ac7f0649d", @@ -52,11 +154,11 @@ var testTx2 = bchain.Tx{ Sequence: 4294967295, }, }, - Vout: []bchain.Vout{ + Vout: []Vout{ { Value: .1, N: 0, - ScriptPubKey: bchain.ScriptPubKey{ + ScriptPubKey: ScriptPubKey{ Hex: "a914cd668d781ece600efa4b2404dc91fd26b8b8aed887", Addresses: []string{ "2NByHN6A8QYkBATzxf4pRGbCSHD5CEN2TRu", @@ -66,7 +168,7 @@ var testTx2 = bchain.Tx{ { Value: 9.20081157, N: 1, - ScriptPubKey: bchain.ScriptPubKey{ + ScriptPubKey: ScriptPubKey{ Hex: "a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a87", Addresses: []string{ "2MvZguYaGjM7JihBgNqgLF2Ca2Enb76Hj9D", @@ -77,11 +179,12 @@ var testTx2 = bchain.Tx{ } var testTxPacked2 = "0007c91a899ab7da6a010000000001019d64f0c72a0d206001decbffaa722eb1044534c74eee7a5df8318e42a4323ec10000000017160014550da1f5d25a9dae2eafd6902b4194c4c6500af6ffffffff02809698000000000017a914cd668d781ece600efa4b2404dc91fd26b8b8aed8870553d7360000000017a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a8702473044022076aba4ad559616905fa51d4ddd357fc1fdb428d40cb388e042cdd1da4a1b7357022011916f90c712ead9a66d5f058252efd280439ad8956a967e95d437d246710bc9012102a80a5964c5612bb769ef73147b2cf3c149bc0fd4ecb02f8097629c94ab013ffd00000000" -func Test_packTx(t *testing.T) { +func Test_PackTx(t *testing.T) { type args struct { - tx bchain.Tx + tx Tx height uint32 blockTime int64 + parser *BitcoinBlockParser } tests := []struct { name string @@ -90,21 +193,31 @@ func Test_packTx(t *testing.T) { wantErr bool }{ { - name: "btc-1", - args: args{testTx1, 123456, 1519053802}, + name: "btc-1", + args: args{ + tx: testTx1, + height: 123456, + blockTime: 1519053802, + parser: &BitcoinBlockParser{Params: GetChainParams("main")}, + }, want: testTxPacked1, wantErr: false, }, { - name: "testnet-1", - args: args{testTx2, 510234, 1235678901}, + name: "testnet-1", + args: args{ + tx: testTx2, + height: 510234, + blockTime: 1235678901, + parser: &BitcoinBlockParser{Params: GetChainParams("test")}, + }, want: testTxPacked2, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := packTx(&tt.args.tx, tt.args.height, tt.args.blockTime) + got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) if (err != nil) != tt.wantErr { t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) return @@ -117,15 +230,15 @@ func Test_packTx(t *testing.T) { } } -func Test_unpackTx(t *testing.T) { +func Test_UnpackTx(t *testing.T) { type args struct { packedTx string - parser *bchain.BitcoinBlockParser + parser *BitcoinBlockParser } tests := []struct { name string args args - want *bchain.Tx + want *Tx want1 uint32 wantErr bool }{ @@ -133,7 +246,7 @@ func Test_unpackTx(t *testing.T) { name: "btc-1", args: args{ packedTx: testTxPacked1, - parser: &bchain.BitcoinBlockParser{Params: bchain.GetChainParams("main")}, + parser: &BitcoinBlockParser{Params: GetChainParams("main")}, }, want: &testTx1, want1: 123456, @@ -143,7 +256,7 @@ func Test_unpackTx(t *testing.T) { name: "testnet-1", args: args{ packedTx: testTxPacked2, - parser: &bchain.BitcoinBlockParser{Params: bchain.GetChainParams("test")}, + parser: &BitcoinBlockParser{Params: GetChainParams("test")}, }, want: &testTx2, want1: 510234, @@ -153,7 +266,7 @@ func Test_unpackTx(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b, _ := hex.DecodeString(tt.args.packedTx) - got, got1, err := unpackTx(b, tt.args.parser) + got, got1, err := tt.args.parser.UnpackTx(b) if (err != nil) != tt.wantErr { t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/bchain/bitcoinwire.go b/bchain/bitcoinwire.go deleted file mode 100644 index baf960b2..00000000 --- a/bchain/bitcoinwire.go +++ /dev/null @@ -1,138 +0,0 @@ -package bchain - -import ( - "bytes" - "encoding/hex" - - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcutil" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" -) - -// GetChainParams contains network parameters for the main Bitcoin network, -// the regression test Bitcoin network, the test Bitcoin network and -// the simulation test Bitcoin network, in this order -func GetChainParams(chain string) *chaincfg.Params { - switch chain { - case "test": - return &chaincfg.TestNet3Params - case "regtest": - return &chaincfg.RegressionNetParams - } - return &chaincfg.MainNetParams -} - -type BitcoinBlockParser struct { - Params *chaincfg.Params -} - -// AddressToOutputScript converts bitcoin address to ScriptPubKey -func (p *BitcoinBlockParser) AddressToOutputScript(address string) ([]byte, error) { - da, err := btcutil.DecodeAddress(address, p.Params) - if err != nil { - return nil, err - } - script, err := txscript.PayToAddrScript(da) - if err != nil { - return nil, err - } - return script, nil -} - -// OutputScriptToAddresses converts ScriptPubKey to bitcoin addresses -func (p *BitcoinBlockParser) OutputScriptToAddresses(script []byte) ([]string, error) { - _, addresses, _, err := txscript.ExtractPkScriptAddrs(script, p.Params) - if err != nil { - return nil, err - } - rv := make([]string, len(addresses)) - for i, a := range addresses { - rv[i] = a.EncodeAddress() - } - return rv, nil -} - -// ParseTx parses byte array containing transaction and returns Tx struct -func (p *BitcoinBlockParser) ParseTx(b []byte) (*Tx, error) { - t := wire.MsgTx{} - r := bytes.NewReader(b) - if err := t.Deserialize(r); err != nil { - return nil, err - } - tx := p.txFromMsgTx(&t, true) - tx.Hex = hex.EncodeToString(b) - return &tx, nil -} - -func (p *BitcoinBlockParser) txFromMsgTx(t *wire.MsgTx, parseAddresses bool) Tx { - vin := make([]Vin, len(t.TxIn)) - for i, in := range t.TxIn { - if blockchain.IsCoinBaseTx(t) { - vin[i] = Vin{ - Coinbase: hex.EncodeToString(in.SignatureScript), - Sequence: in.Sequence, - } - break - } - s := ScriptSig{ - Hex: hex.EncodeToString(in.SignatureScript), - // missing: Asm, - } - vin[i] = Vin{ - Txid: in.PreviousOutPoint.Hash.String(), - Vout: in.PreviousOutPoint.Index, - Sequence: in.Sequence, - ScriptSig: s, - } - } - vout := make([]Vout, len(t.TxOut)) - for i, out := range t.TxOut { - addrs := []string{} - if parseAddresses { - addrs, _ = p.OutputScriptToAddresses(out.PkScript) - } - s := ScriptPubKey{ - Hex: hex.EncodeToString(out.PkScript), - Addresses: addrs, - // missing: Asm, - // missing: Type, - } - vout[i] = Vout{ - Value: float64(out.Value) / 1E8, - N: uint32(i), - ScriptPubKey: s, - } - } - tx := Tx{ - Txid: t.TxHash().String(), - // skip: Version, - LockTime: t.LockTime, - Vin: vin, - Vout: vout, - // skip: BlockHash, - // skip: Confirmations, - // skip: Time, - // skip: Blocktime, - } - return tx -} - -// ParseBlock parses raw block to our Block struct -func (p *BitcoinBlockParser) ParseBlock(b []byte) (*Block, error) { - w := wire.MsgBlock{} - r := bytes.NewReader(b) - - if err := w.Deserialize(r); err != nil { - return nil, err - } - - txs := make([]Tx, len(w.Transactions)) - for ti, t := range w.Transactions { - txs[ti] = p.txFromMsgTx(t, false) - } - - return &Block{Txs: txs}, nil -} diff --git a/bchain/bitcoinwire_test.go b/bchain/bitcoinwire_test.go deleted file mode 100644 index 0710c99d..00000000 --- a/bchain/bitcoinwire_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package bchain - -import ( - "encoding/hex" - "reflect" - "testing" -) - -func TestAddressToOutputScript(t *testing.T) { - type args struct { - address string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "P2PKH", - args: args{address: "1JKgN43B9SyLuZH19H5ECvr4KcfrbVHzZ6"}, - want: "76a914be027bf3eac907bd4ac8cb9c5293b6f37662722088ac", - wantErr: false, - }, - { - name: "P2SH", - args: args{address: "321x69Cb9HZLWwAWGiUBT1U81r1zPLnEjL"}, - want: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87", - wantErr: false, - }, - { - name: "P2WPKH", - args: args{address: "bc1qrsf2l34jvqnq0lduyz0j5pfu2nkd93nnq0qggn"}, - want: "00141c12afc6b2602607fdbc209f2a053c54ecd2c673", - wantErr: false, - }, - { - name: "P2WSH", - args: args{address: "bc1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355sw5exgr"}, - want: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29", - wantErr: false, - }, - } - parser := &BitcoinBlockParser{Params: GetChainParams("main")} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parser.AddressToOutputScript(tt.args.address) - if (err != nil) != tt.wantErr { - t.Errorf("AddressToOutputScript() error = %v, wantErr %v", err, tt.wantErr) - return - } - h := hex.EncodeToString(got) - if !reflect.DeepEqual(h, tt.want) { - t.Errorf("AddressToOutputScript() = %v, want %v", h, tt.want) - } - }) - } -} - -func TestOutputScriptToAddresses(t *testing.T) { - type args struct { - script string - } - tests := []struct { - name string - args args - want []string - wantErr bool - }{ - { - name: "P2PKH", - args: args{script: "76a914be027bf3eac907bd4ac8cb9c5293b6f37662722088ac"}, - want: []string{"1JKgN43B9SyLuZH19H5ECvr4KcfrbVHzZ6"}, - wantErr: false, - }, - { - name: "P2SH", - args: args{script: "a9140394b3cf9a44782c10105b93962daa8dba304d7f87"}, - want: []string{"321x69Cb9HZLWwAWGiUBT1U81r1zPLnEjL"}, - wantErr: false, - }, - { - name: "P2WPKH", - args: args{script: "00141c12afc6b2602607fdbc209f2a053c54ecd2c673"}, - want: []string{"bc1qrsf2l34jvqnq0lduyz0j5pfu2nkd93nnq0qggn"}, - wantErr: false, - }, - { - name: "P2WSH", - args: args{script: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29"}, - want: []string{"bc1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355sw5exgr"}, - wantErr: false, - }, - } - parser := &BitcoinBlockParser{Params: GetChainParams("main")} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b, _ := hex.DecodeString(tt.args.script) - got, err := parser.OutputScriptToAddresses(b) - if (err != nil) != tt.wantErr { - t.Errorf("OutputScriptToAddresses() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("OutputScriptToAddresses() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/bchain/mempool.go b/bchain/mempool.go index 6dd93f6f..4037ff29 100644 --- a/bchain/mempool.go +++ b/bchain/mempool.go @@ -25,7 +25,7 @@ type inputOutput struct { // Mempool is mempool handle. type Mempool struct { - chain *BitcoinRPC + chain BlockChain mux sync.Mutex txToInputOutput map[string]inputOutput scriptToTx map[string][]outpoint @@ -33,7 +33,7 @@ type Mempool struct { } // NewMempool creates new mempool handler. -func NewMempool(chain *BitcoinRPC) *Mempool { +func NewMempool(chain BlockChain) *Mempool { return &Mempool{chain: chain} } @@ -54,8 +54,8 @@ func (m *Mempool) GetTransactions(outputScript []byte) ([]string, error) { return txs, nil } -// GetInput returns transaction which spends given outpoint -func (m *Mempool) GetInput(outputTxid string, vout uint32) string { +// GetSpentOutput returns transaction which spends given outpoint +func (m *Mempool) GetSpentOutput(outputTxid string, vout uint32) string { o := outpoint{txid: outputTxid, vout: vout} return m.inputs[o] } diff --git a/bchain/types.go b/bchain/types.go index cf7b8057..0e1d9947 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -58,3 +58,40 @@ type BlockHeader struct { Height uint32 `json:"height"` Confirmations int `json:"confirmations"` } + +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type BlockChain interface { + // chain info + IsTestnet() bool + GetNetworkName() string + // requests + GetBestBlockHash() (string, error) + GetBestBlockHeight() (uint32, error) + GetBlockHash(height uint32) (string, error) + GetBlockHeader(hash string) (*BlockHeader, error) + GetBlock(hash string) (*Block, error) + GetBlockWithoutHeader(hash string, height uint32) (*Block, error) + GetMempool() ([]string, error) + GetTransaction(txid string) (*Tx, error) + EstimateSmartFee(blocks int, conservative bool) (float64, error) + SendRawTransaction(tx string) (string, error) + // mempool + ResyncMempool(onNewTxAddr func(txid string, addr string)) error + GetMempoolTransactions(outputScript []byte) ([]string, error) + GetMempoolSpentOutput(outputTxid string, vout uint32) string + // parser + GetChainParser() BlockChainParser +} + +type BlockChainParser interface { + AddressToOutputScript(address string) ([]byte, error) + OutputScriptToAddresses(script []byte) ([]string, error) + ParseTx(b []byte) (*Tx, error) + ParseBlock(b []byte) (*Block, error) + PackTx(tx *Tx, height uint32, blockTime int64) ([]byte, error) + UnpackTx(buf []byte) (*Tx, uint32, error) +} diff --git a/blockbook.go b/blockbook.go index 1f697a0f..57140b41 100644 --- a/blockbook.go +++ b/blockbook.go @@ -70,8 +70,7 @@ var ( chanSyncMempool = make(chan struct{}) chanSyncIndexDone = make(chan struct{}) chanSyncMempoolDone = make(chan struct{}) - chain *bchain.BitcoinRPC - mempool *bchain.Mempool + chain bchain.BlockChain index *db.RocksDB txCache *db.TxCache syncWorker *db.SyncWorker @@ -107,9 +106,7 @@ func main() { glog.Fatal("NewBitcoinRPC ", err) } - mempool = bchain.NewMempool(chain) - - index, err = db.NewRocksDB(*dbPath, chain.Parser) + index, err = db.NewRocksDB(*dbPath, chain.GetChainParser()) if err != nil { glog.Fatalf("NewRocksDB %v", err) } @@ -143,8 +140,8 @@ func main() { glog.Error("resyncIndex ", err) return } - if err = mempool.Resync(nil); err != nil { - glog.Error("resyncIndex ", err) + if err = chain.ResyncMempool(nil); err != nil { + glog.Error("resyncMempool ", err) return } } @@ -156,7 +153,7 @@ func main() { var httpServer *server.HTTPServer if *httpServerBinding != "" { - httpServer, err = server.NewHTTPServer(*httpServerBinding, *certFiles, index, mempool, chain, txCache) + httpServer, err = server.NewHTTPServer(*httpServerBinding, *certFiles, index, chain, txCache) if err != nil { glog.Error("https: ", err) return @@ -176,7 +173,7 @@ func main() { var socketIoServer *server.SocketIoServer if *socketIoBinding != "" { - socketIoServer, err = server.NewSocketIoServer(*socketIoBinding, *certFiles, index, mempool, chain, txCache, *explorerURL) + socketIoServer, err = server.NewSocketIoServer(*socketIoBinding, *certFiles, index, chain, txCache, *explorerURL) if err != nil { glog.Error("socketio: ", err) return @@ -224,7 +221,7 @@ func main() { address := *queryAddress if address != "" { - script, err := chain.Parser.AddressToOutputScript(address) + script, err := chain.GetChainParser().AddressToOutputScript(address) if err != nil { glog.Error("GetTransactions ", err) return @@ -299,7 +296,7 @@ func syncMempoolLoop() { glog.Info("syncMempoolLoop starting") // resync mempool about every minute if there are no chanSyncMempool requests, with debounce 1 second tickAndDebounce(resyncMempoolPeriodMs*time.Millisecond, debounceResyncMempoolMs*time.Millisecond, chanSyncMempool, func() { - if err := mempool.Resync(onNewTxAddr); err != nil { + if err := chain.ResyncMempool(onNewTxAddr); err != nil { glog.Error("syncMempoolLoop ", errors.ErrorStack(err)) } }) @@ -313,6 +310,7 @@ func onNewTxAddr(txid string, addr string) { } func mqHandler(m *bchain.MQMessage) { + // TODO - is coin specific, item for abstraction body := hex.EncodeToString(m.Body) glog.V(1).Infof("MQ: %s-%d %s", m.Topic, m.Sequence, body) if m.Topic == "hashblock" { diff --git a/db/rocksdb.go b/db/rocksdb.go index de53549a..d5b9360a 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -34,7 +34,7 @@ type RocksDB struct { wo *gorocksdb.WriteOptions ro *gorocksdb.ReadOptions cfh []*gorocksdb.ColumnFamilyHandle - chainParser *bchain.BitcoinBlockParser + chainParser bchain.BlockChainParser } const ( @@ -94,7 +94,7 @@ func openDB(path string) (*gorocksdb.DB, []*gorocksdb.ColumnFamilyHandle, error) // NewRocksDB opens an internal handle to RocksDB environment. Close // needs to be called to release it. -func NewRocksDB(path string, parser *bchain.BitcoinBlockParser) (d *RocksDB, err error) { +func NewRocksDB(path string, parser bchain.BlockChainParser) (d *RocksDB, err error) { glog.Infof("rocksdb: open %s", path) db, cfh, err := openDB(path) wo := gorocksdb.NewDefaultWriteOptions() @@ -542,7 +542,7 @@ func (d *RocksDB) GetTx(txid string) (*bchain.Tx, uint32, error) { defer val.Free() data := val.Data() if len(data) > 4 { - return unpackTx(data, d.chainParser) + return d.chainParser.UnpackTx(data) } return nil, 0, nil } @@ -553,7 +553,7 @@ func (d *RocksDB) PutTx(tx *bchain.Tx, height uint32, blockTime int64) error { if err != nil { return nil } - buf, err := packTx(tx, height, blockTime) + buf, err := d.chainParser.PackTx(tx, height, blockTime) if err != nil { return err } @@ -571,6 +571,7 @@ func (d *RocksDB) DeleteTx(txid string) error { // Helpers +// TODO - this may be coin specific, refactor const txIdUnpackedLen = 32 var ErrInvalidAddress = errors.New("invalid address") @@ -640,23 +641,3 @@ func packOutputScript(script string) ([]byte, error) { func unpackOutputScript(buf []byte) string { return hex.EncodeToString(buf) } - -func packTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { - bt := packVarint64(blockTime) - buf := make([]byte, 4+len(bt)+len(tx.Hex)/2) - binary.BigEndian.PutUint32(buf[0:4], height) - copy(buf[4:], bt) - _, err := hex.Decode(buf[4+len(bt):], []byte(tx.Hex)) - return buf, err -} - -func unpackTx(buf []byte, parser *bchain.BitcoinBlockParser) (*bchain.Tx, uint32, error) { - height := unpackUint(buf) - bt, l := unpackVarint64(buf[4:]) - tx, err := parser.ParseTx(buf[4+l:]) - if err != nil { - return nil, 0, err - } - tx.Blocktime = bt - return tx, height, nil -} diff --git a/db/sync.go b/db/sync.go index 16bc0a64..733c7042 100644 --- a/db/sync.go +++ b/db/sync.go @@ -14,7 +14,7 @@ import ( // SyncWorker is handle to SyncWorker type SyncWorker struct { db *RocksDB - chain *bchain.BitcoinRPC + chain bchain.BlockChain syncWorkers, syncChunk int dryRun bool startHeight uint32 @@ -22,7 +22,7 @@ type SyncWorker struct { } // NewSyncWorker creates new SyncWorker and returns its handle -func NewSyncWorker(db *RocksDB, chain *bchain.BitcoinRPC, syncWorkers, syncChunk int, minStartHeight int, dryRun bool, chanOsSignal chan os.Signal) (*SyncWorker, error) { +func NewSyncWorker(db *RocksDB, chain bchain.BlockChain, syncWorkers, syncChunk int, minStartHeight int, dryRun bool, chanOsSignal chan os.Signal) (*SyncWorker, error) { if minStartHeight < 0 { minStartHeight = 0 } diff --git a/db/txcache.go b/db/txcache.go index 3ac99d59..656e7699 100644 --- a/db/txcache.go +++ b/db/txcache.go @@ -9,11 +9,11 @@ import ( // TxCache is handle to TxCacheServer type TxCache struct { db *RocksDB - chain *bchain.BitcoinRPC + chain bchain.BlockChain } // NewTxCache creates new TxCache interface and returns its handle -func NewTxCache(db *RocksDB, chain *bchain.BitcoinRPC) (*TxCache, error) { +func NewTxCache(db *RocksDB, chain bchain.BlockChain) (*TxCache, error) { return &TxCache{ db: db, chain: chain, diff --git a/server/https.go b/server/https.go index c0e67a29..50002f90 100644 --- a/server/https.go +++ b/server/https.go @@ -18,26 +18,26 @@ import ( // HTTPServer is handle to HttpServer type HTTPServer struct { - https *http.Server - certFiles string - db *db.RocksDB - txCache *db.TxCache - mempool *bchain.Mempool - chain *bchain.BitcoinRPC + https *http.Server + certFiles string + db *db.RocksDB + txCache *db.TxCache + chain bchain.BlockChain + chainParser bchain.BlockChainParser } // NewHTTPServer creates new REST interface to blockbook and returns its handle -func NewHTTPServer(httpServerBinding string, certFiles string, db *db.RocksDB, mempool *bchain.Mempool, chain *bchain.BitcoinRPC, txCache *db.TxCache) (*HTTPServer, error) { +func NewHTTPServer(httpServerBinding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache) (*HTTPServer, error) { https := &http.Server{ Addr: httpServerBinding, } s := &HTTPServer{ - https: https, - certFiles: certFiles, - db: db, - txCache: txCache, - mempool: mempool, - chain: chain, + https: https, + certFiles: certFiles, + db: db, + txCache: txCache, + chain: chain, + chainParser: chain.GetChainParser(), } r := mux.NewRouter() @@ -135,7 +135,7 @@ func (s *HTTPServer) blockHash(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) getAddress(r *http.Request) (address string, script []byte, err error) { address = mux.Vars(r)["address"] - script, err = s.chain.Parser.AddressToOutputScript(address) + script, err = s.chainParser.AddressToOutputScript(address) return } @@ -164,7 +164,7 @@ func (s *HTTPServer) unconfirmedTransactions(w http.ResponseWriter, r *http.Requ if err != nil { respondError(w, err, fmt.Sprint("unconfirmedTransactions for address", address)) } - txs, err := s.mempool.GetTransactions(script) + txs, err := s.chain.GetMempoolTransactions(script) if err != nil { respondError(w, err, fmt.Sprint("unconfirmedTransactions for address", address)) } @@ -197,7 +197,7 @@ func (s *HTTPServer) transactions(w http.ResponseWriter, r *http.Request) { err = s.db.GetTransactions(script, lower, higher, func(txid string, vout uint32, isOutput bool) error { txList.Txid = append(txList.Txid, txid) if isOutput { - input := s.mempool.GetInput(txid, vout) + input := s.chain.GetMempoolSpentOutput(txid, vout) if input != "" { txList.Txid = append(txList.Txid, txid) } @@ -207,7 +207,7 @@ func (s *HTTPServer) transactions(w http.ResponseWriter, r *http.Request) { if err != nil { respondError(w, err, fmt.Sprint("transactions for address", address)) } - txs, err := s.mempool.GetTransactions(script) + txs, err := s.chain.GetMempoolTransactions(script) if err != nil { respondError(w, err, fmt.Sprint("transactions for address", address)) } diff --git a/server/socketio.go b/server/socketio.go index 0527eafa..6d93cc6a 100644 --- a/server/socketio.go +++ b/server/socketio.go @@ -24,13 +24,13 @@ type SocketIoServer struct { https *http.Server db *db.RocksDB txCache *db.TxCache - mempool *bchain.Mempool - chain *bchain.BitcoinRPC + chain bchain.BlockChain + chainParser bchain.BlockChainParser explorerURL string } // NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle -func NewSocketIoServer(binding string, certFiles string, db *db.RocksDB, mempool *bchain.Mempool, chain *bchain.BitcoinRPC, txCache *db.TxCache, explorerURL string) (*SocketIoServer, error) { +func NewSocketIoServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, explorerURL string) (*SocketIoServer, error) { server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport()) server.On(gosocketio.OnConnection, func(c *gosocketio.Channel) { @@ -64,8 +64,8 @@ func NewSocketIoServer(binding string, certFiles string, db *db.RocksDB, mempool server: server, db: db, txCache: txCache, - mempool: mempool, chain: chain, + chainParser: chain.GetChainParser(), explorerURL: explorerURL, } @@ -241,7 +241,7 @@ func (s *SocketIoServer) getAddressTxids(addr []string, rr *reqRange) (res resul txids := make([]string, 0) lower, higher := uint32(rr.To), uint32(rr.Start) for _, address := range addr { - script, err := s.chain.Parser.AddressToOutputScript(address) + script, err := s.chainParser.AddressToOutputScript(address) if err != nil { return res, err } @@ -249,7 +249,7 @@ func (s *SocketIoServer) getAddressTxids(addr []string, rr *reqRange) (res resul err = s.db.GetTransactions(script, lower, higher, func(txid string, vout uint32, isOutput bool) error { txids = append(txids, txid) if isOutput && rr.QueryMempol { - input := s.mempool.GetInput(txid, vout) + input := s.chain.GetMempoolSpentOutput(txid, vout) if input != "" { txids = append(txids, txid) } @@ -261,7 +261,7 @@ func (s *SocketIoServer) getAddressTxids(addr []string, rr *reqRange) (res resul } } if rr.QueryMempoolOnly || rr.QueryMempol { - mtxids, err := s.mempool.GetTransactions(script) + mtxids, err := s.chain.GetMempoolTransactions(script) if err != nil { return res, err } @@ -580,8 +580,8 @@ func (s *SocketIoServer) getInfo() (res resultGetInfo, err error) { return } res.Result.Blocks = int(height) - res.Result.Testnet = s.chain.Testnet - res.Result.Network = s.chain.Network + res.Result.Testnet = s.chain.IsTestnet() + res.Result.Network = s.chain.GetNetworkName() return }