github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/common/ledger/blkstorage/blockindex.go (about)

     1  /*
     2  Copyright hechain. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package blkstorage
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"path/filepath"
    13  	"unicode/utf8"
    14  
    15  	"github.com/golang/protobuf/proto"
    16  	"github.com/hechain20/hechain/common/ledger/snapshot"
    17  	"github.com/hechain20/hechain/common/ledger/util"
    18  	"github.com/hechain20/hechain/common/ledger/util/leveldbhelper"
    19  	"github.com/hechain20/hechain/internal/pkg/txflags"
    20  	"github.com/hyperledger/fabric-protos-go/common"
    21  	"github.com/hyperledger/fabric-protos-go/peer"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  const (
    26  	blockNumIdxKeyPrefix        = 'n'
    27  	blockHashIdxKeyPrefix       = 'h'
    28  	txIDIdxKeyPrefix            = 't'
    29  	blockNumTranNumIdxKeyPrefix = 'a'
    30  	indexSavePointKeyStr        = "indexCheckpointKey"
    31  
    32  	snapshotFileFormat       = byte(1)
    33  	snapshotDataFileName     = "txids.data"
    34  	snapshotMetadataFileName = "txids.metadata"
    35  )
    36  
    37  var (
    38  	indexSavePointKey              = []byte(indexSavePointKeyStr)
    39  	errIndexSavePointKeyNotPresent = errors.New("NoBlockIndexed")
    40  	errNilValue                    = errors.New("")
    41  	importTxIDsBatchSize           = uint64(10000) // txID is 64 bytes, so batch size roughly translates to 640KB
    42  )
    43  
    44  type blockIdxInfo struct {
    45  	blockNum  uint64
    46  	blockHash []byte
    47  	flp       *fileLocPointer
    48  	txOffsets []*txindexInfo
    49  	metadata  *common.BlockMetadata
    50  }
    51  
    52  type blockIndex struct {
    53  	indexItemsMap map[IndexableAttr]bool
    54  	db            *leveldbhelper.DBHandle
    55  }
    56  
    57  func newBlockIndex(indexConfig *IndexConfig, db *leveldbhelper.DBHandle) (*blockIndex, error) {
    58  	indexItems := indexConfig.AttrsToIndex
    59  	logger.Debugf("newBlockIndex() - indexItems:[%s]", indexItems)
    60  	indexItemsMap := make(map[IndexableAttr]bool)
    61  	for _, indexItem := range indexItems {
    62  		indexItemsMap[indexItem] = true
    63  	}
    64  	return &blockIndex{
    65  		indexItemsMap: indexItemsMap,
    66  		db:            db,
    67  	}, nil
    68  }
    69  
    70  func (index *blockIndex) getLastBlockIndexed() (uint64, error) {
    71  	var blockNumBytes []byte
    72  	var err error
    73  	if blockNumBytes, err = index.db.Get(indexSavePointKey); err != nil {
    74  		return 0, err
    75  	}
    76  	if blockNumBytes == nil {
    77  		return 0, errIndexSavePointKeyNotPresent
    78  	}
    79  	return decodeBlockNum(blockNumBytes), nil
    80  }
    81  
    82  func (index *blockIndex) indexBlock(blockIdxInfo *blockIdxInfo) error {
    83  	// do not index anything
    84  	if len(index.indexItemsMap) == 0 {
    85  		logger.Debug("Not indexing block... as nothing to index")
    86  		return nil
    87  	}
    88  	logger.Debugf("Indexing block [%s]", blockIdxInfo)
    89  	flp := blockIdxInfo.flp
    90  	txOffsets := blockIdxInfo.txOffsets
    91  	blkNum := blockIdxInfo.blockNum
    92  	blkHash := blockIdxInfo.blockHash
    93  	txsfltr := txflags.ValidationFlags(blockIdxInfo.metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER])
    94  	batch := index.db.NewUpdateBatch()
    95  	flpBytes, err := flp.marshal()
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	// Index1
   101  	if index.isAttributeIndexed(IndexableAttrBlockHash) {
   102  		batch.Put(constructBlockHashKey(blkHash), flpBytes)
   103  	}
   104  
   105  	// Index2
   106  	if index.isAttributeIndexed(IndexableAttrBlockNum) {
   107  		batch.Put(constructBlockNumKey(blkNum), flpBytes)
   108  	}
   109  
   110  	// Index3 Used to find a transaction by its transaction id
   111  	if index.isAttributeIndexed(IndexableAttrTxID) {
   112  		for i, txoffset := range txOffsets {
   113  			txFlp := newFileLocationPointer(flp.fileSuffixNum, flp.offset, txoffset.loc)
   114  			logger.Debugf("Adding txLoc [%s] for tx ID: [%s] to txid-index", txFlp, txoffset.txID)
   115  			txFlpBytes, marshalErr := txFlp.marshal()
   116  			if marshalErr != nil {
   117  				return marshalErr
   118  			}
   119  
   120  			indexVal := &TxIDIndexValue{
   121  				BlkLocation:      flpBytes,
   122  				TxLocation:       txFlpBytes,
   123  				TxValidationCode: int32(txsfltr.Flag(i)),
   124  			}
   125  			indexValBytes, err := proto.Marshal(indexVal)
   126  			if err != nil {
   127  				return errors.Wrap(err, "unexpected error while marshaling TxIDIndexValProto message")
   128  			}
   129  			batch.Put(
   130  				constructTxIDKey(txoffset.txID, blkNum, uint64(i)),
   131  				indexValBytes,
   132  			)
   133  		}
   134  	}
   135  
   136  	// Index4 - Store BlockNumTranNum will be used to query history data
   137  	if index.isAttributeIndexed(IndexableAttrBlockNumTranNum) {
   138  		for i, txoffset := range txOffsets {
   139  			txFlp := newFileLocationPointer(flp.fileSuffixNum, flp.offset, txoffset.loc)
   140  			logger.Debugf("Adding txLoc [%s] for tx number:[%d] ID: [%s] to blockNumTranNum index", txFlp, i, txoffset.txID)
   141  			txFlpBytes, marshalErr := txFlp.marshal()
   142  			if marshalErr != nil {
   143  				return marshalErr
   144  			}
   145  			batch.Put(constructBlockNumTranNumKey(blkNum, uint64(i)), txFlpBytes)
   146  		}
   147  	}
   148  
   149  	batch.Put(indexSavePointKey, encodeBlockNum(blockIdxInfo.blockNum))
   150  	// Setting snyc to true as a precaution, false may be an ok optimization after further testing.
   151  	if err := index.db.WriteBatch(batch, true); err != nil {
   152  		return err
   153  	}
   154  	return nil
   155  }
   156  
   157  func (index *blockIndex) isAttributeIndexed(attribute IndexableAttr) bool {
   158  	_, ok := index.indexItemsMap[attribute]
   159  	return ok
   160  }
   161  
   162  func (index *blockIndex) getBlockLocByHash(blockHash []byte) (*fileLocPointer, error) {
   163  	if !index.isAttributeIndexed(IndexableAttrBlockHash) {
   164  		return nil, errors.New("block hashes not maintained in index")
   165  	}
   166  	b, err := index.db.Get(constructBlockHashKey(blockHash))
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	if b == nil {
   171  		return nil, errors.Errorf("no such block hash [%x] in index", blockHash)
   172  	}
   173  	blkLoc := &fileLocPointer{}
   174  	if err := blkLoc.unmarshal(b); err != nil {
   175  		return nil, err
   176  	}
   177  	return blkLoc, nil
   178  }
   179  
   180  func (index *blockIndex) getBlockLocByBlockNum(blockNum uint64) (*fileLocPointer, error) {
   181  	if !index.isAttributeIndexed(IndexableAttrBlockNum) {
   182  		return nil, errors.New("block numbers not maintained in index")
   183  	}
   184  	b, err := index.db.Get(constructBlockNumKey(blockNum))
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	if b == nil {
   189  		return nil, errors.Errorf("no such block number [%d] in index", blockNum)
   190  	}
   191  	blkLoc := &fileLocPointer{}
   192  	if err := blkLoc.unmarshal(b); err != nil {
   193  		return nil, err
   194  	}
   195  	return blkLoc, nil
   196  }
   197  
   198  func (index *blockIndex) getTxLoc(txID string) (*fileLocPointer, error) {
   199  	v, _, err := index.getTxIDVal(txID)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	txFLP := &fileLocPointer{}
   204  	if err = txFLP.unmarshal(v.TxLocation); err != nil {
   205  		return nil, err
   206  	}
   207  	return txFLP, nil
   208  }
   209  
   210  func (index *blockIndex) getBlockLocByTxID(txID string) (*fileLocPointer, error) {
   211  	v, _, err := index.getTxIDVal(txID)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	blkFLP := &fileLocPointer{}
   216  	if err = blkFLP.unmarshal(v.BlkLocation); err != nil {
   217  		return nil, err
   218  	}
   219  	return blkFLP, nil
   220  }
   221  
   222  func (index *blockIndex) getTxValidationCodeByTxID(txID string) (peer.TxValidationCode, uint64, error) {
   223  	v, blkNum, err := index.getTxIDVal(txID)
   224  	if err != nil {
   225  		return peer.TxValidationCode(-1), 0, err
   226  	}
   227  	return peer.TxValidationCode(v.TxValidationCode), blkNum, nil
   228  }
   229  
   230  func (index *blockIndex) txIDExists(txID string) (bool, error) {
   231  	if !index.isAttributeIndexed(IndexableAttrTxID) {
   232  		return false, errors.New("transaction IDs not maintained in index")
   233  	}
   234  	rangeScan := constructTxIDRangeScan(txID)
   235  	itr, err := index.db.GetIterator(rangeScan.startKey, rangeScan.stopKey)
   236  	if err != nil {
   237  		return false, errors.WithMessagef(err, "error while trying to check the presence of TXID [%s]", txID)
   238  	}
   239  	defer itr.Release()
   240  
   241  	present := itr.Next()
   242  	if err := itr.Error(); err != nil {
   243  		return false, errors.Wrapf(err, "error while trying to check the presence of TXID [%s]", txID)
   244  	}
   245  	return present, nil
   246  }
   247  
   248  func (index *blockIndex) getTxIDVal(txID string) (*TxIDIndexValue, uint64, error) {
   249  	if !index.isAttributeIndexed(IndexableAttrTxID) {
   250  		return nil, 0, errors.New("transaction IDs not maintained in index")
   251  	}
   252  	rangeScan := constructTxIDRangeScan(txID)
   253  	itr, err := index.db.GetIterator(rangeScan.startKey, rangeScan.stopKey)
   254  	if err != nil {
   255  		return nil, 0, errors.WithMessagef(err, "error while trying to retrieve transaction info by TXID [%s]", txID)
   256  	}
   257  	defer itr.Release()
   258  
   259  	present := itr.Next()
   260  	if err := itr.Error(); err != nil {
   261  		return nil, 0, errors.Wrapf(err, "error while trying to retrieve transaction info by TXID [%s]", txID)
   262  	}
   263  	if !present {
   264  		return nil, 0, errors.Errorf("no such transaction ID [%s] in index", txID)
   265  	}
   266  	valBytes := itr.Value()
   267  	if len(valBytes) == 0 {
   268  		return nil, 0, errNilValue
   269  	}
   270  	val := &TxIDIndexValue{}
   271  	if err := proto.Unmarshal(valBytes, val); err != nil {
   272  		return nil, 0, errors.Wrapf(err, "unexpected error while unmarshalling bytes [%#v] into TxIDIndexValProto", valBytes)
   273  	}
   274  	blockNum, err := retrieveBlockNum(itr.Key(), len(rangeScan.startKey))
   275  	if err != nil {
   276  		return nil, 0, errors.WithMessage(err, "error while decoding block number from txID index key")
   277  	}
   278  	return val, blockNum, nil
   279  }
   280  
   281  func (index *blockIndex) getTXLocByBlockNumTranNum(blockNum uint64, tranNum uint64) (*fileLocPointer, error) {
   282  	if !index.isAttributeIndexed(IndexableAttrBlockNumTranNum) {
   283  		return nil, errors.New("<blockNumber, transactionNumber> tuple not maintained in index")
   284  	}
   285  	b, err := index.db.Get(constructBlockNumTranNumKey(blockNum, tranNum))
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	if b == nil {
   290  		return nil, errors.Errorf("no such blockNumber, transactionNumber <%d, %d> in index", blockNum, tranNum)
   291  	}
   292  	txFLP := &fileLocPointer{}
   293  	if err := txFLP.unmarshal(b); err != nil {
   294  		return nil, err
   295  	}
   296  	return txFLP, nil
   297  }
   298  
   299  func (index *blockIndex) exportUniqueTxIDs(dir string, newHashFunc snapshot.NewHashFunc) (map[string][]byte, error) {
   300  	if !index.isAttributeIndexed(IndexableAttrTxID) {
   301  		return nil, errors.New("transaction IDs not maintained in index")
   302  	}
   303  
   304  	dbItr, err := index.db.GetIterator([]byte{txIDIdxKeyPrefix}, []byte{txIDIdxKeyPrefix + 1})
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	defer dbItr.Release()
   309  
   310  	var previousTxID string
   311  	var numTxIDs uint64 = 0
   312  	var dataFile *snapshot.FileWriter
   313  	for dbItr.Next() {
   314  		if err := dbItr.Error(); err != nil {
   315  			return nil, errors.Wrap(err, "internal leveldb error while iterating for txids")
   316  		}
   317  		txID, err := retrieveTxID(dbItr.Key())
   318  		if err != nil {
   319  			return nil, err
   320  		}
   321  		// duplicate TxID may be present in the index
   322  		if previousTxID == txID {
   323  			continue
   324  		}
   325  		previousTxID = txID
   326  		if numTxIDs == 0 { // first iteration, create the data file
   327  			dataFile, err = snapshot.CreateFile(filepath.Join(dir, snapshotDataFileName), snapshotFileFormat, newHashFunc)
   328  			if err != nil {
   329  				return nil, err
   330  			}
   331  			defer dataFile.Close()
   332  		}
   333  		if err := dataFile.EncodeString(txID); err != nil {
   334  			return nil, err
   335  		}
   336  		numTxIDs++
   337  	}
   338  
   339  	if dataFile == nil {
   340  		return nil, nil
   341  	}
   342  
   343  	dataHash, err := dataFile.Done()
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	// create the metadata file
   349  	metadataFile, err := snapshot.CreateFile(filepath.Join(dir, snapshotMetadataFileName), snapshotFileFormat, newHashFunc)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	defer metadataFile.Close()
   354  
   355  	if err = metadataFile.EncodeUVarint(numTxIDs); err != nil {
   356  		return nil, err
   357  	}
   358  	metadataHash, err := metadataFile.Done()
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	return map[string][]byte{
   364  		snapshotDataFileName:     dataHash,
   365  		snapshotMetadataFileName: metadataHash,
   366  	}, nil
   367  }
   368  
   369  func importTxIDsFromSnapshot(
   370  	snapshotDir string,
   371  	lastBlockNumInSnapshot uint64,
   372  	db *leveldbhelper.DBHandle) error {
   373  	txIDsMetadata, err := snapshot.OpenFile(filepath.Join(snapshotDir, snapshotMetadataFileName), snapshotFileFormat)
   374  	if err != nil {
   375  		return err
   376  	}
   377  	numTxIDs, err := txIDsMetadata.DecodeUVarInt()
   378  	if err != nil {
   379  		return err
   380  	}
   381  	txIDsData, err := snapshot.OpenFile(filepath.Join(snapshotDir, snapshotDataFileName), snapshotFileFormat)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	batch := db.NewUpdateBatch()
   387  	for i := uint64(0); i < numTxIDs; i++ {
   388  		txID, err := txIDsData.DecodeString()
   389  		if err != nil {
   390  			return err
   391  		}
   392  		batch.Put(
   393  			constructTxIDKey(txID, lastBlockNumInSnapshot, uint64(i)),
   394  			[]byte{},
   395  		)
   396  		if (i+1)%importTxIDsBatchSize == 0 {
   397  			if err := db.WriteBatch(batch, true); err != nil {
   398  				return err
   399  			}
   400  			batch.Reset()
   401  		}
   402  	}
   403  	batch.Put(indexSavePointKey, encodeBlockNum(lastBlockNumInSnapshot))
   404  	if err := db.WriteBatch(batch, true); err != nil {
   405  		return err
   406  	}
   407  	return nil
   408  }
   409  
   410  func constructBlockNumKey(blockNum uint64) []byte {
   411  	blkNumBytes := util.EncodeOrderPreservingVarUint64(blockNum)
   412  	return append([]byte{blockNumIdxKeyPrefix}, blkNumBytes...)
   413  }
   414  
   415  func constructBlockHashKey(blockHash []byte) []byte {
   416  	return append([]byte{blockHashIdxKeyPrefix}, blockHash...)
   417  }
   418  
   419  func constructTxIDKey(txID string, blkNum, txNum uint64) []byte {
   420  	k := append(
   421  		[]byte{txIDIdxKeyPrefix},
   422  		util.EncodeOrderPreservingVarUint64(uint64(len(txID)))...,
   423  	)
   424  	k = append(k, txID...)
   425  	k = append(k, util.EncodeOrderPreservingVarUint64(blkNum)...)
   426  	return append(k, util.EncodeOrderPreservingVarUint64(txNum)...)
   427  }
   428  
   429  // retrieveTxID takes input an encoded txid key of the format `prefix:len(TxID):TxID:BlkNum:TxNum`
   430  // and returns the TxID from this
   431  func retrieveTxID(encodedTxIDKey []byte) (string, error) {
   432  	if len(encodedTxIDKey) == 0 {
   433  		return "", errors.New("invalid txIDKey - zero-length slice")
   434  	}
   435  	if encodedTxIDKey[0] != txIDIdxKeyPrefix {
   436  		return "", errors.Errorf("invalid txIDKey {%x} - unexpected prefix", encodedTxIDKey)
   437  	}
   438  	remainingBytes := encodedTxIDKey[utf8.RuneLen(txIDIdxKeyPrefix):]
   439  
   440  	txIDLen, n, err := util.DecodeOrderPreservingVarUint64(remainingBytes)
   441  	if err != nil {
   442  		return "", errors.WithMessagef(err, "invalid txIDKey {%x}", encodedTxIDKey)
   443  	}
   444  	remainingBytes = remainingBytes[n:]
   445  	if len(remainingBytes) <= int(txIDLen) {
   446  		return "", errors.Errorf("invalid txIDKey {%x}, fewer bytes present", encodedTxIDKey)
   447  	}
   448  	return string(remainingBytes[:int(txIDLen)]), nil
   449  }
   450  
   451  func retrieveBlockNum(encodedTxIDKey []byte, BlkNumStartingIndex int) (uint64, error) {
   452  	n, _, err := util.DecodeOrderPreservingVarUint64(encodedTxIDKey[BlkNumStartingIndex:])
   453  	return n, err
   454  }
   455  
   456  type rangeScan struct {
   457  	startKey []byte
   458  	stopKey  []byte
   459  }
   460  
   461  func constructTxIDRangeScan(txID string) *rangeScan {
   462  	sk := append(
   463  		[]byte{txIDIdxKeyPrefix},
   464  		util.EncodeOrderPreservingVarUint64(uint64(len(txID)))...,
   465  	)
   466  	sk = append(sk, txID...)
   467  	return &rangeScan{
   468  		startKey: sk,
   469  		stopKey:  append(sk, 0xff),
   470  	}
   471  }
   472  
   473  func constructBlockNumTranNumKey(blockNum uint64, txNum uint64) []byte {
   474  	blkNumBytes := util.EncodeOrderPreservingVarUint64(blockNum)
   475  	tranNumBytes := util.EncodeOrderPreservingVarUint64(txNum)
   476  	key := append(blkNumBytes, tranNumBytes...)
   477  	return append([]byte{blockNumTranNumIdxKeyPrefix}, key...)
   478  }
   479  
   480  func encodeBlockNum(blockNum uint64) []byte {
   481  	return proto.EncodeVarint(blockNum)
   482  }
   483  
   484  func decodeBlockNum(blockNumBytes []byte) uint64 {
   485  	blockNum, _ := proto.DecodeVarint(blockNumBytes)
   486  	return blockNum
   487  }
   488  
   489  type locPointer struct {
   490  	offset      int
   491  	bytesLength int
   492  }
   493  
   494  func (lp *locPointer) String() string {
   495  	return fmt.Sprintf("offset=%d, bytesLength=%d",
   496  		lp.offset, lp.bytesLength)
   497  }
   498  
   499  // fileLocPointer
   500  type fileLocPointer struct {
   501  	fileSuffixNum int
   502  	locPointer
   503  }
   504  
   505  func newFileLocationPointer(fileSuffixNum int, beginningOffset int, relativeLP *locPointer) *fileLocPointer {
   506  	flp := &fileLocPointer{fileSuffixNum: fileSuffixNum}
   507  	flp.offset = beginningOffset + relativeLP.offset
   508  	flp.bytesLength = relativeLP.bytesLength
   509  	return flp
   510  }
   511  
   512  func (flp *fileLocPointer) marshal() ([]byte, error) {
   513  	buffer := proto.NewBuffer([]byte{})
   514  	e := buffer.EncodeVarint(uint64(flp.fileSuffixNum))
   515  	if e != nil {
   516  		return nil, errors.Wrapf(e, "unexpected error while marshaling fileLocPointer [%s]", flp)
   517  	}
   518  	e = buffer.EncodeVarint(uint64(flp.offset))
   519  	if e != nil {
   520  		return nil, errors.Wrapf(e, "unexpected error while marshaling fileLocPointer [%s]", flp)
   521  	}
   522  	e = buffer.EncodeVarint(uint64(flp.bytesLength))
   523  	if e != nil {
   524  		return nil, errors.Wrapf(e, "unexpected error while marshaling fileLocPointer [%s]", flp)
   525  	}
   526  	return buffer.Bytes(), nil
   527  }
   528  
   529  func (flp *fileLocPointer) unmarshal(b []byte) error {
   530  	buffer := proto.NewBuffer(b)
   531  	i, e := buffer.DecodeVarint()
   532  	if e != nil {
   533  		return errors.Wrapf(e, "unexpected error while unmarshalling bytes [%#v] into fileLocPointer", b)
   534  	}
   535  	flp.fileSuffixNum = int(i)
   536  
   537  	i, e = buffer.DecodeVarint()
   538  	if e != nil {
   539  		return errors.Wrapf(e, "unexpected error while unmarshalling bytes [%#v] into fileLocPointer", b)
   540  	}
   541  	flp.offset = int(i)
   542  	i, e = buffer.DecodeVarint()
   543  	if e != nil {
   544  		return errors.Wrapf(e, "unexpected error while unmarshalling bytes [%#v] into fileLocPointer", b)
   545  	}
   546  	flp.bytesLength = int(i)
   547  	return nil
   548  }
   549  
   550  func (flp *fileLocPointer) String() string {
   551  	return fmt.Sprintf("fileSuffixNum=%d, %s", flp.fileSuffixNum, flp.locPointer.String())
   552  }
   553  
   554  func (blockIdxInfo *blockIdxInfo) String() string {
   555  	var buffer bytes.Buffer
   556  	for _, txOffset := range blockIdxInfo.txOffsets {
   557  		buffer.WriteString("txId=")
   558  		buffer.WriteString(txOffset.txID)
   559  		buffer.WriteString(" locPointer=")
   560  		buffer.WriteString(txOffset.loc.String())
   561  		buffer.WriteString("\n")
   562  	}
   563  	txOffsetsString := buffer.String()
   564  
   565  	return fmt.Sprintf("blockNum=%d, blockHash=%#v txOffsets=\n%s", blockIdxInfo.blockNum, blockIdxInfo.blockHash, txOffsetsString)
   566  }