d2193e980a
* Generate secondary hashXNotification(s) on every headerNotification. * Attempt LBCD connection with rpc.Client. * Optional --daemon-url. * Correct HashXStatusKey field. Should be HASHX_LEN. * Connect to lbcd using lbcd/rpcclient library. * Handle deprecation of node.js 12 actions. * Add --daemon-ca-path argument and establish HTTPS connection if specified. * Remove dead code. Tighten definition of TransactionBroadcastReq. * Correct default daemon URL.
1157 lines
32 KiB
Go
1157 lines
32 KiB
Go
package db
|
|
|
|
// db_get.go contains the basic access functions to the database.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/lbryio/herald.go/db/prefixes"
|
|
"github.com/lbryio/herald.go/db/stack"
|
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
|
"github.com/lbryio/lbcd/wire"
|
|
"github.com/linxGnu/grocksdb"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// GetExpirationHeight returns the expiration height for the given height. Uses
|
|
// the original claim expiration time.
|
|
func GetExpirationHeight(lastUpdatedHeight uint32) uint32 {
|
|
return GetExpirationHeightFull(lastUpdatedHeight, false)
|
|
}
|
|
|
|
// GetExpirationHeightFull returns the expiration height for the given height.
|
|
// Takes boolean to indicated whether to use extended or original expiration time.
|
|
func GetExpirationHeightFull(lastUpdatedHeight uint32, extended bool) uint32 {
|
|
if extended {
|
|
return lastUpdatedHeight + ExtendedClaimExpirationTime
|
|
}
|
|
if lastUpdatedHeight < ExtendedClaimExpirationForkHeight {
|
|
return lastUpdatedHeight + OriginalClaimExpirationTime
|
|
}
|
|
return lastUpdatedHeight + ExtendedClaimExpirationTime
|
|
}
|
|
|
|
// EnsureHandle is a helper function to ensure that the db has a handle to the given column family.
|
|
func (db *ReadOnlyDBColumnFamily) EnsureHandle(prefix byte) (*grocksdb.ColumnFamilyHandle, error) {
|
|
cfName := string(prefix)
|
|
handle := db.Handles[cfName]
|
|
if handle == nil {
|
|
return nil, fmt.Errorf("%s handle not found", cfName)
|
|
}
|
|
return handle, nil
|
|
}
|
|
|
|
// GetBlockHash returns the block hash for the given height.
|
|
func (db *ReadOnlyDBColumnFamily) GetBlockHash(height uint32) ([]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.BlockHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewBlockHashKey(height)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, err
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
return rawValue, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetBlockTXs(height uint32) ([]*chainhash.Hash, error) {
|
|
handle, err := db.EnsureHandle(prefixes.BlockTXs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.BlockTxsKey{
|
|
Prefix: []byte{prefixes.BlockTXs},
|
|
Height: height,
|
|
}
|
|
slice, err := db.DB.GetCF(db.Opts, handle, key.PackKey())
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.BlockTxsValueUnpack(rawValue)
|
|
return value.TxHashes, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTouchedHashXs(height uint32) ([][]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.TouchedHashX)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.TouchedHashXKey{
|
|
Prefix: []byte{prefixes.TouchedHashX},
|
|
Height: height,
|
|
}
|
|
slice, err := db.DB.GetCF(db.Opts, handle, key.PackKey())
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.TouchedHashXValue{}
|
|
value.UnpackValue(rawValue)
|
|
return value.TouchedHashXs, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetHeader(height uint32) ([]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewHeaderKey(height)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, err
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
return rawValue, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetHeaders(height uint32, count uint32) ([][112]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
startKeyRaw := prefixes.NewHeaderKey(height).PackKey()
|
|
endKeyRaw := prefixes.NewHeaderKey(height + count).PackKey()
|
|
options := NewIterateOptions().WithDB(db).WithPrefix([]byte{prefixes.Header}).WithCfHandle(handle)
|
|
options = options.WithIncludeKey(false).WithIncludeValue(true) //.WithIncludeStop(true)
|
|
options = options.WithStart(startKeyRaw).WithStop(endKeyRaw)
|
|
|
|
result := make([][112]byte, 0, count)
|
|
for kv := range IterCF(db.DB, options) {
|
|
h := [112]byte{}
|
|
copy(h[:], kv.Value.(*prefixes.BlockHeaderValue).Header[:112])
|
|
result = append(result, h)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetBalance(hashX []byte) (uint64, uint64, error) {
|
|
handle, err := db.EnsureHandle(prefixes.UTXO)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
startKey := prefixes.UTXOKey{
|
|
Prefix: []byte{prefixes.UTXO},
|
|
HashX: hashX,
|
|
TxNum: 0,
|
|
Nout: 0,
|
|
}
|
|
endKey := prefixes.UTXOKey{
|
|
Prefix: []byte{prefixes.UTXO},
|
|
HashX: hashX,
|
|
TxNum: math.MaxUint32,
|
|
Nout: math.MaxUint16,
|
|
}
|
|
|
|
startKeyRaw := startKey.PackKey()
|
|
endKeyRaw := endKey.PackKey()
|
|
// Prefix and handle
|
|
options := NewIterateOptions().WithDB(db).WithPrefix([]byte{prefixes.UTXO}).WithCfHandle(handle)
|
|
// Start and stop bounds
|
|
options = options.WithStart(startKeyRaw).WithStop(endKeyRaw).WithIncludeStop(true)
|
|
// Don't include the key
|
|
options = options.WithIncludeKey(false).WithIncludeValue(true)
|
|
|
|
ch := IterCF(db.DB, options)
|
|
var confirmed uint64 = 0
|
|
var unconfirmed uint64 = 0 // TODO
|
|
for kv := range ch {
|
|
confirmed += kv.Value.(*prefixes.UTXOValue).Amount
|
|
}
|
|
|
|
return confirmed, unconfirmed, nil
|
|
}
|
|
|
|
type TXOInfo struct {
|
|
TxHash *chainhash.Hash
|
|
TxPos uint16
|
|
Height uint32
|
|
Value uint64
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetUnspent(hashX []byte) ([]TXOInfo, error) {
|
|
startKey := &prefixes.UTXOKey{
|
|
Prefix: []byte{prefixes.UTXO},
|
|
HashX: hashX,
|
|
TxNum: 0,
|
|
Nout: 0,
|
|
}
|
|
endKey := &prefixes.UTXOKey{
|
|
Prefix: []byte{prefixes.UTXO},
|
|
HashX: hashX,
|
|
TxNum: math.MaxUint32,
|
|
Nout: math.MaxUint16,
|
|
}
|
|
selectedUTXO, err := db.selectFrom([]byte{prefixes.UTXO}, startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selectTxHashByTxNum := func(in []*prefixes.PrefixRowKV) ([]*IterOptions, error) {
|
|
historyKey := in[0].Key.(*prefixes.UTXOKey)
|
|
out := make([]*IterOptions, 0, 100)
|
|
startKey := &prefixes.TxHashKey{
|
|
Prefix: []byte{prefixes.TxHash},
|
|
TxNum: historyKey.TxNum,
|
|
}
|
|
endKey := &prefixes.TxHashKey{
|
|
Prefix: []byte{prefixes.TxHash},
|
|
TxNum: historyKey.TxNum,
|
|
}
|
|
selectedTxHash, err := db.selectFrom([]byte{prefixes.TxHash}, startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, selectedTxHash...)
|
|
return out, nil
|
|
}
|
|
|
|
results := make([]TXOInfo, 0, 1000)
|
|
for kvs := range innerJoin(db.DB, iterate(db.DB, selectedUTXO), selectTxHashByTxNum) {
|
|
if err := checkForError(kvs); err != nil {
|
|
return results, err
|
|
}
|
|
utxoKey := kvs[0].Key.(*prefixes.UTXOKey)
|
|
utxoValue := kvs[0].Value.(*prefixes.UTXOValue)
|
|
txhashValue := kvs[1].Value.(*prefixes.TxHashValue)
|
|
results = append(results,
|
|
TXOInfo{
|
|
TxHash: txhashValue.TxHash,
|
|
TxPos: utxoKey.Nout,
|
|
Height: stack.BisectRight(db.TxCounts, []uint32{utxoKey.TxNum})[0],
|
|
Value: utxoValue.Amount,
|
|
},
|
|
)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
type TxInfo struct {
|
|
TxHash *chainhash.Hash
|
|
Height uint32
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetHistory(hashX []byte) ([]TxInfo, error) {
|
|
startKey := &prefixes.HashXHistoryKey{
|
|
Prefix: []byte{prefixes.HashXHistory},
|
|
HashX: hashX,
|
|
Height: 0,
|
|
}
|
|
endKey := &prefixes.HashXHistoryKey{
|
|
Prefix: []byte{prefixes.HashXHistory},
|
|
HashX: hashX,
|
|
Height: math.MaxUint32,
|
|
}
|
|
selectedHistory, err := db.selectFrom([]byte{prefixes.HashXHistory}, startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selectTxHashByTxNums := func(in []*prefixes.PrefixRowKV) ([]*IterOptions, error) {
|
|
historyValue := in[0].Value.(*prefixes.HashXHistoryValue)
|
|
out := make([]*IterOptions, 0, 100)
|
|
for _, txnum := range historyValue.TxNums {
|
|
startKey := &prefixes.TxHashKey{
|
|
Prefix: []byte{prefixes.TxHash},
|
|
TxNum: txnum,
|
|
}
|
|
endKey := &prefixes.TxHashKey{
|
|
Prefix: []byte{prefixes.TxHash},
|
|
TxNum: txnum,
|
|
}
|
|
selectedTxHash, err := db.selectFrom([]byte{prefixes.TxHash}, startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, selectedTxHash...)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
results := make([]TxInfo, 0, 1000)
|
|
for kvs := range innerJoin(db.DB, iterate(db.DB, selectedHistory), selectTxHashByTxNums) {
|
|
if err := checkForError(kvs); err != nil {
|
|
return results, err
|
|
}
|
|
historyKey := kvs[0].Key.(*prefixes.HashXHistoryKey)
|
|
txHashValue := kvs[1].Value.(*prefixes.TxHashValue)
|
|
results = append(results, TxInfo{
|
|
TxHash: txHashValue.TxHash,
|
|
Height: historyKey.Height,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetStatus(hashX []byte) ([]byte, error) {
|
|
// Lookup in HashXMempoolStatus first.
|
|
status, err := db.getMempoolStatus(hashX)
|
|
if err == nil && status != nil {
|
|
log.Debugf("(mempool) status(%#v) -> %#v", hashX, status)
|
|
return status, err
|
|
}
|
|
|
|
// No indexed mempool status. Lookup in HashXStatus second.
|
|
handle, err := db.EnsureHandle(prefixes.HashXStatus)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := &prefixes.HashXStatusKey{
|
|
Prefix: []byte{prefixes.HashXStatus},
|
|
HashX: hashX,
|
|
}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err == nil && slice.Size() > 0 {
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.HashXStatusValue{}
|
|
value.UnpackValue(rawValue)
|
|
log.Debugf("status(%#v) -> %#v", hashX, value.Status)
|
|
return value.Status, nil
|
|
}
|
|
|
|
// No indexed status. Fall back to enumerating HashXHistory.
|
|
txs, err := db.GetHistory(hashX)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(txs) == 0 {
|
|
return []byte{}, err
|
|
}
|
|
|
|
hash := sha256.New()
|
|
for _, tx := range txs {
|
|
hash.Write([]byte(fmt.Sprintf("%s:%d:", tx.TxHash.String(), tx.Height)))
|
|
}
|
|
// TODO: Mempool history
|
|
return hash.Sum(nil), err
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) getMempoolStatus(hashX []byte) ([]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.HashXMempoolStatus)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := &prefixes.HashXMempoolStatusKey{
|
|
Prefix: []byte{prefixes.HashXMempoolStatus},
|
|
HashX: hashX,
|
|
}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.HashXMempoolStatusValue{}
|
|
value.UnpackValue(rawValue)
|
|
return value.Status, nil
|
|
}
|
|
|
|
// GetStreamsAndChannelRepostedByChannelHashes returns a map of streams and channel hashes that are reposted by the given channel hashes.
|
|
func (db *ReadOnlyDBColumnFamily) GetStreamsAndChannelRepostedByChannelHashes(reposterChannelHashes [][]byte) (map[string][]byte, map[string][]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.ChannelToClaim)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
streams := make(map[string][]byte)
|
|
channels := make(map[string][]byte)
|
|
|
|
for _, reposterChannelHash := range reposterChannelHashes {
|
|
key := prefixes.NewChannelToClaimKeyWHash(reposterChannelHash)
|
|
rawKeyPrefix := key.PartialPack(1)
|
|
options := NewIterateOptions().WithDB(db).WithCfHandle(handle).WithPrefix(rawKeyPrefix)
|
|
options = options.WithIncludeKey(false).WithIncludeValue(true)
|
|
ch := IterCF(db.DB, options)
|
|
// for stream := range Iterate(db.DB, prefixes.ChannelToClaim, []byte{reposterChannelHash}, false) {
|
|
for stream := range ch {
|
|
value := stream.Value.(*prefixes.ChannelToClaimValue)
|
|
repost, err := db.GetRepost(value.ClaimHash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if repost != nil {
|
|
txo, err := db.GetClaimTxo(repost)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if txo != nil {
|
|
repostStr := hex.EncodeToString(repost)
|
|
if normalName := txo.NormalizedName(); len(normalName) > 0 && normalName[0] == '@' {
|
|
channels[repostStr] = reposterChannelHash
|
|
} else {
|
|
streams[repostStr] = reposterChannelHash
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return streams, channels, nil
|
|
}
|
|
|
|
// GetClaimsInChannelCount returns the number of claims in the given channel.
|
|
func (db *ReadOnlyDBColumnFamily) GetClaimsInChannelCount(channelHash []byte) (uint32, error) {
|
|
handle, err := db.EnsureHandle(prefixes.ChannelCount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
key := prefixes.NewChannelCountKey(channelHash)
|
|
rawKey := key.PackKey()
|
|
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
} else if slice.Size() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.ChannelCountValueUnpack(rawValue)
|
|
|
|
return value.Count, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetShortClaimIdUrl(name string, normalizedName string, claimHash []byte, rootTxNum uint32, rootPosition uint16) (string, error) {
|
|
prefix := []byte{prefixes.ClaimShortIdPrefix}
|
|
handle, err := db.EnsureHandle(prefixes.ClaimShortIdPrefix)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
claimId := hex.EncodeToString(claimHash)
|
|
claimIdLen := len(claimId)
|
|
for prefixLen := 0; prefixLen < 10; prefixLen++ {
|
|
var j int = prefixLen + 1
|
|
if j > claimIdLen {
|
|
j = claimIdLen
|
|
}
|
|
partialClaimId := claimId[:j]
|
|
partialKey := prefixes.NewClaimShortIDKey(normalizedName, partialClaimId)
|
|
log.Printf("partialKey: %#v\n", partialKey)
|
|
keyPrefix := partialKey.PartialPack(2)
|
|
// Prefix and handle
|
|
options := NewIterateOptions().WithDB(db).WithPrefix(prefix).WithCfHandle(handle)
|
|
// Start and stop bounds
|
|
options = options.WithStart(keyPrefix).WithStop(keyPrefix)
|
|
// Don't include the key
|
|
options = options.WithIncludeValue(false)
|
|
|
|
ch := IterCF(db.DB, options)
|
|
row := <-ch
|
|
if row == nil {
|
|
continue
|
|
}
|
|
|
|
key := row.Key.(*prefixes.ClaimShortIDKey)
|
|
if key.RootTxNum == rootTxNum && key.RootPosition == rootPosition {
|
|
return fmt.Sprintf("%s#%s", name, key.PartialClaimId), nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetRepost(claimHash []byte) ([]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.Repost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewRepostKey(claimHash)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.RepostValueUnpack(rawValue)
|
|
return value.RepostedClaimHash, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetRepostedCount(claimHash []byte) (int, error) {
|
|
handle, err := db.EnsureHandle(prefixes.RepostedCount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
key := prefixes.RepostedCountKey{Prefix: []byte{prefixes.RepostedCount}, ClaimHash: claimHash}
|
|
rawKey := key.PackKey()
|
|
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
} else if slice.Size() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
value := prefixes.RepostedCountValue{}
|
|
value.UnpackValue(slice.Data())
|
|
return int(value.RepostedCount), nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetChannelForClaim(claimHash []byte, txNum uint32, position uint16) ([]byte, error) {
|
|
handle, err := db.EnsureHandle(prefixes.ClaimToChannel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewClaimToChannelKey(claimHash, txNum, position)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.ClaimToChannelValueUnpack(rawValue)
|
|
return value.SigningHash, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetActiveAmount(claimHash []byte, txoType uint8, height uint32) (uint64, error) {
|
|
handle, err := db.EnsureHandle(prefixes.ActiveAmount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
startKey := prefixes.NewActiveAmountKey(claimHash, txoType, 0)
|
|
endKey := prefixes.NewActiveAmountKey(claimHash, txoType, height+1)
|
|
|
|
startKeyRaw := startKey.PartialPack(3)
|
|
endKeyRaw := endKey.PartialPack(3)
|
|
// Prefix and handle
|
|
options := NewIterateOptions().WithDB(db).WithPrefix([]byte{prefixes.ActiveAmount}).WithCfHandle(handle)
|
|
// Start and stop bounds
|
|
options = options.WithStart(startKeyRaw).WithStop(endKeyRaw)
|
|
// Don't include the key
|
|
options = options.WithIncludeKey(false).WithIncludeValue(true)
|
|
|
|
ch := IterCF(db.DB, options)
|
|
var sum uint64 = 0
|
|
for kv := range ch {
|
|
sum += kv.Value.(*prefixes.ActiveAmountValue).Amount
|
|
}
|
|
|
|
return sum, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetEffectiveAmount(claimHash []byte, supportOnly bool) (uint64, error) {
|
|
handle, err := db.EnsureHandle(prefixes.EffectiveAmount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
key := prefixes.EffectiveAmountKey{Prefix: []byte{prefixes.EffectiveAmount}, ClaimHash: claimHash}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
} else if slice.Size() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
value := prefixes.EffectiveAmountValue{}
|
|
value.UnpackValue(slice.Data())
|
|
var amount uint64
|
|
if supportOnly {
|
|
amount += value.ActivatedSupportSum
|
|
} else {
|
|
amount += value.ActivatedSum
|
|
}
|
|
return amount, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetSupportAmount(claimHash []byte) (uint64, error) {
|
|
handle, err := db.EnsureHandle(prefixes.SupportAmount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
key := prefixes.NewSupportAmountKey(claimHash)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
} else if slice.Size() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.SupportAmountValueUnpack(rawValue)
|
|
return value.Amount, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTxHash(txNum uint32) ([]byte, error) {
|
|
// TODO: caching
|
|
handle, err := db.EnsureHandle(prefixes.TxHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewTxHashKey(txNum)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
return rawValue, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetActivation(txNum uint32, postition uint16) (uint32, error) {
|
|
return db.GetActivationFull(txNum, postition, false)
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetActivationFull(txNum uint32, postition uint16, isSupport bool) (uint32, error) {
|
|
var typ uint8
|
|
|
|
handle, err := db.EnsureHandle(prefixes.ActivatedClaimAndSupport)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if isSupport {
|
|
typ = prefixes.ActivatedSupportTXOType
|
|
} else {
|
|
typ = prefixes.ActivateClaimTXOType
|
|
}
|
|
|
|
key := prefixes.NewActivationKey(typ, txNum, postition)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.ActivationValueUnpack(rawValue)
|
|
// Does this need to explicitly return an int64, in case the uint32 overflows the max of an int?
|
|
return value.Height, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetClaimTxo(claim []byte) (*prefixes.ClaimToTXOValue, error) {
|
|
return db.GetCachedClaimTxo(claim, false)
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetCachedClaimTxo(claim []byte, useCache bool) (*prefixes.ClaimToTXOValue, error) {
|
|
// TODO: implement cache
|
|
handle, err := db.EnsureHandle(prefixes.ClaimToTXO)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewClaimToTXOKey(claim)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.ClaimToTXOValueUnpack(rawValue)
|
|
return value, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) ControllingClaimIter() <-chan *prefixes.PrefixRowKV {
|
|
handle, err := db.EnsureHandle(prefixes.ClaimTakeover)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
key := prefixes.NewClaimTakeoverKey("")
|
|
var rawKeyPrefix []byte = nil
|
|
rawKeyPrefix = key.PartialPack(0)
|
|
options := NewIterateOptions().WithDB(db).WithCfHandle(handle).WithPrefix(rawKeyPrefix)
|
|
options = options.WithIncludeValue(true) //.WithIncludeStop(true)
|
|
ch := IterCF(db.DB, options)
|
|
return ch
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetControllingClaim(name string) (*prefixes.ClaimTakeoverValue, error) {
|
|
handle, err := db.EnsureHandle(prefixes.ClaimTakeover)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Println(name)
|
|
key := prefixes.NewClaimTakeoverKey(name)
|
|
rawKey := key.PackKey()
|
|
log.Println(hex.EncodeToString(rawKey))
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.ClaimTakeoverValueUnpack(rawValue)
|
|
return value, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) FsGetClaimByHash(claimHash []byte) (*ResolveResult, error) {
|
|
claim, err := db.GetCachedClaimTxo(claimHash, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activation, err := db.GetActivation(claim.TxNum, claim.Position)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Printf("%#v\n%#v\n%#v\n", claim, hex.EncodeToString(claimHash), activation)
|
|
return PrepareResolveResult(
|
|
db,
|
|
claim.TxNum,
|
|
claim.Position,
|
|
claimHash,
|
|
claim.Name,
|
|
claim.RootTxNum,
|
|
claim.RootPosition,
|
|
activation,
|
|
claim.ChannelSignatureIsValid,
|
|
)
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTx(txhash *chainhash.Hash) ([]byte, *wire.MsgTx, error) {
|
|
// Lookup in MempoolTx first.
|
|
raw, tx, err := db.getMempoolTx(txhash)
|
|
if err == nil && raw != nil && tx != nil {
|
|
return raw, tx, err
|
|
}
|
|
|
|
handle, err := db.EnsureHandle(prefixes.Tx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key := prefixes.TxKey{Prefix: []byte{prefixes.Tx}, TxHash: txhash}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.TxValue{}
|
|
value.UnpackValue(rawValue)
|
|
var msgTx wire.MsgTx
|
|
err = msgTx.Deserialize(bytes.NewReader(value.RawTx))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return value.RawTx, &msgTx, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) getMempoolTx(txhash *chainhash.Hash) ([]byte, *wire.MsgTx, error) {
|
|
handle, err := db.EnsureHandle(prefixes.MempoolTx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key := prefixes.MempoolTxKey{Prefix: []byte{prefixes.Tx}, TxHash: txhash}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.MempoolTxValue{}
|
|
value.UnpackValue(rawValue)
|
|
var msgTx wire.MsgTx
|
|
err = msgTx.Deserialize(bytes.NewReader(value.RawTx))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return value.RawTx, &msgTx, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTxCount(height uint32) (*prefixes.TxCountValue, error) {
|
|
handle, err := db.EnsureHandle(prefixes.TxCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewTxCountKey(height)
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.TxCountValueUnpack(rawValue)
|
|
return value, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTxHeight(txhash *chainhash.Hash) (uint32, error) {
|
|
handle, err := db.EnsureHandle(prefixes.TxNum)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
key := prefixes.TxNumKey{Prefix: []byte{prefixes.TxNum}, TxHash: txhash}
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if slice.Size() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
// No slice copy needed. Value will be abandoned.
|
|
value := prefixes.TxNumValueUnpack(slice.Data())
|
|
height := stack.BisectRight(db.TxCounts, []uint32{value.TxNum})[0]
|
|
return height, nil
|
|
}
|
|
|
|
type TxMerkle struct {
|
|
TxHash *chainhash.Hash
|
|
RawTx []byte
|
|
Height int
|
|
Pos uint32
|
|
Merkle []*chainhash.Hash
|
|
}
|
|
|
|
// merklePath selects specific transactions by position within blockTxs.
|
|
// The resulting merkle path (aka merkle branch, or merkle) is a list of TX hashes
|
|
// which are in sibling relationship with TX nodes on the path to the root.
|
|
func merklePath(pos uint32, blockTxs, partial []*chainhash.Hash) []*chainhash.Hash {
|
|
parent := func(p uint32) uint32 {
|
|
return p >> 1
|
|
}
|
|
sibling := func(p uint32) uint32 {
|
|
if p%2 == 0 {
|
|
return p + 1
|
|
} else {
|
|
return p - 1
|
|
}
|
|
}
|
|
p := parent(pos)
|
|
if p == 0 {
|
|
// No parent, path is complete.
|
|
return partial
|
|
}
|
|
// Add sibling to partial path and proceed to parent TX.
|
|
return merklePath(p, blockTxs, append(partial, blockTxs[sibling(pos)]))
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetTxMerkle(tx_hashes []chainhash.Hash) ([]TxMerkle, error) {
|
|
|
|
selectedTxNum := make([]*IterOptions, 0, len(tx_hashes))
|
|
for _, txhash := range tx_hashes {
|
|
key := prefixes.TxNumKey{Prefix: []byte{prefixes.TxNum}, TxHash: &txhash}
|
|
log.Debugf("%v", key)
|
|
opt, err := db.selectFrom(key.Prefix, &key, &key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
selectedTxNum = append(selectedTxNum, opt...)
|
|
}
|
|
|
|
selectTxByTxNum := func(in []*prefixes.PrefixRowKV) ([]*IterOptions, error) {
|
|
txNumKey := in[0].Key.(*prefixes.TxNumKey)
|
|
log.Debugf("%v", txNumKey.TxHash.String())
|
|
out := make([]*IterOptions, 0, 100)
|
|
startKey := &prefixes.TxKey{
|
|
Prefix: []byte{prefixes.Tx},
|
|
TxHash: txNumKey.TxHash,
|
|
}
|
|
endKey := &prefixes.TxKey{
|
|
Prefix: []byte{prefixes.Tx},
|
|
TxHash: txNumKey.TxHash,
|
|
}
|
|
selectedTx, err := db.selectFrom([]byte{prefixes.Tx}, startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, selectedTx...)
|
|
return out, nil
|
|
}
|
|
|
|
blockTxsCache := make(map[uint32][]*chainhash.Hash)
|
|
results := make([]TxMerkle, 0, 500)
|
|
for kvs := range innerJoin(db.DB, iterate(db.DB, selectedTxNum), selectTxByTxNum) {
|
|
if err := checkForError(kvs); err != nil {
|
|
return results, err
|
|
}
|
|
txNumKey, txNumVal := kvs[0].Key.(*prefixes.TxNumKey), kvs[0].Value.(*prefixes.TxNumValue)
|
|
_, txVal := kvs[1].Key.(*prefixes.TxKey), kvs[1].Value.(*prefixes.TxValue)
|
|
txHeight := stack.BisectRight(db.TxCounts, []uint32{txNumVal.TxNum})[0]
|
|
txPos := txNumVal.TxNum - db.TxCounts.Get(txHeight-1)
|
|
// We need all the TX hashes in order to select out the relevant ones.
|
|
if _, ok := blockTxsCache[txHeight]; !ok {
|
|
txs, err := db.GetBlockTXs(txHeight)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
blockTxsCache[txHeight] = txs
|
|
}
|
|
blockTxs := blockTxsCache[txHeight]
|
|
results = append(results, TxMerkle{
|
|
TxHash: txNumKey.TxHash,
|
|
RawTx: txVal.RawTx,
|
|
Height: int(txHeight),
|
|
Pos: txPos,
|
|
Merkle: merklePath(txPos, blockTxs, []*chainhash.Hash{}),
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetClaimByID(claimID string) ([]*ExpandedResolveResult, []*ExpandedResolveResult, error) {
|
|
rows := make([]*ExpandedResolveResult, 0)
|
|
extras := make([]*ExpandedResolveResult, 0)
|
|
claimHash, err := hex.DecodeString(claimID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
stream, err := db.FsGetClaimByHash(claimHash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var res = NewExpandedResolveResult()
|
|
res.Stream = &optionalResolveResultOrError{res: stream}
|
|
rows = append(rows, res)
|
|
|
|
if stream != nil && stream.ChannelHash != nil {
|
|
channel, err := db.FsGetClaimByHash(stream.ChannelHash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var res = NewExpandedResolveResult()
|
|
res.Channel = &optionalResolveResultOrError{res: channel}
|
|
extras = append(extras, res)
|
|
}
|
|
|
|
if stream != nil && stream.RepostedClaimHash != nil {
|
|
repost, err := db.FsGetClaimByHash(stream.RepostedClaimHash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var res = NewExpandedResolveResult()
|
|
res.Repost = &optionalResolveResultOrError{res: repost}
|
|
extras = append(extras, res)
|
|
}
|
|
|
|
return rows, extras, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetDBState() (*prefixes.DBStateValue, error) {
|
|
handle, err := db.EnsureHandle(prefixes.DBState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewDBStateKey()
|
|
rawKey := key.PackKey()
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.DBStateValueUnpack(rawValue)
|
|
return value, nil
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) BidOrderNameIter(normalizedName string) <-chan *prefixes.PrefixRowKV {
|
|
handle, err := db.EnsureHandle(prefixes.BidOrder)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
key := prefixes.NewBidOrderKey(normalizedName)
|
|
var rawKeyPrefix []byte = nil
|
|
rawKeyPrefix = key.PartialPack(1)
|
|
options := NewIterateOptions().WithDB(db).WithCfHandle(handle).WithPrefix(rawKeyPrefix)
|
|
options = options.WithIncludeValue(true) //.WithIncludeStop(true)
|
|
ch := IterCF(db.DB, options)
|
|
return ch
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) ClaimShortIdIter(normalizedName string, claimId string) <-chan *prefixes.PrefixRowKV {
|
|
handle, err := db.EnsureHandle(prefixes.ClaimShortIdPrefix)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
key := prefixes.NewClaimShortIDKey(normalizedName, claimId)
|
|
var rawKeyPrefix []byte = nil
|
|
if claimId != "" {
|
|
rawKeyPrefix = key.PartialPack(2)
|
|
} else {
|
|
rawKeyPrefix = key.PartialPack(1)
|
|
}
|
|
options := NewIterateOptions().WithDB(db).WithCfHandle(handle).WithPrefix(rawKeyPrefix)
|
|
options = options.WithIncludeValue(true) //.WithIncludeStop(true)
|
|
ch := IterCF(db.DB, options)
|
|
return ch
|
|
}
|
|
|
|
func (db *ReadOnlyDBColumnFamily) GetCachedClaimHash(txNum uint32, position uint16) (*prefixes.TXOToClaimValue, error) {
|
|
// TODO: implement cache
|
|
handle, err := db.EnsureHandle(prefixes.TXOToClaim)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := prefixes.NewTXOToClaimKey(txNum, position)
|
|
rawKey := key.PackKey()
|
|
|
|
slice, err := db.DB.GetCF(db.Opts, handle, rawKey)
|
|
defer slice.Free()
|
|
if err != nil {
|
|
return nil, err
|
|
} else if slice.Size() == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rawValue := make([]byte, len(slice.Data()))
|
|
copy(rawValue, slice.Data())
|
|
value := prefixes.TXOToClaimValueUnpack(rawValue)
|
|
return value, nil
|
|
}
|
|
|
|
// GetBlockerHash get the hash of the blocker or filterer of the claim.
|
|
// TODO: this currently converts the byte arrays to strings, which is not
|
|
// very efficient. Might want to figure out a better way to do this.
|
|
func (db *ReadOnlyDBColumnFamily) GetBlockerHash(claimHash, repostedClaimHash, channelHash []byte) ([]byte, []byte, error) {
|
|
claimHashStr := string(claimHash)
|
|
respostedClaimHashStr := string(repostedClaimHash)
|
|
channelHashStr := string(channelHash)
|
|
|
|
var blockedHash []byte = nil
|
|
var filteredHash []byte = nil
|
|
|
|
blockedHash = db.BlockedStreams[claimHashStr]
|
|
if blockedHash == nil {
|
|
blockedHash = db.BlockedStreams[respostedClaimHashStr]
|
|
}
|
|
if blockedHash == nil {
|
|
blockedHash = db.BlockedChannels[claimHashStr]
|
|
}
|
|
if blockedHash == nil {
|
|
blockedHash = db.BlockedChannels[respostedClaimHashStr]
|
|
}
|
|
if blockedHash == nil {
|
|
blockedHash = db.BlockedChannels[channelHashStr]
|
|
}
|
|
|
|
filteredHash = db.FilteredStreams[claimHashStr]
|
|
if filteredHash == nil {
|
|
filteredHash = db.FilteredStreams[respostedClaimHashStr]
|
|
}
|
|
if filteredHash == nil {
|
|
filteredHash = db.FilteredChannels[claimHashStr]
|
|
}
|
|
if filteredHash == nil {
|
|
filteredHash = db.FilteredChannels[respostedClaimHashStr]
|
|
}
|
|
if filteredHash == nil {
|
|
filteredHash = db.FilteredChannels[channelHashStr]
|
|
}
|
|
|
|
return blockedHash, filteredHash, nil
|
|
}
|