Implement Golomb block filters for each block
Contains a websocket method `getBlockFilter` and REST endpoint `block-filters`
This commit is contained in:
parent
bc8ce22ed0
commit
911454f171
@ -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 ""
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:" {
|
||||
|
||||
@ -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
68
db/blockfilter.go
Normal 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
98
db/blockfilter_test.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -908,6 +908,8 @@ The websocket interface provides the following requests:
|
||||
- getCurrentFiatRates
|
||||
- getFiatRatesTickersList
|
||||
- getFiatRatesForTimestamps
|
||||
- getMempoolFilters
|
||||
- getBlockFilter
|
||||
- estimateFee
|
||||
- sendTransaction
|
||||
- ping
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, '/')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user