diff --git a/bchain/coins/btc/bitcoinlikeparser.go b/bchain/coins/btc/bitcoinlikeparser.go index 67a31d0c..ab718c2b 100644 --- a/bchain/coins/btc/bitcoinlikeparser.go +++ b/bchain/coins/btc/bitcoinlikeparser.go @@ -297,6 +297,7 @@ func (p *BitcoinLikeParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: w.Header.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: w.Header.Timestamp.Unix(), }, diff --git a/bchain/coins/btc/bitcoinrpc_integration_test.go b/bchain/coins/btc/bitcoinrpc_integration_test.go new file mode 100644 index 00000000..556183ad --- /dev/null +++ b/bchain/coins/btc/bitcoinrpc_integration_test.go @@ -0,0 +1,111 @@ +//go:build integration + +package btc + +import ( + "encoding/json" + "testing" + + "github.com/trezor/blockbook/bchain" +) + +const blockHeightLag = 100 + +func newTestBitcoinRPC(t *testing.T) *BitcoinRPC { + t.Helper() + + cfg := bchain.LoadBlockchainCfg(t, "bitcoin") + config := Configuration{ + RPCURL: cfg.RpcUrl, + RPCUser: cfg.RpcUser, + RPCPass: cfg.RpcPass, + RPCTimeout: cfg.RpcTimeout, + Parse: cfg.Parse, + } + raw, err := json.Marshal(config) + if err != nil { + t.Fatalf("marshal config: %v", err) + } + chain, err := NewBitcoinRPC(raw, nil) + if err != nil { + t.Fatalf("new bitcoin rpc: %v", err) + } + rpcClient, ok := chain.(*BitcoinRPC) + if !ok { + t.Fatalf("unexpected rpc client type %T", chain) + } + if err := rpcClient.Initialize(); err != nil { + t.Skipf("skipping: cannot connect to RPC at %s: %v", cfg.RpcUrl, err) + return nil + } + return rpcClient +} + +func assertBlockBasics(t *testing.T, block *bchain.Block, hash string, height uint32) { + t.Helper() + if block.Hash != hash { + t.Fatalf("hash mismatch: got %s want %s", block.Hash, hash) + } + if block.Height != height { + t.Fatalf("height mismatch: got %d want %d", block.Height, height) + } + if block.Time <= 0 { + t.Fatalf("expected block time > 0, got %d", block.Time) + } +} + +// TestBitcoinRPCGetBlockIntegration validates GetBlock by hash/height and checks +// previous hash availability for fork detection. +func TestBitcoinRPCGetBlockIntegration(t *testing.T) { + rpcClient := newTestBitcoinRPC(t) + if rpcClient == nil { + return + } + + best, err := rpcClient.GetBestBlockHeight() + if err != nil { + t.Fatalf("GetBestBlockHeight: %v", err) + } + if best <= blockHeightLag { + t.Skipf("best height %d too low for lag %d", best, blockHeightLag) + return + } + height := best - blockHeightLag + if height == 0 { + t.Skip("block height is zero, cannot validate previous hash") + return + } + + hash, err := rpcClient.GetBlockHash(height) + if err != nil { + t.Fatalf("GetBlockHash height %d: %v", height, err) + } + prevHash, err := rpcClient.GetBlockHash(height - 1) + if err != nil { + t.Fatalf("GetBlockHash height %d: %v", height-1, err) + } + + blockByHash, err := rpcClient.GetBlock(hash, 0) + if err != nil { + t.Fatalf("GetBlock by hash: %v", err) + } + assertBlockBasics(t, blockByHash, hash, height) + if blockByHash.Confirmations <= 0 { + t.Fatalf("expected confirmations > 0, got %d", blockByHash.Confirmations) + } + if blockByHash.Prev != prevHash { + t.Fatalf("previous hash mismatch: got %s want %s", blockByHash.Prev, prevHash) + } + + blockByHeight, err := rpcClient.GetBlock("", height) + if err != nil { + t.Fatalf("GetBlock by height: %v", err) + } + assertBlockBasics(t, blockByHeight, hash, height) + if blockByHeight.Prev != prevHash { + t.Fatalf("previous hash mismatch by height: got %s want %s", blockByHeight.Prev, prevHash) + } + if len(blockByHeight.Txs) != len(blockByHash.Txs) { + t.Fatalf("tx count mismatch: by hash %d vs by height %d", len(blockByHash.Txs), len(blockByHeight.Txs)) + } +} diff --git a/bchain/coins/btg/bgoldparser.go b/bchain/coins/btg/bgoldparser.go index aca095e0..8055c7d6 100644 --- a/bchain/coins/btg/bgoldparser.go +++ b/bchain/coins/btg/bgoldparser.go @@ -83,12 +83,18 @@ func GetChainParams(chain string) *chaincfg.Params { // headerFixedLength is the length of fixed fields of a block (i.e. without solution) // see https://github.com/BTCGPU/BTCGPU/wiki/Technical-Spec#block-header const headerFixedLength = 44 + (chainhash.HashSize * 3) +const prevHashOffset = 4 const timestampOffset = 100 const timestampLength = 4 // ParseBlock parses raw block to our Block struct func (p *BGoldParser) ParseBlock(b []byte) (*bchain.Block, error) { r := bytes.NewReader(b) + prev, err := readPrevBlockHash(r) + if err != nil { + return nil, err + } + time, err := getTimestampAndSkipHeader(r, 0) if err != nil { return nil, err @@ -107,6 +113,7 @@ func (p *BGoldParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: prev, // needed for fork detection when parsing raw blocks Size: len(b), Time: time, }, @@ -114,6 +121,21 @@ func (p *BGoldParser) ParseBlock(b []byte) (*bchain.Block, error) { }, nil } +func readPrevBlockHash(r io.ReadSeeker) (string, error) { + // Read prev hash directly so fork detection still works with raw parsing. + if _, err := r.Seek(prevHashOffset, io.SeekStart); err != nil { + // Return the seek error when the header layout can't be accessed. + return "", err + } + var prevHash chainhash.Hash + if _, err := io.ReadFull(r, prevHash[:]); err != nil { + // Return read errors for truncated or malformed headers. + return "", err + } + // Return the canonical display string for comparison in sync logic. + return prevHash.String(), nil +} + func getTimestampAndSkipHeader(r io.ReadSeeker, pver uint32) (int64, error) { _, err := r.Seek(timestampOffset, io.SeekStart) if err != nil { diff --git a/bchain/coins/btg/bgoldparser_test.go b/bchain/coins/btg/bgoldparser_test.go index 01922e2a..0a8f430a 100644 --- a/bchain/coins/btg/bgoldparser_test.go +++ b/bchain/coins/btg/bgoldparser_test.go @@ -25,12 +25,14 @@ type testBlock struct { size int time int64 txs []string + prev string } var testParseBlockTxs = map[int]testBlock{ 104000: { size: 15776, time: 1295705889, + prev: "00000000000138de0496607bfc85ec4bfcebb6de0ff30048dd4bc4b12da48997", txs: []string{ "331d4ef64118e9e5be75f0f51f1a4c5057550c3320e22ff7206f3e1101f113d0", "1f4817d8e91c21d8c8d163dabccdd1875f760fd2dc34a1c2b7b8fa204e103597", @@ -84,6 +86,7 @@ var testParseBlockTxs = map[int]testBlock{ 532144: { size: 12198, time: 1528372417, + prev: "0000000048de525aea2af2ac305a7b196222fc327a34298f45110e378f838dce", txs: []string{ "574348e23301cc89535408b6927bf75f2ac88fadf8fdfb181c17941a5de02fe0", "9f048446401e7fac84963964df045b1f3992eda330a87b02871e422ff0a3fd28", @@ -143,6 +146,10 @@ func TestParseBlock(t *testing.T) { t.Errorf("ParseBlock() block time: got %d, want %d", blk.Time, tb.time) } + if blk.Prev != tb.prev { + t.Errorf("ParseBlock() prev hash: got %s, want %s", blk.Prev, tb.prev) + } + if len(blk.Txs) != len(tb.txs) { t.Errorf("ParseBlock() number of transactions: got %d, want %d", len(blk.Txs), len(tb.txs)) } diff --git a/bchain/coins/dcr/decredparser.go b/bchain/coins/dcr/decredparser.go index 8d20269d..b96cb964 100644 --- a/bchain/coins/dcr/decredparser.go +++ b/bchain/coins/dcr/decredparser.go @@ -119,6 +119,7 @@ func (p *DecredParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/divi/diviparser.go b/bchain/coins/divi/diviparser.go index 02b461e0..40548600 100755 --- a/bchain/coins/divi/diviparser.go +++ b/bchain/coins/divi/diviparser.go @@ -99,6 +99,7 @@ func (p *DivicoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/dogecoin/dogecoinparser.go b/bchain/coins/dogecoin/dogecoinparser.go index d38de058..743fea10 100644 --- a/bchain/coins/dogecoin/dogecoinparser.go +++ b/bchain/coins/dogecoin/dogecoinparser.go @@ -92,6 +92,7 @@ func (p *DogecoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/firo/firoparser.go b/bchain/coins/firo/firoparser.go index cfdf9c4a..742cb467 100644 --- a/bchain/coins/firo/firoparser.go +++ b/bchain/coins/firo/firoparser.go @@ -271,6 +271,7 @@ func (p *FiroParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: header.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: header.Timestamp.Unix(), }, diff --git a/bchain/coins/firo/firoparser_test.go b/bchain/coins/firo/firoparser_test.go index e4efd132..253323df 100644 --- a/bchain/coins/firo/firoparser_test.go +++ b/bchain/coins/firo/firoparser_test.go @@ -731,6 +731,7 @@ func TestParseBlock(t *testing.T) { }, want: &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: "a3b419a943bdc31aba65d40fc71f12ceb4ef2edcf1c8bd6d83b839261387e0d9", Size: 200286, Time: 1547120622, }, @@ -746,6 +747,7 @@ func TestParseBlock(t *testing.T) { }, want: &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: "0fb6e382a25a9e298a533237f359cb6cd86a99afb8d98e3d981e650fd5012c00", Size: 25298, Time: 1482107572, }, @@ -761,6 +763,7 @@ func TestParseBlock(t *testing.T) { }, want: &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: "12c117c25e52f71e8863eadd0ccc7cd7d45e7ef907cfadf99ca4b4d390cb1a0a", Size: 200062, Time: 1591752749, }, diff --git a/bchain/coins/monetaryunit/monetaryunitparser.go b/bchain/coins/monetaryunit/monetaryunitparser.go index 045eba03..d83dafb3 100644 --- a/bchain/coins/monetaryunit/monetaryunitparser.go +++ b/bchain/coins/monetaryunit/monetaryunitparser.go @@ -105,6 +105,7 @@ func (p *MonetaryUnitParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/myriad/myriadparser.go b/bchain/coins/myriad/myriadparser.go index 9cc50593..9783d197 100644 --- a/bchain/coins/myriad/myriadparser.go +++ b/bchain/coins/myriad/myriadparser.go @@ -85,6 +85,7 @@ func (p *MyriadParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/namecoin/namecoinparser.go b/bchain/coins/namecoin/namecoinparser.go index 9cf89e33..30e1f705 100644 --- a/bchain/coins/namecoin/namecoinparser.go +++ b/bchain/coins/namecoin/namecoinparser.go @@ -82,6 +82,7 @@ func (p *NamecoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/omotenashicoin/omotenashicoinparser.go b/bchain/coins/omotenashicoin/omotenashicoinparser.go index dd179fab..3b823a33 100644 --- a/bchain/coins/omotenashicoin/omotenashicoinparser.go +++ b/bchain/coins/omotenashicoin/omotenashicoinparser.go @@ -112,6 +112,7 @@ func (p *OmotenashiCoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/pivx/pivxparser.go b/bchain/coins/pivx/pivxparser.go index bd0c2800..57acff44 100644 --- a/bchain/coins/pivx/pivxparser.go +++ b/bchain/coins/pivx/pivxparser.go @@ -118,6 +118,7 @@ func (p *PivXParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/qtum/qtumparser.go b/bchain/coins/qtum/qtumparser.go index 8bc2d5e9..da3f0aee 100644 --- a/bchain/coins/qtum/qtumparser.go +++ b/bchain/coins/qtum/qtumparser.go @@ -124,6 +124,7 @@ func (p *QtumParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/ritocoin/ritocoinparser.go b/bchain/coins/ritocoin/ritocoinparser.go index de134b6e..ea1e3cfc 100644 --- a/bchain/coins/ritocoin/ritocoinparser.go +++ b/bchain/coins/ritocoin/ritocoinparser.go @@ -85,6 +85,7 @@ func (p *RitocoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/unobtanium/unobtaniumparser.go b/bchain/coins/unobtanium/unobtaniumparser.go index 3e803ff8..500c25f7 100644 --- a/bchain/coins/unobtanium/unobtaniumparser.go +++ b/bchain/coins/unobtanium/unobtaniumparser.go @@ -82,6 +82,7 @@ func (p *UnobtaniumParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/viacoin/viacoinparser.go b/bchain/coins/viacoin/viacoinparser.go index 369d45de..071cb09e 100644 --- a/bchain/coins/viacoin/viacoinparser.go +++ b/bchain/coins/viacoin/viacoinparser.go @@ -99,6 +99,7 @@ func (p *ViacoinParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/coins/vipstarcoin/vipstarcoinparser.go b/bchain/coins/vipstarcoin/vipstarcoinparser.go index 005cab8f..8751ed85 100644 --- a/bchain/coins/vipstarcoin/vipstarcoinparser.go +++ b/bchain/coins/vipstarcoin/vipstarcoinparser.go @@ -120,6 +120,7 @@ func (p *VIPSTARCOINParser) ParseBlock(b []byte) (*bchain.Block, error) { return &bchain.Block{ BlockHeader: bchain.BlockHeader{ + Prev: h.PrevBlock.String(), // needed for fork detection when parsing raw blocks Size: len(b), Time: h.Timestamp.Unix(), }, diff --git a/bchain/config_loader.go b/bchain/config_loader.go index 1c950c5f..7d70f428 100644 --- a/bchain/config_loader.go +++ b/bchain/config_loader.go @@ -17,8 +17,12 @@ import ( // BlockchainCfg contains fields read from blockbook's blockchaincfg.json after being rendered from templates. type BlockchainCfg struct { // more fields can be added later as needed - RpcUrl string `json:"rpc_url"` - RpcUrlWs string `json:"rpc_url_ws"` + RpcUrl string `json:"rpc_url"` + RpcUrlWs string `json:"rpc_url_ws"` + RpcUser string `json:"rpc_user"` + RpcPass string `json:"rpc_pass"` + RpcTimeout int `json:"rpc_timeout"` + Parse bool `json:"parse"` } // LoadBlockchainCfg returns the resolved blockchaincfg.json (env overrides are honored in tests)