github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/scrape/scrape_consolidate.go (about)

     1  package scrapePkg
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    13  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    14  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/file"
    15  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/index"
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    17  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/notify"
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    19  )
    20  
    21  const asciiAppearanceSize = 59
    22  
    23  // Consolidate calls into the block scraper to (a) call Blaze and (b) consolidate if applicable
    24  func (bm *BlazeManager) Consolidate(ctx context.Context, blocks []base.Blknum) error {
    25  	var err error
    26  	chain := bm.chain
    27  
    28  	backup := file.BackupFile{}
    29  	stageFn, _ := file.LatestFileInFolder(bm.StageFolder()) // it may not exist...
    30  	if file.FileExists(stageFn) {
    31  		backup, err = file.MakeBackup(filepath.Join(config.PathToCache(chain), "tmp"), stageFn)
    32  		if err != nil {
    33  			return errors.New("Could not create backup file: " + err.Error())
    34  		}
    35  		defer func() {
    36  			backup.Restore()
    37  		}()
    38  	}
    39  
    40  	// After this point if we fail the backup file will replace the original file, so
    41  	// we can safely remove these the stage file (and ripe files) and it will get replaced
    42  
    43  	// Some counters...
    44  	nAppsFound := 0
    45  	nAddrsFound := 0
    46  
    47  	// Load the stage into the map...
    48  	exists := file.FileExists(stageFn) // order matters
    49  	appMap, chunkRange, nAppearances := bm.AsciiFileToAppearanceMap(stageFn)
    50  	if !exists {
    51  		// Brand new stage.
    52  		chunkRange = base.FileRange{First: bm.meta.Finalized + 1, Last: blocks[0]}
    53  	}
    54  
    55  	// For each block...
    56  	nChunks := 0
    57  	for _, block := range blocks {
    58  		if ctx.Err() != nil {
    59  			// This means the context got cancelled, i.e. we got a SIGINT.
    60  			return nil
    61  		}
    62  		fn := fmt.Sprintf("%09d.txt", block)
    63  		if file.FileExists(filepath.Join(bm.UnripeFolder(), fn)) {
    64  			continue // skip unripe files
    65  		}
    66  
    67  		ripeFn := filepath.Join(bm.RipeFolder(), fn)
    68  		if !file.FileExists(ripeFn) {
    69  			if bm.hasNoAddresses(block) {
    70  				// this is okay -- see comment at the function
    71  			} else {
    72  				msg := fmt.Sprintf("ripe file not found for block %d", block) + spaces
    73  				if !bm.AllowMissing() {
    74  					_ = cleanEphemeralIndexFolders(chain)
    75  					return errors.New(msg)
    76  				} else {
    77  					logger.Warn(msg)
    78  				}
    79  			}
    80  		}
    81  
    82  		// Read in the ripe file, add it to the appMap and...
    83  		thisMap, _, thisCount := bm.AsciiFileToAppearanceMap(ripeFn)
    84  		nAppearances += thisCount
    85  		nAppsFound += thisCount
    86  		nAddrsFound += len(thisMap)
    87  		for addr, apps := range thisMap {
    88  			appMap[addr] = append(appMap[addr], apps...)
    89  		}
    90  		chunkRange.Last = block
    91  
    92  		// ...decide if we need to consolidate...
    93  		isSnap := bm.IsSnap(chunkRange.Last)            // Have we hit a snap point?
    94  		isOvertop := nAppearances >= int(bm.PerChunk()) // Does this block overtop a chunk?
    95  		if isSnap || isOvertop {
    96  			// Make a chunk - i.e., consolidate
    97  			chunkPath := filepath.Join(config.PathToIndex(chain), "finalized", chunkRange.String()+".bin")
    98  			publisher := base.ZeroAddr
    99  			var chunk index.Chunk
   100  			if report, err := chunk.Write(chain, publisher, chunkPath, appMap, nAppearances); err != nil {
   101  				// Remove file if it exists, because it might not be correct
   102  				_ = os.Remove(index.ToIndexPath(chunkPath))
   103  				return NewCriticalError(err)
   104  			} else if report == nil {
   105  				logger.Fatal("should not happen ==> write chunk returned empty report")
   106  			} else {
   107  				report.Snapped = isSnap
   108  				report.FileSize = file.FileSize(chunkPath)
   109  				report.Report()
   110  			}
   111  			if err = bm.opts.NotifyChunkWritten(chunk, chunkPath); err != nil {
   112  				return err
   113  			}
   114  
   115  			// reset for next chunk
   116  			bm.meta, _ = bm.opts.Conn.GetMetaData(bm.IsTestMode())
   117  			appMap = make(map[string][]types.AppRecord, 0)
   118  			chunkRange.First = chunkRange.Last + 1
   119  			chunkRange.Last = chunkRange.Last + 1
   120  			nAppearances = 0
   121  			nChunks++
   122  		}
   123  	}
   124  
   125  	var newRange base.FileRange
   126  	if len(appMap) > 0 { // are there any appearances in this block range?
   127  		newRange = base.FileRange{First: bm.meta.Finalized + 1, Last: 0}
   128  
   129  		// We need an array because we're going to write it back to disc
   130  		appearances := make([]string, 0, nAppearances)
   131  		for addr, apps := range appMap {
   132  			for _, app := range apps {
   133  				if ctx.Err() != nil {
   134  					// This means the context got cancelled, i.e. we got a SIGINT.
   135  					return nil
   136  				}
   137  				record := fmt.Sprintf("%s\t%09d\t%05d", addr, app.BlockNumber, app.TransactionIndex)
   138  				appearances = append(appearances, record)
   139  				newRange.Last = base.Max(newRange.Last, base.Blknum(app.BlockNumber))
   140  			}
   141  		}
   142  
   143  		// The stage needs to be sorted because the end user queries it and we want the search to be fast
   144  		sort.Strings(appearances)
   145  
   146  		stageFn = filepath.Join(bm.StageFolder(), fmt.Sprintf("%s.txt", newRange))
   147  		if err := file.LinesToAsciiFile(stageFn, appearances); err != nil {
   148  			os.Remove(stageFn)
   149  			return err
   150  		}
   151  	}
   152  
   153  	// Let the user know what happened...
   154  	nAppsNow := int(file.FileSize(stageFn) / asciiAppearanceSize)
   155  	bm.report(len(blocks), int(bm.PerChunk()), nChunks, nAppsNow, nAppsFound, nAddrsFound)
   156  
   157  	if bm.opts.Notify {
   158  		if err := Notify(notify.Notification[string]{
   159  			Msg:     notify.MessageStageUpdated,
   160  			Meta:    bm.meta,
   161  			Payload: newRange.String(),
   162  		}); err != nil {
   163  			return err
   164  		}
   165  	}
   166  
   167  	// Commit the change by deleting the backup file.
   168  	backup.Clear()
   169  
   170  	return nil
   171  }
   172  
   173  // AsciiFileToAppearanceMap reads the appearances from the stage file and returns them as a map
   174  func (bm *BlazeManager) AsciiFileToAppearanceMap(fn string) (map[string][]types.AppRecord, base.FileRange, int) {
   175  	appearances := file.AsciiFileToLines(fn)
   176  	os.Remove(fn) // It's okay to remove this. If it fails, we'll just start over.
   177  
   178  	appMap := make(map[string][]types.AppRecord, len(appearances))
   179  	fileRange := base.FileRange{First: base.NOPOSN, Last: 0}
   180  
   181  	if len(appearances) == 0 {
   182  		return appMap, base.FileRange{First: 0, Last: 0}, 0
   183  	}
   184  
   185  	nAdded := 0
   186  	for _, line := range appearances {
   187  		parts := strings.Split(line, "\t")
   188  		if len(parts) == 3 { // shouldn't be needed, but just in case...
   189  			addr := strings.ToLower(parts[0])
   190  			bn := base.MustParseBlknum(strings.TrimLeft(parts[1], "0"))
   191  			txid := base.MustParseTxnum(strings.TrimLeft(parts[2], "0"))
   192  			// See #3252
   193  			if addr == base.SentinalAddr.Hex() && txid == types.MisconfigReward {
   194  				continue
   195  			}
   196  			fileRange.First = base.Min(fileRange.First, bn)
   197  			fileRange.Last = base.Max(fileRange.Last, bn)
   198  			appMap[addr] = append(appMap[addr], types.AppRecord{
   199  				BlockNumber:      uint32(bn),
   200  				TransactionIndex: uint32(txid),
   201  			})
   202  			nAdded++
   203  		}
   204  	}
   205  
   206  	return appMap, fileRange, nAdded
   207  }
   208  
   209  // hasNoAddresses returns true if (a) the miner is zero, (b) there are no transactions, uncles, or withdrawals.
   210  // (It is truly a block with no addresses -- for example block 15537860 on mainnet.)
   211  func (bm *BlazeManager) hasNoAddresses(bn base.Blknum) bool {
   212  	if block, err := bm.opts.Conn.GetBlockHeaderByNumber(bn); err != nil {
   213  		return false
   214  	} else {
   215  		return base.IsPrecompile(block.Miner.Hex()) &&
   216  			len(block.Transactions) == 0 &&
   217  			len(block.Uncles) == 0 &&
   218  			len(block.Withdrawals) == 0
   219  	}
   220  }