diff --git a/bchain/mempool_bitcoin_type.go b/bchain/mempool_bitcoin_type.go index 32a20b9d..cce99d8b 100644 --- a/bchain/mempool_bitcoin_type.go +++ b/bchain/mempool_bitcoin_type.go @@ -125,39 +125,33 @@ func (m *MempoolBitcoinType) getInputAddress(payload *chanInputPayload) *addrInd } -func isTaproot(addrDesc AddressDescriptor) bool { - if len(addrDesc) == 34 && addrDesc[0] == 0x51 && addrDesc[1] == 0x20 { - return true - } - return false -} - func (m *MempoolBitcoinType) computeGolombFilter(mtx *MempoolTx) string { uniqueScripts := make(map[string]struct{}) filterData := make([][]byte, 0) - for i := range mtx.Vin { - vin := &mtx.Vin[i] - if m.filterScripts == filterScriptsAll || (m.filterScripts == filterScriptsTaproot && isTaproot(vin.AddrDesc)) { - s := string(vin.AddrDesc) + + handleAddrDesc := func(ad AddressDescriptor) { + if m.filterScripts == filterScriptsAll || (m.filterScripts == filterScriptsTaproot && ad.IsTaproot()) { + if len(ad) == 0 { + return + } + s := string(ad) if _, found := uniqueScripts[s]; !found { - filterData = append(filterData, vin.AddrDesc) + filterData = append(filterData, ad) uniqueScripts[s] = struct{}{} } } } - for i := range mtx.Vout { - vout := &mtx.Vout[i] + + for _, vin := range mtx.Vin { + handleAddrDesc(vin.AddrDesc) + } + for _, vout := range mtx.Vout { b, err := hex.DecodeString(vout.ScriptPubKey.Hex) if err == nil { - if m.filterScripts == filterScriptsAll || (m.filterScripts == filterScriptsTaproot && isTaproot(b)) { - s := string(b) - if _, found := uniqueScripts[s]; !found { - filterData = append(filterData, b) - uniqueScripts[s] = struct{}{} - } - } + handleAddrDesc(b) } } + if len(filterData) == 0 { return "" } diff --git a/bchain/mempool_bitcoin_type_test.go b/bchain/mempool_bitcoin_type_test.go index f6b799cc..194f5e19 100644 --- a/bchain/mempool_bitcoin_type_test.go +++ b/bchain/mempool_bitcoin_type_test.go @@ -211,8 +211,8 @@ func TestMempoolBitcoinType_computeGolombFilter_taproot(t *testing.T) { if err != nil { t.Errorf("filter.Match vin[%d] unexpected error %v", i, err) } - if match != isTaproot(tt.mtx.Vin[i].AddrDesc) { - t.Errorf("filter.Match vin[%d] got %v, want %v", i, match, isTaproot(tt.mtx.Vin[i].AddrDesc)) + if match != tt.mtx.Vin[i].AddrDesc.IsTaproot() { + t.Errorf("filter.Match vin[%d] got %v, want %v", i, match, tt.mtx.Vin[i].AddrDesc.IsTaproot()) } } // check that the vout scripts match the filter @@ -222,8 +222,8 @@ func TestMempoolBitcoinType_computeGolombFilter_taproot(t *testing.T) { if err != nil { t.Errorf("filter.Match vout[%d] unexpected error %v", i, err) } - if match != isTaproot(s) { - t.Errorf("filter.Match vout[%d] got %v, want %v", i, match, isTaproot(s)) + if match != AddressDescriptor(s).IsTaproot() { + t.Errorf("filter.Match vout[%d] got %v, want %v", i, match, AddressDescriptor(s).IsTaproot()) } } // check that a random script does not match the filter diff --git a/bchain/types.go b/bchain/types.go index b53ae8ce..91774948 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -226,6 +226,13 @@ func (ad AddressDescriptor) String() string { return "ad:" + hex.EncodeToString(ad) } +func (ad AddressDescriptor) IsTaproot() bool { + if len(ad) == 34 && ad[0] == 0x51 && ad[1] == 0x20 { + return true + } + return false +} + // AddressDescriptorFromString converts string created by AddressDescriptor.String to AddressDescriptor func AddressDescriptorFromString(s string) (AddressDescriptor, error) { if len(s) > 3 && s[0:3] == "ad:" { diff --git a/common/internalstate.go b/common/internalstate.go index f064386b..6d1b2250 100644 --- a/common/internalstate.go +++ b/common/internalstate.go @@ -92,6 +92,10 @@ type InternalState struct { // database migrations UtxoChecked bool `json:"utxoChecked"` SortedAddressContracts bool `json:"sortedAddressContracts"` + + // TODO: add golombFilterP for block filters and check it at each startup + // if consistent with supplied config value + // Change of this value would require reindex } // StartedSync signals start of synchronization diff --git a/db/blockfilter.go b/db/blockfilter.go new file mode 100644 index 00000000..f6d73ea7 --- /dev/null +++ b/db/blockfilter.go @@ -0,0 +1,68 @@ +package db + +import ( + "encoding/hex" + + "github.com/golang/glog" + "github.com/martinboehm/btcutil/gcs" + "github.com/trezor/blockbook/bchain" +) + +func computeBlockFilter(allAddrDesc [][]byte, blockHash string, taprootOnly bool) string { + // TODO: take these from config - how to access it? From BitcoinRPC? + // TODO: these things should probably be an argument to this function, + // so it is better testable + golombFilterP := uint8(20) + golombFilterM := uint64(1 << golombFilterP) + + // TODO: code below is almost a copy-paste from computeGolombFilter, + // it might be possible to refactor it into a common function, e.g. + // computeGolomb(allAddrDescriptors, P, M, taprootOnly, hashIdentifier) -> filterData + // but where to put it? + + uniqueScripts := make(map[string]struct{}) + filterData := make([][]byte, 0) + + handleAddrDesc := func(ad bchain.AddressDescriptor) { + if taprootOnly && !ad.IsTaproot() { + return + } + if len(ad) == 0 { + return + } + s := string(ad) + if _, found := uniqueScripts[s]; !found { + filterData = append(filterData, ad) + uniqueScripts[s] = struct{}{} + } + } + + for _, ad := range allAddrDesc { + handleAddrDesc(ad) + } + + if len(filterData) == 0 { + return "" + } + + b, _ := hex.DecodeString(blockHash) + if len(b) < gcs.KeySize { + return "" + } + + filter, err := gcs.BuildGCSFilter(golombFilterP, golombFilterM, *(*[gcs.KeySize]byte)(b[:gcs.KeySize]), filterData) + if err != nil { + glog.Error("Cannot create golomb filter for ", blockHash, ", ", err) + return "" + } + + fb, err := filter.NBytes() + if err != nil { + glog.Error("Error getting NBytes from golomb filter for ", blockHash, ", ", err) + return "" + } + + // TODO: maybe not returning string but []byte, when we are saving it + // as []byte anyway? + return hex.EncodeToString(fb) +} diff --git a/db/blockfilter_test.go b/db/blockfilter_test.go new file mode 100644 index 00000000..ae90245c --- /dev/null +++ b/db/blockfilter_test.go @@ -0,0 +1,98 @@ +//go:build unittest + +package db + +import ( + "math/big" + "testing" + + "github.com/trezor/blockbook/tests/dbtestdata" +) + +func TestComputeBlockFilter(t *testing.T) { + // TODO: add more (vectorized) tests, with taproot txs + // - both taprootOnly=true and taprootOnly=false + // - check that decoding with different P does not work + allAddrDesc := getallAddrDesc() + blockHash := "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6" + taprootOnly := false + got := computeBlockFilter(allAddrDesc, blockHash, taprootOnly) + want := "0847a3118f0a689307a375c45c1b02379119579910ee80" + if got != want { + t.Errorf("computeBlockFilter() failed, expected: %s, got: %s", want, got) + } +} + +func getallAddrDesc() [][]byte { + allAddrDesc := make([][]byte, 0) + parser := bitcoinTestnetParser() + + // TODO: this data is copied exactly, make it common and reuse it + ta := &TxAddresses{ + Height: 12345, + VSize: 321, + Inputs: []TxInput{ + { + AddrDesc: addressToAddrDesc("2N7iL7AvS4LViugwsdjTB13uN4T7XhV1bCP", parser), + ValueSat: *big.NewInt(9011000000), + Txid: "c50c7ce2f5670fd52de738288299bd854a85ef1bb304f62f35ced1bd49a8a810", + Vout: 0, + }, + { + AddrDesc: addressToAddrDesc("2Mt9v216YiNBAzobeNEzd4FQweHrGyuRHze", parser), + ValueSat: *big.NewInt(8011000000), + Txid: "e96672c7fcc8da131427fcea7e841028614813496a56c11e8a6185c16861c495", + Vout: 1, + }, + { + AddrDesc: addressToAddrDesc("2NDyqJpHvHnqNtL1F9xAeCWMAW8WLJmEMyD", parser), + ValueSat: *big.NewInt(7011000000), + Txid: "ed308c72f9804dfeefdbb483ef8fd1e638180ad81d6b33f4b58d36d19162fa6d", + Vout: 134, + }, + }, + Outputs: []TxOutput{ + { + AddrDesc: addressToAddrDesc("2MuwoFGwABMakU7DCpdGDAKzyj2nTyRagDP", parser), + ValueSat: *big.NewInt(5011000000), + Spent: true, + SpentTxid: dbtestdata.TxidB1T1, + SpentIndex: 0, + SpentHeight: 432112345, + }, + { + AddrDesc: addressToAddrDesc("2Mvcmw7qkGXNWzkfH1EjvxDcNRGL1Kf2tEM", parser), + ValueSat: *big.NewInt(6011000000), + }, + { + AddrDesc: addressToAddrDesc("2N9GVuX3XJGHS5MCdgn97gVezc6EgvzikTB", parser), + ValueSat: *big.NewInt(7011000000), + Spent: true, + SpentTxid: dbtestdata.TxidB1T2, + SpentIndex: 14231, + SpentHeight: 555555, + }, + { + AddrDesc: addressToAddrDesc("mzii3fuRSpExMLJEHdHveW8NmiX8MPgavk", parser), + ValueSat: *big.NewInt(999900000), + }, + { + AddrDesc: addressToAddrDesc("mqHPFTRk23JZm9W1ANuEFtwTYwxjESSgKs", parser), + ValueSat: *big.NewInt(5000000000), + Spent: true, + SpentTxid: dbtestdata.TxidB2T1, + SpentIndex: 674541, + SpentHeight: 6666666, + }, + }, + } + + for _, input := range ta.Inputs { + allAddrDesc = append(allAddrDesc, input.AddrDesc) + } + for _, output := range ta.Outputs { + allAddrDesc = append(allAddrDesc, output.AddrDesc) + } + + return allAddrDesc +} diff --git a/db/bulkconnect.go b/db/bulkconnect.go index 6fa2b542..e0adc607 100644 --- a/db/bulkconnect.go +++ b/db/bulkconnect.go @@ -27,6 +27,7 @@ type BulkConnect struct { bulkAddressesCount int ethBlockTxs []ethBlockTx txAddressesMap map[string]*TxAddresses + blockFilters map[string]string balances map[string]*AddrBalance addressContracts map[string]*AddrContracts height uint32 @@ -40,6 +41,7 @@ const ( partialStoreBalances = maxBulkBalances / 10 maxBulkAddrContracts = 1200000 partialStoreAddrContracts = maxBulkAddrContracts / 10 + maxBlockFilters = 1000 ) // InitBulkConnect initializes bulk connect and switches DB to inconsistent state @@ -170,9 +172,20 @@ func (b *BulkConnect) storeBulkAddresses(wb *grocksdb.WriteBatch) error { return nil } +func (b *BulkConnect) storeBulkBlockFilters(wb *grocksdb.WriteBatch) error { + for blockHash, blockFilter := range b.blockFilters { + if err := b.d.storeBlockFilter(wb, blockHash, blockFilter); err != nil { + return err + } + } + b.blockFilters = make(map[string]string) + return nil +} + func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs bool) error { addresses := make(addressesMap) - if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances); err != nil { + allBlockAddrDesc := make([][]byte, 0) + if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances, &allBlockAddrDesc); err != nil { return err } var storeAddressesChan, storeBalancesChan chan error @@ -199,8 +212,13 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs addresses: addresses, }) b.bulkAddressesCount += len(addresses) + if b.blockFilters == nil { + b.blockFilters = make(map[string]string) // TODO: where to put this? + } + taprootOnly := true // TODO: take from config + b.blockFilters[block.BlockHeader.Hash] = computeBlockFilter(allBlockAddrDesc, block.BlockHeader.Hash, taprootOnly) // open WriteBatch only if going to write - if sa || b.bulkAddressesCount > maxBulkAddresses || storeBlockTxs { + if sa || b.bulkAddressesCount > maxBulkAddresses || storeBlockTxs || len(b.blockFilters) > maxBlockFilters { start := time.Now() wb := grocksdb.NewWriteBatch() defer wb.Destroy() @@ -215,6 +233,11 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs return err } } + if len(b.blockFilters) > maxBlockFilters { + if err := b.storeBulkBlockFilters(wb); err != nil { + return err + } + } if err := b.d.WriteBatch(wb); err != nil { return err } @@ -388,6 +411,9 @@ func (b *BulkConnect) Close() error { if err := b.storeBulkAddresses(wb); err != nil { return err } + if err := b.storeBulkBlockFilters(wb); err != nil { + return err + } if err := b.d.WriteBatch(wb); err != nil { return err } diff --git a/db/rocksdb.go b/db/rocksdb.go index 521ecd74..2b980291 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -83,6 +83,7 @@ const ( // BitcoinType cfAddressBalance cfTxAddresses + cfBlockFilter __break__ @@ -102,7 +103,7 @@ var cfNames []string var cfBaseNames = []string{"default", "height", "addresses", "blockTxs", "transactions", "fiatRates"} // type specific columns -var cfNamesBitcoinType = []string{"addressBalance", "txAddresses"} +var cfNamesBitcoinType = []string{"addressBalance", "txAddresses", "blockFilter"} var cfNamesEthereumType = []string{"addressContracts", "internalData", "contracts", "functionSignatures", "blockInternalDataErrors", "addressAliases"} func openDB(path string, c *grocksdb.Cache, openFiles int) (*grocksdb.DB, []*grocksdb.ColumnFamilyHandle, error) { @@ -347,8 +348,9 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { addresses := make(addressesMap) if chainType == bchain.ChainBitcoinType { txAddressesMap := make(map[string]*TxAddresses) + allBlockAddrDesc := make([][]byte, 0) balances := make(map[string]*AddrBalance) - if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances); err != nil { + if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances, &allBlockAddrDesc); err != nil { return err } if err := d.storeTxAddresses(wb, txAddressesMap); err != nil { @@ -360,6 +362,11 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if err := d.storeAndCleanupBlockTxs(wb, block); err != nil { return err } + taprootOnly := true // TODO: take from config + blockFilter := computeBlockFilter(allBlockAddrDesc, block.BlockHeader.Hash, taprootOnly) + if err := d.storeBlockFilter(wb, block.BlockHeader.Hash, blockFilter); err != nil { + return err + } } else if chainType == bchain.ChainEthereumType { addressContracts := make(map[string]*AddrContracts) blockTxs, err := d.processAddressesEthereumType(block, addresses, addressContracts) @@ -590,7 +597,8 @@ func (d *RocksDB) GetAndResetConnectBlockStats() string { return s } -func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses addressesMap, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance) error { +// TODO: maybe return allBlockAddrDesc from this function instead of taking it as argument +func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses addressesMap, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance, allBlockAddrDesc *[][]byte) error { blockTxIDs := make([][]byte, len(block.Txs)) blockTxAddresses := make([]*TxAddresses, len(block.Txs)) // first process all outputs so that inputs can refer to txs in this block @@ -628,6 +636,7 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add } continue } + *allBlockAddrDesc = append(*allBlockAddrDesc, addrDesc) // new addrDesc tao.AddrDesc = addrDesc if d.chainParser.IsAddrDescIndexable(addrDesc) { strAddrDesc := string(addrDesc) @@ -702,6 +711,7 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add if spentOutput.Spent { glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is double spend", block.Height, tx.Txid, input.Txid, input.Vout) } + *allBlockAddrDesc = append(*allBlockAddrDesc, spentOutput.AddrDesc) // new addrDesc tai.AddrDesc = spentOutput.AddrDesc tai.ValueSat = spentOutput.ValueSat // mark the output as spent in tx @@ -1550,6 +1560,19 @@ func (d *RocksDB) disconnectTxAddressesOutputs(wb *grocksdb.WriteBatch, btxID [] return nil } +func (d *RocksDB) disconnectBlockFilter(wb *grocksdb.WriteBatch, height uint32) error { + blockHash, err := d.GetBlockHash(height) + if err != nil { + return err + } + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return err + } + wb.DeleteCF(d.cfh[cfBlockFilter], blockHashBytes) + return nil +} + func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { wb := grocksdb.NewWriteBatch() defer wb.Destroy() @@ -1635,6 +1658,9 @@ func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { wb.DeleteCF(d.cfh[cfTransactions], b) wb.DeleteCF(d.cfh[cfTxAddresses], b) } + if err := d.disconnectBlockFilter(wb, height); err != nil { + return err + } return d.WriteBatch(wb) } @@ -1873,6 +1899,7 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro if is.ExtendedIndex != d.extendedIndex { return nil, errors.Errorf("ExtendedIndex setting does not match. DB extendedIndex %v, extendedIndex in options %v", is.ExtendedIndex, d.extendedIndex) } + // TODO: verify the block filter P and error if it does not match } nc, err := d.checkColumns(is) if err != nil { @@ -2191,6 +2218,36 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { return nil } +func (d *RocksDB) storeBlockFilter(wb *grocksdb.WriteBatch, blockHash string, blockFilter string) error { + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return err + } + blockFilterBytes, err := hex.DecodeString(blockFilter) + if err != nil { + return err + } + wb.PutCF(d.cfh[cfBlockFilter], blockHashBytes, blockFilterBytes) + return nil +} + +func (d *RocksDB) GetBlockFilter(blockHash string) (string, error) { + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return "", err + } + val, err := d.db.GetCF(d.ro, d.cfh[cfBlockFilter], blockHashBytes) + if err != nil { + return "", err + } + defer val.Free() + buf := val.Data() + if buf == nil { + return "", nil + } + return hex.EncodeToString(buf), nil +} + // Helpers func packAddressKey(addrDesc bchain.AddressDescriptor, height uint32) []byte { diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index 70dc5741..71857886 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -15,6 +15,7 @@ import ( vlq "github.com/bsm/go-vlq" "github.com/juju/errors" + "github.com/linxGnu/grocksdb" "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins/btc" @@ -802,6 +803,45 @@ func Test_BulkConnect_BitcoinType(t *testing.T) { } } +func Test_BlockFilter_GetAndStore(t *testing.T) { + d := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }) + defer closeAndDestroyRocksDB(t, d) + + blockHash := "0000000000000003d0c9722718f8ee86c2cf394f9cd458edb1c854de2a7b1a91" + blockFilter := "042c6340895e413d8a811fa0" + + // Empty at the beginning + got, err := d.GetBlockFilter(blockHash) + if err != nil { + t.Fatal(err) + } + want := "" + if got != want { + t.Fatalf("GetBlockFilter(%s) = %s, want %s", blockHash, got, want) + } + + // Store the filter + wb := grocksdb.NewWriteBatch() + if err := d.storeBlockFilter(wb, blockHash, blockFilter); err != nil { + t.Fatal(err) + } + if err := d.WriteBatch(wb); err != nil { + t.Fatal(err) + } + + // Get the filter + got, err = d.GetBlockFilter(blockHash) + if err != nil { + t.Fatal(err) + } + want = blockFilter + if got != want { + t.Fatalf("GetBlockFilter(%s) = %s, want %s", blockHash, got, want) + } +} + func Test_packBigint_unpackBigint(t *testing.T) { bigbig1, _ := big.NewInt(0).SetString("123456789123456789012345", 10) bigbig2, _ := big.NewInt(0).SetString("12345678912345678901234512389012345123456789123456789012345123456789123456789012345", 10) diff --git a/docs/api.md b/docs/api.md index beb3a1c5..69ac4579 100644 --- a/docs/api.md +++ b/docs/api.md @@ -908,6 +908,8 @@ The websocket interface provides the following requests: - getCurrentFiatRates - getFiatRatesTickersList - getFiatRatesForTimestamps +- getMempoolFilters +- getBlockFilter - estimateFee - sendTransaction - ping diff --git a/docs/testing.md b/docs/testing.md index 9a6db96b..639bd3a8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,7 +16,7 @@ You can use Go's flag *-run* to filter which tests should be executed. Use *ARGS ## Unit tests -Unit test file must start with constraint `// +build unittest` followed by blank line (constraints are described +Unit test file must start with constraint `//go:build unittest` followed by blank line (constraints are described [here](https://golang.org/pkg/go/build/#hdr-Build_Constraints)). Every coin implementation must have unit tests. At least for parser. Usual test suite define real transaction data diff --git a/server/public.go b/server/public.go index 236f6e0d..580c6812 100644 --- a/server/public.go +++ b/server/public.go @@ -184,6 +184,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1)) } serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault)) + serveMux.HandleFunc(path+"api/block-filters/", s.jsonHandler(s.apiBlockFilters, apiDefault)) serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault)) serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault)) serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault)) @@ -196,6 +197,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) // v2 format serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2)) + serveMux.HandleFunc(path+"api/v2/block-filters/", s.jsonHandler(s.apiBlockFilters, apiV2)) serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2)) serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2)) serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2)) @@ -1226,6 +1228,81 @@ func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface }, nil } +func (s *PublicServer) apiBlockFilters(r *http.Request, apiVersion int) (interface{}, error) { + // Define return type + type resBlockFilters struct { + BlockFilters map[int]map[string]string `json:"blockFilters"` + } + + // Parse parameters + lastN, ec := strconv.Atoi(r.URL.Query().Get("lastN")) + if ec != nil { + lastN = 0 + } + from, ec := strconv.Atoi(r.URL.Query().Get("from")) + if ec != nil { + from = 0 + } + to, ec := strconv.Atoi(r.URL.Query().Get("to")) + if ec != nil { + to = 0 + } + + // Sanity checks + if lastN == 0 && from == 0 && to == 0 { + return nil, api.NewAPIError("Missing parameters", true) + } + if from > to { + return nil, api.NewAPIError("Invalid parameters - from > to", true) + } + + // Best height is needed more than once + bestHeight, _, err := s.db.GetBestBlock() + if err != nil { + glog.Error(err) + return nil, err + } + + // Modify to/from if needed + if lastN > 0 { + // Get data for last N blocks + to = int(bestHeight) + from = to - lastN + 1 + } else { + // Get data for specified from-to range + // From will always stay the same (even if 0) + // To will be the best block if not specified + if to == 0 { + to = int(bestHeight) + } + } + + handleBlockFiltersResultFromTo := func(fromHeight int, toHeight int) (interface{}, error) { + blockFiltersMap := make(map[int]map[string]string) + for i := fromHeight; i <= toHeight; i++ { + blockHash, err := s.db.GetBlockHash(uint32(i)) + if err != nil { + glog.Error(err) + return nil, err + } + blockFilter, err := s.db.GetBlockFilter(blockHash) + if err != nil { + glog.Error(err) + return nil, err + } + resultMap := make(map[string]string) + resultMap["blockHash"] = blockHash + resultMap["filter"] = blockFilter + blockFiltersMap[i] = resultMap + } + return resBlockFilters{ + BlockFilters: blockFiltersMap, + }, nil + } + + return handleBlockFiltersResultFromTo(from, to) +} + func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) { var txid string i := strings.LastIndexByte(r.URL.Path, '/') diff --git a/server/public_test.go b/server/public_test.go index 2217696b..19351d69 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -1471,6 +1471,16 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { }, want: `{"id":"42","data":{"error":{"message":"Unsupported script filter invalid"}}}`, }, + { + name: "websocket getBlockFilter", + req: websocketReq{ + Method: "getBlockFilter", + Params: map[string]interface{}{ + "blockHash": "abcd", + }, + }, + want: `{"id":"43","data":""}`, + }, } // send all requests at once diff --git a/server/websocket.go b/server/websocket.go index 1e5ee568..9b7f9e08 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -350,6 +350,14 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsRe } return }, + "getBlockFilter": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBlockFilterReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getBlockFilter(&r) + } + return + }, "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewBlock(c, req) }, @@ -645,6 +653,10 @@ func (s *WebsocketServer) getMempoolFilters(r *WsMempoolFiltersReq) (res bchain. return } +func (s *WebsocketServer) getBlockFilter(r *WsBlockFilterReq) (res string, err error) { + return s.db.GetBlockFilter(r.BlockHash) +} + type subscriptionResponse struct { Subscribed bool `json:"subscribed"` } diff --git a/server/ws_types.go b/server/ws_types.go index b8094fd8..f6074b44 100644 --- a/server/ws_types.go +++ b/server/ws_types.go @@ -81,6 +81,10 @@ type WsMempoolFiltersReq struct { FromTimestamp uint32 `json:"fromTimestamp"` } +type WsBlockFilterReq struct { + BlockHash string `json:"blockHash"` +} + type WsTransactionSpecificReq struct { Txid string `json:"txid"` } diff --git a/static/test-websocket.html b/static/test-websocket.html index 97f01771..1f4ea808 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -411,6 +411,17 @@ }); } + function getBlockFilter() { + const method = 'getBlockFilter'; + let blockHash = document.getElementById('getBlockFilterBlockHash').value; + const params = { + blockHash, + }; + send(method, params, function (result) { + document.getElementById('getBlockFilterResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + function subscribeNewFiatRatesTicker() { const method = 'subscribeFiatRates'; var currency = document.getElementById('subscribeFiatRatesCurrency').value; @@ -689,6 +700,17 @@