Implement Golomb block filters for each block

Contains a websocket method `getBlockFilter` and REST endpoint `block-filters`
This commit is contained in:
grdddj 2023-08-10 08:17:55 +00:00 committed by Martin Boehm
parent bc8ce22ed0
commit 911454f171
16 changed files with 453 additions and 32 deletions

View File

@ -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 ""
}

View File

@ -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

View File

@ -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:" {

View File

@ -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

68
db/blockfilter.go Normal file
View File

@ -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)
}

98
db/blockfilter_test.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -908,6 +908,8 @@ The websocket interface provides the following requests:
- getCurrentFiatRates
- getFiatRatesTickersList
- getFiatRatesForTimestamps
- getMempoolFilters
- getBlockFilter
- estimateFee
- sendTransaction
- ping

View File

@ -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

View File

@ -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, '/')

View File

@ -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

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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 @@
<div class="row">
<div class="col" id="getMempoolFiltersResult"></div>
</div>
<div class="row">
<div class="col-2">
<input class="btn btn-secondary" type="button" value="get block filter" onclick="getBlockFilter()">
</div>
<div class="col-5">
<input type="text" class="form-control" id="getBlockFilterBlockHash" value="000000000000001cb4edd91be03b6775abd351fb51b1fbb0871fc1451454f362" placeholder="block hash">
</div>
</div>
<div class="row">
<div class="col" id="getBlockFilterResult"></div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="subscribe new block" onclick="subscribeNewBlock()">
@ -761,4 +783,4 @@
document.getElementById('serverAddress').value = window.location.protocol + "//" + window.location.host;
</script>
</html>
</html>