diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65139178337..f6d7deb2d39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,7 @@ on: - 'release/**' pull_request: branches: - - main - - 'release/**' + - '**' types: - opened - reopened diff --git a/cmd/commitment-prefix/main.go b/cmd/commitment-prefix/main.go index f1f7cd69b1d..464b78e8a26 100644 --- a/cmd/commitment-prefix/main.go +++ b/cmd/commitment-prefix/main.go @@ -17,6 +17,7 @@ package main import ( + "bytes" "errors" "flag" "fmt" @@ -42,6 +43,8 @@ var ( flagConcurrency = flag.Int("j", 4, "amount of concurrently proceeded files") flagTrieVariant = flag.String("trie", "hex", "commitment trie variant (values are hex and bin)") flagCompression = flag.String("compression", "none", "compression type (none, k, v, kv)") + flagPrintState = flag.Bool("state", false, "print state of file") + flagDepth = flag.Int("depth", 0, "depth of the prefixes to analyze") ) func main() { @@ -69,9 +72,10 @@ func proceedFiles(files []string) { for i, fp := range files { fpath, pos := fp, i + _ = pos <-sema - fmt.Printf("\r[%d/%d] - %s..", pos+1, len(files), path.Base(fpath)) + fmt.Printf("[%d/%d] - %s..", pos+1, len(files), path.Base(fpath)) wg.Add(1) go func(wg *sync.WaitGroup, mu *sync.Mutex) { @@ -86,9 +90,9 @@ func proceedFiles(files []string) { mu.Lock() page.AddCharts( - prefixLenCountChart(fpath, stat), countersChart(fpath, stat), + mediansChart(fpath, stat), fileContentsMapChart(fpath, stat), ) mu.Unlock() @@ -96,6 +100,9 @@ func proceedFiles(files []string) { } wg.Wait() fmt.Println() + if *flagPrintState { + return + } dir := filepath.Dir(files[0]) if *flagOutputDirectory != "" { @@ -180,21 +187,39 @@ func extractKVPairFromCompressed(filename string, keysSink chan commitment.Branc size := dec.Size() paris := dec.Count() / 2 cpair := 0 - + depth := *flagDepth + var afterValPos uint64 + var key, val []byte getter := seg.NewReader(dec.MakeGetter(), fc) for getter.HasNext() { - key, _ := getter.Next(nil) + key, _ = getter.Next(key[:0]) if !getter.HasNext() { return errors.New("invalid key/value pair during decompression") } - val, afterValPos := getter.Next(nil) + if *flagPrintState && !bytes.Equal(key, []byte("state")) { + getter.Skip() + continue + } + + val, afterValPos = getter.Next(val[:0]) cpair++ + if bytes.Equal(key, []byte("state")) { + str, err := commitment.HexTrieStateToString(val) + if err != nil { + fmt.Printf("[ERR] failed to decode state: %v", err) + } + fmt.Printf("\n%s: %s\n", dec.FileName(), str) + continue + } if cpair%100000 == 0 { fmt.Printf("\r%s pair %d/%d %s/%s", filename, cpair, paris, datasize.ByteSize(afterValPos).HumanReadable(), datasize.ByteSize(size).HumanReadable()) } + if depth > len(key) { + continue + } stat := commitment.DecodeBranchAndCollectStat(key, val, tv) if stat == nil { fmt.Printf("failed to decode branch: %x %x\n", key, val) @@ -252,7 +277,7 @@ func prefixLenCountChart(fname string, data *overallStat) *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTooltipOpts(opts.Tooltip{Show: true}), - charts.WithTitleOpts(opts.Title{Subtitle: fname, Title: "key prefix length distribution (bytes)", Top: "25"}), + charts.WithTitleOpts(opts.Title{Subtitle: filepath.Base(fname), Title: "key prefix length distribution (bytes)", Top: "25"}), ) pie.AddSeries("prefixLen/count", items) @@ -285,13 +310,17 @@ func fileContentsMapChart(fileName string, data *overallStat) *charts.TreeMap { Value: int(data.branches.ExtSize), }, { - Name: "apk", + Name: "accountKey", Value: int(data.branches.APKSize), }, { - Name: "spk", + Name: "storageKey", Value: int(data.branches.SPKSize), }, + { + Name: "leafHashes", + Value: int(data.branches.LeafHashSize), + }, } graph := charts.NewTreeMap() @@ -305,12 +334,10 @@ func fileContentsMapChart(fileName string, data *overallStat) *charts.TreeMap { ) // Add initialized data to graph. - graph.AddSeries(fileName, TreeMap). + graph.AddSeries(filepath.Base(fileName), TreeMap). SetSeriesOptions( charts.WithTreeMapOpts( opts.TreeMapChart{ - Animation: true, - //Roam: true, UpperLabel: &opts.UpperLabel{Show: true, Color: "#fff"}, Levels: &[]opts.TreeMapLevel{ { // Series @@ -383,9 +410,6 @@ func countersChart(fname string, data *overallStat) *charts.Sankey { sankey.SetGlobalOptions( charts.WithLegendOpts(opts.Legend{Show: true}), charts.WithTooltipOpts(opts.Tooltip{Show: true}), - //charts.WithTitleOpts(opts.Title{ - // Title: "Sankey-basic-example", - //}), ) nodes := []opts.SankeyNode{ @@ -394,15 +418,53 @@ func countersChart(fname string, data *overallStat) *charts.Sankey { {Name: "SPK"}, {Name: "Hashes"}, {Name: "Extensions"}, + {Name: "LeafHashes"}, } sankeyLink := []opts.SankeyLink{ {Source: nodes[0].Name, Target: nodes[1].Name, Value: float32(data.branches.APKCount)}, {Source: nodes[0].Name, Target: nodes[2].Name, Value: float32(data.branches.SPKCount)}, {Source: nodes[0].Name, Target: nodes[3].Name, Value: float32(data.branches.HashCount)}, {Source: nodes[0].Name, Target: nodes[4].Name, Value: float32(data.branches.ExtCount)}, + {Source: nodes[0].Name, Target: nodes[5].Name, Value: float32(data.branches.LeafHashCount)}, + } + + sankey.AddSeries("Counts "+filepath.Base(fname), nodes, sankeyLink). + SetSeriesOptions( + charts.WithLineStyleOpts(opts.LineStyle{ + Color: "source", + Curveness: 0.5, + }), + charts.WithLabelOpts(opts.Label{ + Show: true, + }), + ) + return sankey +} + +func mediansChart(fname string, data *overallStat) *charts.Sankey { + sankey := charts.NewSankey() + sankey.SetGlobalOptions( + charts.WithLegendOpts(opts.Legend{Show: true}), + charts.WithTooltipOpts(opts.Tooltip{Show: true}), + ) + + nodes := []opts.SankeyNode{ + {Name: "Cells"}, + {Name: "Addr"}, + {Name: "Addr+Storage"}, + {Name: "Hashes"}, + {Name: "Extensions"}, + {Name: "LeafHashes"}, + } + sankeyLink := []opts.SankeyLink{ + {Source: nodes[0].Name, Target: nodes[1].Name, Value: float32(data.branches.MedianAPK)}, + {Source: nodes[0].Name, Target: nodes[2].Name, Value: float32(data.branches.MedianSPK)}, + {Source: nodes[0].Name, Target: nodes[3].Name, Value: float32(data.branches.MedianHash)}, + {Source: nodes[0].Name, Target: nodes[4].Name, Value: float32(data.branches.MedianExt)}, + {Source: nodes[0].Name, Target: nodes[5].Name, Value: float32(data.branches.MedianLH)}, } - sankey.AddSeries(fname, nodes, sankeyLink). + sankey.AddSeries("Medians "+filepath.Base(fname), nodes, sankeyLink). SetSeriesOptions( charts.WithLineStyleOpts(opts.LineStyle{ Color: "source", diff --git a/cmd/integration/commands/stages.go b/cmd/integration/commands/stages.go index 623a1a47378..0b3cd33ba82 100644 --- a/cmd/integration/commands/stages.go +++ b/cmd/integration/commands/stages.go @@ -254,7 +254,7 @@ var cmdStageCustomTrace = &cobra.Command{ } var cmdStagePatriciaTrie = &cobra.Command{ - Use: "rebuild_trie3_files", + Use: "commitment_rebuild", Short: "", Run: func(cmd *cobra.Command, args []string) { logger := debug.SetupCobra(cmd, "integration") @@ -1178,20 +1178,15 @@ func stagePatriciaTrie(db kv.RwDB, ctx context.Context, logger log.Logger) error if reset { return reset2.Reset(ctx, db, stages.Execution) } - tx, err := db.BeginRw(ctx) - if err != nil { - return err - } - defer tx.Rollback() br, _ := blocksIO(db, logger) historyV3 := true cfg := stagedsync.StageTrieCfg(db, true /* checkRoot */, true /* saveHashesToDb */, false /* badBlockHalt */, dirs.Tmp, br, nil /* hd */, historyV3, agg) - if _, err := stagedsync.RebuildPatriciaTrieBasedOnFiles(tx, cfg, ctx, logger); err != nil { + if _, err := stagedsync.RebuildPatriciaTrieBasedOnFiles(ctx, cfg); err != nil { return err } - return tx.Commit() + return nil } func stageTxLookup(db kv.RwDB, ctx context.Context, logger log.Logger) error { diff --git a/erigon-lib/commitment/commitment.go b/erigon-lib/commitment/commitment.go index bfa01ff0226..2d7e6c316c5 100644 --- a/erigon-lib/commitment/commitment.go +++ b/erigon-lib/commitment/commitment.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "math/bits" + "sort" "strings" "github.com/holiman/uint256" @@ -41,8 +42,43 @@ import ( ) var ( - mxKeys = metrics.GetOrCreateCounter("domain_commitment_keys") - mxBranchUpdatesApplied = metrics.GetOrCreateCounter("domain_commitment_updates_applied") + mxTrieProcessedKeys = metrics.GetOrCreateCounter("domain_commitment_keys") + mxTrieBranchesUpdated = metrics.GetOrCreateCounter("domain_commitment_updates_applied") + + mxTrieStateSkipRate = metrics.GetOrCreateCounter("trie_state_skip_rate") + mxTrieStateLoadRate = metrics.GetOrCreateCounter("trie_state_load_rate") + mxTrieStateLevelledSkipRatesAccount = [...]metrics.Counter{ + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L0",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L1",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L2",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L3",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L4",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="recent",key="account"}`), + } + mxTrieStateLevelledSkipRatesStorage = [...]metrics.Counter{ + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L0",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L1",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L2",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L3",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="L4",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_skip_rate{level="recent",key="storage"}`), + } + mxTrieStateLevelledLoadRatesAccount = [...]metrics.Counter{ + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L0",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L1",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L2",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L3",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L4",key="account"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="recent",key="account"}`), + } + mxTrieStateLevelledLoadRatesStorage = [...]metrics.Counter{ + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L0",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L1",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L2",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L3",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="L4",key="storage"}`), + metrics.GetOrCreateCounter(`trie_state_levelled_load_rate{level="recent",key="storage"}`), + } ) // Trie represents commitment variant. @@ -113,54 +149,25 @@ const ( fieldAccountAddr cellFields = 2 fieldStorageAddr cellFields = 4 fieldHash cellFields = 8 + fieldStateHash cellFields = 16 ) -type BranchData []byte - -func (branchData BranchData) String() string { - if len(branchData) == 0 { - return "" - } - touchMap := binary.BigEndian.Uint16(branchData[0:]) - afterMap := binary.BigEndian.Uint16(branchData[2:]) - pos := 4 +func (p cellFields) String() string { var sb strings.Builder - var cell cell - fmt.Fprintf(&sb, "touchMap %016b, afterMap %016b\n", touchMap, afterMap) - for bitset, j := touchMap, 0; bitset != 0; j++ { - bit := bitset & -bitset - nibble := bits.TrailingZeros16(bit) - fmt.Fprintf(&sb, " %x => ", nibble) - if afterMap&bit == 0 { - sb.WriteString("{DELETED}\n") - } else { - fieldBits := cellFields(branchData[pos]) - pos++ - var err error - if pos, err = cell.fillFromFields(branchData, pos, fieldBits); err != nil { - // This is used for test output, so ok to panic - panic(err) - } - sb.WriteString("{") - var comma string - if cell.hashedExtLen > 0 { - fmt.Fprintf(&sb, "hashedKey=[%x]", cell.hashedExtension[:cell.hashedExtLen]) - comma = "," - } - if cell.accountAddrLen > 0 { - fmt.Fprintf(&sb, "%saccountPlainKey=[%x]", comma, cell.accountAddr[:cell.accountAddrLen]) - comma = "," - } - if cell.storageAddrLen > 0 { - fmt.Fprintf(&sb, "%sstoragePlainKey=[%x]", comma, cell.storageAddr[:cell.storageAddrLen]) - comma = "," - } - if cell.hashLen > 0 { - fmt.Fprintf(&sb, "%shash=[%x]", comma, cell.hash[:cell.hashLen]) - } - sb.WriteString("}\n") - } - bitset ^= bit + if p&fieldExtension != 0 { + sb.WriteString("DownHash") + } + if p&fieldAccountAddr != 0 { + sb.WriteString("+AccountPlain") + } + if p&fieldStorageAddr != 0 { + sb.WriteString("+StoragePlain") + } + if p&fieldHash != 0 { + sb.WriteString("+Hash") + } + if p&fieldStateHash != 0 { + sb.WriteString("+LeafHash") } return sb.String() } @@ -187,8 +194,9 @@ func (be *BranchEncoder) initCollector() { if be.updates != nil { be.updates.Close() } - be.updates = etl.NewCollector("commitment.BranchEncoder", be.tmpdir, etl.NewOldestEntryBuffer(etl.BufferOptimalSize/2), log.Root().New("branch-encoder")) + be.updates = etl.NewCollector("commitment.BranchEncoder", be.tmpdir, etl.NewOldestEntryBuffer(etl.BufferOptimalSize/4), log.Root().New("branch-encoder")) be.updates.LogLvl(log.LvlDebug) + be.updates.SortAndFlushInBackground(true) } func (be *BranchEncoder) Load(pc PatriciaContext, args etl.TransformArgs) error { @@ -207,7 +215,7 @@ func (be *BranchEncoder) Load(pc PatriciaContext, args etl.TransformArgs) error if err = pc.PutBranch(cp, cu, stateValue, stateStep); err != nil { return err } - mxBranchUpdatesApplied.Inc() + mxTrieBranchesUpdated.Inc() return nil }, args); err != nil { return err @@ -223,19 +231,18 @@ func (be *BranchEncoder) CollectUpdate( readCell func(nibble int, skip bool) (*cell, error), ) (lastNibble int, err error) { - var update []byte - update, lastNibble, err = be.EncodeBranch(bitmap, touchMap, afterMap, readCell) + prev, prevStep, err := ctx.Branch(prefix) if err != nil { return 0, err } - - prev, prevStep, err := ctx.Branch(prefix) - _ = prevStep + update, lastNibble, err := be.EncodeBranch(bitmap, touchMap, afterMap, readCell) if err != nil { return 0, err } + if len(prev) > 0 { if bytes.Equal(prev, update) { + //fmt.Printf("skip collectBranchUpdate [%x]\n", prefix) return lastNibble, nil // do not write the same data for prefix } update, err = be.merger.Merge(prev, update) @@ -243,12 +250,11 @@ func (be *BranchEncoder) CollectUpdate( return 0, err } } - //fmt.Printf("collectBranchUpdate [%x] -> [%x]\n", prefix, update) + //fmt.Printf("\ncollectBranchUpdate [%x] -> %s\n", prefix, BranchData(update).String()) // has to copy :( if err = ctx.PutBranch(common.Copy(prefix), common.Copy(update), prev, prevStep); err != nil { return 0, err } - mxBranchUpdatesApplied.Inc() return lastNibble, nil } @@ -312,6 +318,9 @@ func (be *BranchEncoder) EncodeBranch(bitmap, touchMap, afterMap uint16, readCel if cell.hashLen > 0 { fields |= fieldHash } + if cell.stateHashLen == 32 && (cell.accountAddrLen > 0 || cell.storageAddrLen > 0) { + fields |= fieldStateHash + } if err := be.buf.WriteByte(byte(fields)); err != nil { return nil, 0, err } @@ -335,15 +344,76 @@ func (be *BranchEncoder) EncodeBranch(bitmap, touchMap, afterMap uint16, readCel return nil, 0, err } } + if fields&fieldStateHash != 0 { + if err := putUvarAndVal(uint64(cell.stateHashLen), cell.stateHash[:cell.stateHashLen]); err != nil { + return nil, 0, err + } + } } bitset ^= bit } + res := make([]byte, be.buf.Len()) + copy(res, be.buf.Bytes()) + //fmt.Printf("EncodeBranch [%x] size: %d\n", be.buf.Bytes(), be.buf.Len()) - return be.buf.Bytes(), lastNibble, nil + return res, lastNibble, nil } func RetrieveCellNoop(nibble int, skip bool) (*cell, error) { return nil, nil } +type BranchData []byte + +func (branchData BranchData) String() string { + if len(branchData) == 0 { + return "" + } + touchMap := binary.BigEndian.Uint16(branchData[0:]) + afterMap := binary.BigEndian.Uint16(branchData[2:]) + pos := 4 + var sb strings.Builder + var cell cell + fmt.Fprintf(&sb, "touchMap %016b, afterMap %016b\n", touchMap, afterMap) + for bitset, j := touchMap, 0; bitset != 0; j++ { + bit := bitset & -bitset + nibble := bits.TrailingZeros16(bit) + fmt.Fprintf(&sb, " %x => ", nibble) + if afterMap&bit == 0 { + sb.WriteString("{DELETED}\n") + } else { + fields := cellFields(branchData[pos]) + pos++ + var err error + if pos, err = cell.fillFromFields(branchData, pos, fields); err != nil { + // This is used for test output, so ok to panic + panic(err) + } + sb.WriteString("{") + var comma string + if cell.hashedExtLen > 0 { + fmt.Fprintf(&sb, "hashedExtension=[%x]", cell.hashedExtension[:cell.hashedExtLen]) + comma = "," + } + if cell.accountAddrLen > 0 { + fmt.Fprintf(&sb, "%saccountAddr=[%x]", comma, cell.accountAddr[:cell.accountAddrLen]) + comma = "," + } + if cell.storageAddrLen > 0 { + fmt.Fprintf(&sb, "%sstorageAddr=[%x]", comma, cell.storageAddr[:cell.storageAddrLen]) + comma = "," + } + if cell.hashLen > 0 { + fmt.Fprintf(&sb, "%shash=[%x]", comma, cell.hash[:cell.hashLen]) + } + if cell.stateHashLen > 0 { + fmt.Fprintf(&sb, "%sleafHash=[%x]", comma, cell.stateHash[:cell.stateHashLen]) + } + sb.WriteString("}\n") + } + bitset ^= bit + } + return sb.String() +} + // if fn returns nil, the original key will be copied from branchData func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte, isStorage bool) (newKey []byte, err error)) (BranchData, error) { if len(branchData) < 4 { @@ -360,10 +430,10 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte newData = append(newData[:0], branchData[:4]...) for bitset, j := touchMap&afterMap, 0; bitset != 0; j++ { bit := bitset & -bitset - fieldBits := cellFields(branchData[pos]) - newData = append(newData, byte(fieldBits)) + fields := cellFields(branchData[pos]) + newData = append(newData, byte(fields)) pos++ - if fieldBits&fieldExtension != 0 { + if fields&fieldExtension != 0 { l, n := binary.Uvarint(branchData[pos:]) if n == 0 { return nil, errors.New("replacePlainKeys buffer too small for hashedKey len") @@ -373,14 +443,14 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte newData = append(newData, branchData[pos:pos+n]...) pos += n if len(branchData) < pos+int(l) { - return nil, errors.New("replacePlainKeys buffer too small for hashedKey") + return nil, fmt.Errorf("replacePlainKeys buffer too small for hashedKey: expected %d got %d", pos+int(l), len(branchData)) } if l > 0 { newData = append(newData, branchData[pos:pos+int(l)]...) pos += int(l) } } - if fieldBits&fieldAccountAddr != 0 { + if fields&fieldAccountAddr != 0 { l, n := binary.Uvarint(branchData[pos:]) if n == 0 { return nil, errors.New("replacePlainKeys buffer too small for accountAddr len") @@ -389,7 +459,7 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte } pos += n if len(branchData) < pos+int(l) { - return nil, errors.New("replacePlainKeys buffer too small for accountAddr") + return nil, fmt.Errorf("replacePlainKeys buffer too small for accountAddr: expected %d got %d", pos+int(l), len(branchData)) } if l > 0 { pos += int(l) @@ -400,20 +470,20 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte } if newKey == nil { newData = append(newData, branchData[pos-int(l)-n:pos]...) - if l != length.Addr { - fmt.Printf("COPY %x LEN %d\n", []byte(branchData[pos-int(l):pos]), l) - } + //if l != length.Addr { + // fmt.Printf("COPY %x LEN %d\n", []byte(branchData[pos-int(l):pos]), l) + //} } else { - if len(newKey) > 8 && len(newKey) != length.Addr { - fmt.Printf("SHORT %x LEN %d\n", newKey, len(newKey)) - } + //if len(newKey) > 8 && len(newKey) != length.Addr { + // fmt.Printf("SHORT %x LEN %d\n", newKey, len(newKey)) + //} n = binary.PutUvarint(numBuf[:], uint64(len(newKey))) newData = append(newData, numBuf[:n]...) newData = append(newData, newKey...) } } - if fieldBits&fieldStorageAddr != 0 { + if fields&fieldStorageAddr != 0 { l, n := binary.Uvarint(branchData[pos:]) if n == 0 { return nil, errors.New("replacePlainKeys buffer too small for storageAddr len") @@ -422,7 +492,7 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte } pos += n if len(branchData) < pos+int(l) { - return nil, errors.New("replacePlainKeys buffer too small for storageAddr") + return nil, fmt.Errorf("replacePlainKeys buffer too small for storageAddr: expected %d got %d", pos+int(l), len(branchData)) } if l > 0 { pos += int(l) @@ -446,7 +516,7 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte newData = append(newData, newKey...) } } - if fieldBits&fieldHash != 0 { + if fields&fieldHash != 0 { l, n := binary.Uvarint(branchData[pos:]) if n == 0 { return nil, errors.New("replacePlainKeys buffer too small for hash len") @@ -456,13 +526,31 @@ func (branchData BranchData) ReplacePlainKeys(newData []byte, fn func(key []byte newData = append(newData, branchData[pos:pos+n]...) pos += n if len(branchData) < pos+int(l) { - return nil, errors.New("replacePlainKeys buffer too small for hash") + return nil, fmt.Errorf("replacePlainKeys buffer too small for hash: expected %d got %d", pos+int(l), len(branchData)) + } + if l > 0 { + newData = append(newData, branchData[pos:pos+int(l)]...) + pos += int(l) + } + } + if fields&fieldStateHash != 0 { + l, n := binary.Uvarint(branchData[pos:]) + if n == 0 { + return nil, errors.New("replacePlainKeys buffer too small for acLeaf hash len") + } else if n < 0 { + return nil, errors.New("replacePlainKeys value overflow for acLeafhash len") + } + newData = append(newData, branchData[pos:pos+n]...) + pos += n + if len(branchData) < pos+int(l) { + return nil, fmt.Errorf("replacePlainKeys buffer too small for LeafHash: expected %d got %d", pos+int(l), len(branchData)) } if l > 0 { newData = append(newData, branchData[pos:pos+int(l)]...) pos += int(l) } } + bitset ^= bit } @@ -504,10 +592,10 @@ func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData [] bit := bitset & -bitset if bitmap2&bit != 0 { // Add fields from branchData2 - fieldBits := cellFields(branchData2[pos2]) - newData = append(newData, byte(fieldBits)) + fields := cellFields(branchData2[pos2]) + newData = append(newData, byte(fields)) pos2++ - for i := 0; i < bits.OnesCount8(byte(fieldBits)); i++ { + for i := 0; i < bits.OnesCount8(byte(fields)); i++ { l, n := binary.Uvarint(branchData2[pos2:]) if n == 0 { return nil, errors.New("MergeHexBranches buffer2 too small for field") @@ -517,7 +605,7 @@ func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData [] newData = append(newData, branchData2[pos2:pos2+n]...) pos2 += n if len(branchData2) < pos2+int(l) { - return nil, errors.New("MergeHexBranches buffer2 too small for field") + return nil, fmt.Errorf("MergeHexBranches buffer2 too small for %s : expected %d got %d", fields&cellFields(1< 0 { newData = append(newData, branchData2[pos2:pos2+int(l)]...) @@ -527,12 +615,12 @@ func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData [] } if bitmap1&bit != 0 { add := (touchMap2&bit == 0) && (afterMap2&bit != 0) // Add fields from branchData1 - fieldBits := cellFields(branchData[pos1]) + fields := cellFields(branchData[pos1]) if add { - newData = append(newData, byte(fieldBits)) + newData = append(newData, byte(fields)) } pos1++ - for i := 0; i < bits.OnesCount8(byte(fieldBits)); i++ { + for i := 0; i < bits.OnesCount8(byte(fields)); i++ { l, n := binary.Uvarint(branchData[pos1:]) if n == 0 { return nil, errors.New("MergeHexBranches buffer1 too small for field") @@ -544,7 +632,7 @@ func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData [] } pos1 += n if len(branchData) < pos1+int(l) { - return nil, errors.New("MergeHexBranches buffer1 too small for field") + return nil, fmt.Errorf("MergeHexBranches buffer1 too small for %s : expected %d got %d", fields&cellFields(1< 0 { if add { @@ -567,10 +655,10 @@ func (branchData BranchData) decodeCells() (touchMap, afterMap uint16, row [16]* bit := bitset & -bitset nibble := bits.TrailingZeros16(bit) if afterMap&bit != 0 { - fieldBits := cellFields(branchData[pos]) + fields := cellFields(branchData[pos]) pos++ row[nibble] = new(cell) - if pos, err = row[nibble].fillFromFields(branchData, pos, fieldBits); err != nil { + if pos, err = row[nibble].fillFromFields(branchData, pos, fields); err != nil { err = fmt.Errorf("failed to fill cell at nibble %x: %w", nibble, err) return } @@ -618,11 +706,11 @@ func (m *BranchMerger) Merge(branch1 BranchData, branch2 BranchData) (BranchData bit := bitset & -bitset if bitmap2&bit != 0 { // Add fields from branch2 - fieldBits := cellFields(branch2[pos2]) - m.buf = append(m.buf, byte(fieldBits)) + fields := cellFields(branch2[pos2]) + m.buf = append(m.buf, byte(fields)) pos2++ - for i := 0; i < bits.OnesCount8(byte(fieldBits)); i++ { + for i := 0; i < bits.OnesCount8(byte(fields)); i++ { l, n := binary.Uvarint(branch2[pos2:]) if n == 0 { return nil, errors.New("MergeHexBranches branch2 is too small: expected node info size") @@ -645,12 +733,12 @@ func (m *BranchMerger) Merge(branch1 BranchData, branch2 BranchData) (BranchData } if bitmap1&bit != 0 { add := (touchMap2&bit == 0) && (afterMap2&bit != 0) // Add fields from branchData1 - fieldBits := cellFields(branch1[pos1]) + fields := cellFields(branch1[pos1]) if add { - m.buf = append(m.buf, byte(fieldBits)) + m.buf = append(m.buf, byte(fields)) } pos1++ - for i := 0; i < bits.OnesCount8(byte(fieldBits)); i++ { + for i := 0; i < bits.OnesCount8(byte(fields)); i++ { l, n := binary.Uvarint(branch1[pos1:]) if n == 0 { return nil, errors.New("MergeHexBranches branch1 is too small: expected node info size") @@ -694,21 +782,28 @@ func ParseTrieVariant(s string) TrieVariant { } type BranchStat struct { - KeySize uint64 - ValSize uint64 - MinCellSize uint64 - MaxCellSize uint64 - CellCount uint64 - APKSize uint64 - SPKSize uint64 - ExtSize uint64 - HashSize uint64 - APKCount uint64 - SPKCount uint64 - HashCount uint64 - ExtCount uint64 - TAMapsSize uint64 - IsRoot bool + KeySize uint64 + ValSize uint64 + MinCellSize uint64 + MaxCellSize uint64 + CellCount uint64 + APKSize uint64 + SPKSize uint64 + ExtSize uint64 + HashSize uint64 + APKCount uint64 + SPKCount uint64 + HashCount uint64 + ExtCount uint64 + TAMapsSize uint64 + LeafHashSize uint64 + LeafHashCount uint64 + MedianAPK uint64 + MedianSPK uint64 + MedianHash uint64 + MedianExt uint64 + MedianLH uint64 + IsRoot bool } // do not add stat of root node to other branch stat @@ -730,6 +825,26 @@ func (bs *BranchStat) Collect(other *BranchStat) { bs.SPKCount += other.SPKCount bs.HashCount += other.HashCount bs.ExtCount += other.ExtCount + + setMedian := func(median *uint64, otherMedian uint64) { + if *median == 0 { + *median = otherMedian + } else { + *median = (*median + otherMedian) / 2 + } + } + setMedian(&bs.MedianExt, other.MedianExt) + setMedian(&bs.MedianAPK, other.MedianAPK) + setMedian(&bs.MedianSPK, other.MedianSPK) + setMedian(&bs.MedianHash, other.MedianHash) + setMedian(&bs.MedianLH, other.MedianLH) + bs.MedianHash = (bs.MedianHash + other.MedianHash) / 2 + bs.MedianAPK = (bs.MedianAPK + other.MedianAPK) / 2 + bs.MedianSPK = (bs.MedianSPK + other.MedianSPK) / 2 + bs.MedianLH = (bs.MedianLH + other.MedianLH) / 2 + bs.TAMapsSize += other.TAMapsSize + bs.LeafHashSize += other.LeafHashSize + bs.LeafHashCount += other.LeafHashCount } func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat { @@ -752,6 +867,8 @@ func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat } stat.TAMapsSize = uint64(2 + 2) // touchMap + afterMap stat.CellCount = uint64(bits.OnesCount16(tm & am)) + + medians := make(map[string][]int) for _, c := range cells { if c == nil { continue @@ -763,15 +880,25 @@ func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat case c.accountAddrLen > 0: stat.APKSize += uint64(c.accountAddrLen) stat.APKCount++ + medians["apk"] = append(medians["apk"], c.accountAddrLen) case c.storageAddrLen > 0: stat.SPKSize += uint64(c.storageAddrLen) stat.SPKCount++ + medians["spk"] = append(medians["spk"], c.storageAddrLen) case c.hashLen > 0: stat.HashSize += uint64(c.hashLen) stat.HashCount++ + medians["hash"] = append(medians["hash"], c.hashLen) + case c.stateHashLen > 0: + stat.LeafHashSize += uint64(c.stateHashLen) + stat.LeafHashCount++ + medians["lh"] = append(medians["lh"], c.stateHashLen) + case c.extLen > 0: + stat.ExtSize += uint64(c.extLen) + stat.ExtCount++ + medians["ext"] = append(medians["ext"], c.extLen) default: - panic("no plain key" + fmt.Sprintf("#+%v", c)) - //case c.extLen > 0: + panic("unexpected cell " + fmt.Sprintf("%s", c.FullString())) } if c.extLen > 0 { switch tv { @@ -783,6 +910,22 @@ func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat stat.ExtCount++ } } + + for k, v := range medians { + sort.Ints(v) + switch k { + case "apk": + stat.MedianAPK = uint64(v[len(v)/2]) + case "spk": + stat.MedianSPK = uint64(v[len(v)/2]) + case "hash": + stat.MedianHash = uint64(v[len(v)/2]) + case "ext": + stat.MedianExt = uint64(v[len(v)/2]) + case "lh": + stat.MedianLH = uint64(v[len(v)/2]) + } + } } return stat } @@ -851,6 +994,26 @@ func NewUpdates(m Mode, tmpdir string, hasher keyHasher) *Updates { } return t } +func (t *Updates) SetMode(m Mode) { + t.mode = m + if t.mode == ModeDirect && t.keys == nil { + t.keys = make(map[string]struct{}) + t.initCollector() + } else if t.mode == ModeUpdate && t.tree == nil { + t.tree = btree.NewG[*KeyUpdate](64, keyUpdateLessFn) + } + t.Reset() +} + +func (t *Updates) initCollector() { + if t.etl != nil { + t.etl.Close() + t.etl = nil + } + t.etl = etl.NewCollector("commitment", t.tmpdir, etl.NewSortableBuffer(etl.BufferOptimalSize/4), log.Root().New("update-tree")) + t.etl.LogLvl(log.LvlDebug) + t.etl.SortAndFlushInBackground(true) +} func (t *Updates) Mode() Mode { return t.mode } @@ -865,16 +1028,6 @@ func (t *Updates) Size() (updates uint64) { } } -func (t *Updates) initCollector() { - if t.etl != nil { - t.etl.Close() - t.etl = nil - } - t.etl = etl.NewCollector("commitment", t.tmpdir, etl.NewSortableBuffer(etl.BufferOptimalSize/2), log.Root().New("update-tree")) - t.etl.LogLvl(log.LvlDebug) - t.etl.SortAndFlushInBackground(true) -} - // TouchPlainKey marks plainKey as updated and applies different fn for different key types // (different behaviour for Code, Account and Storage key modifications). func (t *Updates) TouchPlainKey(key, val []byte, fn func(c *KeyUpdate, val []byte)) { @@ -911,7 +1064,7 @@ func (t *Updates) TouchAccount(c *KeyUpdate, val []byte) { return } if c.update.Flags&DeleteUpdate != 0 { - c.update.Flags = 0 + c.update.Flags = 0 // also could invert with ^ but 0 is just a reset } nonce, balance, chash := types.DecodeAccountBytesV3(val) if c.update.Nonce != nonce { diff --git a/erigon-lib/commitment/commitment_bench_test.go b/erigon-lib/commitment/commitment_bench_test.go index 63ea06804ac..6856e58ccdc 100644 --- a/erigon-lib/commitment/commitment_bench_test.go +++ b/erigon-lib/commitment/commitment_bench_test.go @@ -93,6 +93,8 @@ func BenchmarkBranchData_ReplacePlainKeys(b *testing.B) { enc, _, err := be.EncodeBranch(bm, bm, bm, cg) require.NoError(b, err) + b.ResetTimer() + original := common.Copy(enc) for i := 0; i < b.N; i++ { target := make([]byte, 0, len(enc)) diff --git a/erigon-lib/commitment/commitment_test.go b/erigon-lib/commitment/commitment_test.go index 02780283385..9c8ba0cf08f 100644 --- a/erigon-lib/commitment/commitment_test.go +++ b/erigon-lib/commitment/commitment_test.go @@ -139,6 +139,29 @@ func TestBranchData_MergeHexBranches3(t *testing.T) { //_, _ = tm, am } +func TestDecodeBranchWithLeafHashes(t *testing.T) { + // enc := "00061614a8f8d73af90eee32dc9729ce8d5bb762f30d21a434a8f8d73af90eee32dc9729ce8d5bb762f30d21a49f49fdd48601f00df18ebc29b1264e27d09cf7cbd514fe8af173e534db038033203c7e2acaef5400189202e1a6a3b0b3d9add71fb52ad24ae35be6b6c85ca78bb51214ba7a3b7b095d3370c022ca655c790f0c0ead66f52025c143802ceb44bbe35e883927edb5933fc33416d4cc354dd88c7bcf1aad66a1" + // unfoldBranchDataFromString(t, enc) + + row, bm := generateCellRow(t, 16) + + for i := 0; i < len(row); i++ { + if row[i].accountAddrLen > 0 { + rand.Read(row[i].stateHash[:]) + row[i].stateHashLen = 32 + } + } + + be := NewBranchEncoder(1024, t.TempDir()) + enc, _, err := be.EncodeBranch(bm, bm, bm, func(i int, skip bool) (*cell, error) { + return row[i], nil + }) + require.NoError(t, err) + + fmt.Printf("%s\n", enc.String()) + +} + // helper to decode row of cells from string func unfoldBranchDataFromString(tb testing.TB, encs string) (row []*cell, am uint16) { tb.Helper() diff --git a/erigon-lib/commitment/hex_patricia_hashed.go b/erigon-lib/commitment/hex_patricia_hashed.go index 22f998f018d..d26b17fdf42 100644 --- a/erigon-lib/commitment/hex_patricia_hashed.go +++ b/erigon-lib/commitment/hex_patricia_hashed.go @@ -28,6 +28,9 @@ import ( "math/bits" "path/filepath" "runtime" + "sort" + "strings" + "sync/atomic" "time" "github.com/erigontech/erigon-lib/etl" @@ -59,7 +62,7 @@ type HexPatriciaHashed struct { // How many rows (starting from row 0) are currently active and have corresponding selected columns // Last active row does not have selected column activeRows int - // Length of the key that reflects current positioning of the grid. It maybe larger than number of active rows, + // Length of the key that reflects current positioning of the grid. It may be larger than number of active rows, // if an account leaf cell represents multiple nibbles in the key currentKeyLen int accountKeyLen int @@ -81,6 +84,9 @@ type HexPatriciaHashed struct { hashAuxBuffer [128]byte // buffer to compute cell hash or write hash-related things auxBuffer *bytes.Buffer // auxiliary buffer used during branch updates encoding branchEncoder *BranchEncoder + + depthsToTxNum [129]uint64 // endTxNum of file with branch data for that depth + hadToLoadL map[uint64]skipStat } func NewHexPatriciaHashed(accountKeyLen int, ctx PatriciaContext, tmpdir string) *HexPatriciaHashed { @@ -90,6 +96,7 @@ func NewHexPatriciaHashed(accountKeyLen int, ctx PatriciaContext, tmpdir string) keccak2: sha3.NewLegacyKeccak256().(keccakState), accountKeyLen: accountKeyLen, auxBuffer: bytes.NewBuffer(make([]byte, 8192)), + hadToLoadL: make(map[uint64]skipStat), } hph.branchEncoder = NewBranchEncoder(1024, filepath.Join(tmpdir, "branch-encoder")) return hph @@ -101,14 +108,55 @@ type cell struct { accountAddr [length.Addr]byte // account plain key storageAddr [length.Addr + length.Hash]byte // storage plain key hash [length.Hash]byte // cell hash - hashLen int // Length of the hash (or embedded) - accountAddrLen int // length of account plain key - storageAddrLen int // length of the storage plain key - hashedExtLen int // length of the hashed extension, if any - extLen int // length of the extension, if any - Update // state update + stateHash [length.Hash]byte + hashedExtLen int // length of the hashed extension, if any + extLen int // length of the extension, if any + accountAddrLen int // length of account plain key + storageAddrLen int // length of the storage plain key + hashLen int // Length of the hash (or embedded) + stateHashLen int // stateHash length, if > 0 can reuse + loaded loadFlags // folded Cell have only hash, unfolded have all fields + Update // state update +} + +type loadFlags uint8 + +func (f loadFlags) String() string { + var b strings.Builder + if f == cellLoadNone { + b.WriteString("false") + } else { + if f.account() { + b.WriteString("Account ") + } + if f.storage() { + b.WriteString("Storage ") + } + } + return b.String() +} + +func (f loadFlags) account() bool { + return f&cellLoadAccount != 0 +} + +func (f loadFlags) storage() bool { + return f&cellLoadStorage != 0 } +func (f loadFlags) addFlag(loadFlags loadFlags) loadFlags { + if loadFlags == cellLoadNone { + return f + } + return f | loadFlags +} + +const ( + cellLoadNone = loadFlags(0) + cellLoadAccount = loadFlags(1) + cellLoadStorage = loadFlags(2) +) + var ( EmptyRootHash = hexutility.MustDecodeHex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") EmptyCodeHash = hexutility.MustDecodeHex("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") @@ -121,6 +169,8 @@ func (cell *cell) reset() { cell.hashedExtLen = 0 cell.extLen = 0 cell.hashLen = 0 + cell.stateHashLen = 0 + cell.loaded = cellLoadNone clear(cell.hashedExtension[:]) clear(cell.extension[:]) clear(cell.accountAddr[:]) @@ -129,7 +179,79 @@ func (cell *cell) reset() { cell.Update.Reset() } -func (cell *cell) setFromUpdate(update *Update) { cell.Update.Merge(update) } +func (cell *cell) String() string { + b := new(strings.Builder) + b.WriteString("{") + b.WriteString(fmt.Sprintf("loaded=%v ", cell.loaded)) + if cell.Deleted() { + b.WriteString("DELETED ") + } + if cell.accountAddrLen > 0 { + b.WriteString(fmt.Sprintf("addr=%x ", cell.accountAddr[:cell.accountAddrLen])) + } + if cell.storageAddrLen > 0 { + b.WriteString(fmt.Sprintf("addr[s]=%x", cell.storageAddr[:cell.storageAddrLen])) + + } + if cell.hashLen > 0 { + b.WriteString(fmt.Sprintf("h=%x", cell.hash[:cell.hashLen])) + } + b.WriteString("}") + return b.String() +} + +func (cell *cell) FullString() string { + b := new(strings.Builder) + b.WriteString("{") + b.WriteString(fmt.Sprintf("loaded=%v ", cell.loaded)) + if cell.Deleted() { + b.WriteString("DELETED ") + } + + if cell.accountAddrLen > 0 { + b.WriteString(fmt.Sprintf("addr=%x ", cell.accountAddr[:cell.accountAddrLen])) + b.WriteString(fmt.Sprintf("balance=%s ", cell.Balance.String())) + b.WriteString(fmt.Sprintf("nonce=%d ", cell.Nonce)) + if cell.CodeHash != EmptyCodeHashArray { + b.WriteString(fmt.Sprintf("codeHash=%x ", cell.CodeHash[:])) + } else { + b.WriteString("codeHash=EMPTY ") + } + } + if cell.storageAddrLen > 0 { + b.WriteString(fmt.Sprintf("addr[s]=%x ", cell.storageAddr[:cell.storageAddrLen])) + b.WriteString(fmt.Sprintf("storage=%x ", cell.Storage[:cell.StorageLen])) + } + if cell.hashLen > 0 { + b.WriteString(fmt.Sprintf("h=%x ", cell.hash[:cell.hashLen])) + } + if cell.stateHashLen > 0 { + b.WriteString(fmt.Sprintf("memHash=%x ", cell.stateHash[:cell.stateHashLen])) + } + if cell.extLen > 0 { + b.WriteString(fmt.Sprintf("extension=%x ", cell.extension[:cell.extLen])) + } + if cell.hashedExtLen > 0 { + b.WriteString(fmt.Sprintf("hashedExtension=%x ", cell.hashedExtension[:cell.hashedExtLen])) + } + + b.WriteString("}") + return b.String() +} + +func (cell *cell) setFromUpdate(update *Update) { + cell.Update.Merge(update) + if update.Flags&StorageUpdate != 0 { + cell.loaded = cell.loaded.addFlag(cellLoadStorage) + mxTrieStateLoadRate.Inc() + hadToLoad.Add(1) + } + if update.Flags&BalanceUpdate != 0 || update.Flags&NonceUpdate != 0 || update.Flags&CodeUpdate != 0 { + cell.loaded = cell.loaded.addFlag(cellLoadAccount) + mxTrieStateLoadRate.Inc() + hadToLoad.Add(1) + } +} func (cell *cell) fillFromUpperCell(upCell *cell, depth, depthIncrement int) { if upCell.hashedExtLen >= depthIncrement { @@ -175,8 +297,10 @@ func (cell *cell) fillFromUpperCell(upCell *cell, depth, depthIncrement int) { if upCell.hashLen > 0 { copy(cell.hash[:], upCell.hash[:upCell.hashLen]) } + cell.loaded = upCell.loaded } +// fillFromLowerCell fills the cell with the data from the cell of the lower row during fold func (cell *cell) fillFromLowerCell(lowCell *cell, lowDepth int, preExtension []byte, nibble int) { if lowCell.accountAddrLen > 0 || lowDepth < 64 { cell.accountAddrLen = lowCell.accountAddrLen @@ -218,6 +342,7 @@ func (cell *cell) fillFromLowerCell(lowCell *cell, lowDepth int, preExtension [] if lowCell.hashLen > 0 { copy(cell.hash[:], lowCell.hash[:lowCell.hashLen]) } + cell.loaded = lowCell.loaded } func hashKey(keccak keccakState, plainKey []byte, dest []byte, hashedKeyOffset int) error { @@ -289,88 +414,68 @@ func (cell *cell) deriveHashedKeys(depth int, keccak keccakState, accountKeyLen } func (cell *cell) fillFromFields(data []byte, pos int, fieldBits cellFields) (int, error) { - if fieldBits&fieldExtension != 0 { - l, n := binary.Uvarint(data[pos:]) - if n == 0 { - return 0, errors.New("fillFromFields buffer too small for hashedKey len") - } else if n < 0 { - return 0, errors.New("fillFromFields value overflow for hashedKey len") - } - pos += n - if len(data) < pos+int(l) { - return 0, fmt.Errorf("fillFromFields buffer too small for hashedKey exp %d got %d", pos+int(l), len(data)) - } - cell.hashedExtLen = int(l) - cell.extLen = int(l) - if l > 0 { - copy(cell.hashedExtension[:], data[pos:pos+int(l)]) - copy(cell.extension[:], data[pos:pos+int(l)]) - pos += int(l) - } - } else { - cell.hashedExtLen = 0 - cell.extLen = 0 - } - if fieldBits&fieldAccountAddr != 0 { - l, n := binary.Uvarint(data[pos:]) - if n == 0 { - return 0, errors.New("fillFromFields buffer too small for accountAddr len") - } else if n < 0 { - return 0, errors.New("fillFromFields value overflow for accountAddr len") - } - pos += n - if len(data) < pos+int(l) { - return 0, errors.New("fillFromFields buffer too small for accountAddr") - } - cell.accountAddrLen = int(l) - if l > 0 { - copy(cell.accountAddr[:], data[pos:pos+int(l)]) - pos += int(l) - } - } else { - cell.accountAddrLen = 0 + fields := []struct { + flag cellFields + lenField *int + dataField []byte + extraFunc func(int) + }{ + {fieldExtension, &cell.hashedExtLen, cell.hashedExtension[:], func(l int) { + cell.extLen = l + if l > 0 { + copy(cell.extension[:], cell.hashedExtension[:l]) + } + }}, + {fieldAccountAddr, &cell.accountAddrLen, cell.accountAddr[:], nil}, + {fieldStorageAddr, &cell.storageAddrLen, cell.storageAddr[:], nil}, + {fieldHash, &cell.hashLen, cell.hash[:], nil}, + {fieldStateHash, &cell.stateHashLen, cell.stateHash[:], nil}, } - if fieldBits&fieldStorageAddr != 0 { - l, n := binary.Uvarint(data[pos:]) - if n == 0 { - return 0, errors.New("fillFromFields buffer too small for storageAddr len") - } else if n < 0 { - return 0, errors.New("fillFromFields value overflow for storageAddr len") - } - pos += n - if len(data) < pos+int(l) { - return 0, errors.New("fillFromFields buffer too small for storageAddr") - } - cell.storageAddrLen = int(l) - if l > 0 { - copy(cell.storageAddr[:], data[pos:pos+int(l)]) - pos += int(l) + + for _, f := range fields { + if fieldBits&f.flag != 0 { + l, n, err := readUvarint(data[pos:]) + if err != nil { + return 0, err + } + pos += n + + if len(data) < pos+int(l) { + return 0, fmt.Errorf("buffer too small for %v", f.flag) + } + + *f.lenField = int(l) + if l > 0 { + copy(f.dataField, data[pos:pos+int(l)]) + pos += int(l) + } + if f.extraFunc != nil { + f.extraFunc(int(l)) + } + } else { + *f.lenField = 0 + if f.flag == fieldExtension { + cell.extLen = 0 + } } - } else { - cell.storageAddrLen = 0 } - if fieldBits&fieldHash != 0 { - l, n := binary.Uvarint(data[pos:]) - if n == 0 { - return 0, errors.New("fillFromFields buffer too small for hash len") - } else if n < 0 { - return 0, errors.New("fillFromFields value overflow for hash len") - } - pos += n - if len(data) < pos+int(l) { - return 0, errors.New("fillFromFields buffer too small for hash") - } - cell.hashLen = int(l) - if l > 0 { - copy(cell.hash[:], data[pos:pos+int(l)]) - pos += int(l) - } - } else { - cell.hashLen = 0 + + if fieldBits&fieldAccountAddr != 0 { + copy(cell.CodeHash[:], EmptyCodeHash) } return pos, nil } +func readUvarint(data []byte) (uint64, int, error) { + l, n := binary.Uvarint(data) + if n == 0 { + return 0, 0, errors.New("buffer too small for length") + } else if n < 0 { + return 0, 0, errors.New("value overflow for length") + } + return l, n, nil +} + func (cell *cell) accountForHashing(buffer []byte, storageRootHash [length.Hash]byte) int { balanceBytes := 0 if !cell.Balance.LtUint64(128) { @@ -442,10 +547,10 @@ func (cell *cell) accountForHashing(buffer []byte, storageRootHash [length.Hash] func (hph *HexPatriciaHashed) completeLeafHash(buf, keyPrefix []byte, kp, kl, compactLen int, key []byte, compact0 byte, ni int, val rlp.RlpSerializable, singleton bool) ([]byte, error) { totalLen := kp + kl + val.DoubleRLPLen() var lenPrefix [4]byte - pt := rlp.GenerateStructLen(lenPrefix[:], totalLen) - embedded := !singleton && totalLen+pt < length.Hash + pl := rlp.GenerateStructLen(lenPrefix[:], totalLen) + canEmbed := !singleton && totalLen+pl < length.Hash var writer io.Writer - if embedded { + if canEmbed { //hph.byteArrayWriter.Setup(buf) hph.auxBuffer.Reset() writer = hph.auxBuffer @@ -453,14 +558,13 @@ func (hph *HexPatriciaHashed) completeLeafHash(buf, keyPrefix []byte, kp, kl, co hph.keccak.Reset() writer = hph.keccak } - if _, err := writer.Write(lenPrefix[:pt]); err != nil { + if _, err := writer.Write(lenPrefix[:pl]); err != nil { return nil, err } if _, err := writer.Write(keyPrefix[:kp]); err != nil { return nil, err } - var b [1]byte - b[0] = compact0 + b := [1]byte{compact0} if _, err := writer.Write(b[:]); err != nil { return nil, err } @@ -475,7 +579,7 @@ func (hph *HexPatriciaHashed) completeLeafHash(buf, keyPrefix []byte, kp, kl, co if err := val.ToDoubleRLP(writer, prefixBuf[:]); err != nil { return nil, err } - if embedded { + if canEmbed { buf = hph.auxBuffer.Bytes() } else { var hashBuf [33]byte @@ -616,6 +720,10 @@ func (hph *HexPatriciaHashed) extensionHash(key []byte, hash []byte) ([length.Ha func (hph *HexPatriciaHashed) computeCellHashLen(cell *cell, depth int) int { if cell.storageAddrLen > 0 && depth >= 64 { + if cell.stateHashLen > 0 { + return cell.stateHashLen + 1 + } + keyLen := 128 - depth + 1 // Length of hex key with terminator character var kp, kl int compactLen := (keyLen-1)/2 + 1 @@ -639,7 +747,7 @@ func (hph *HexPatriciaHashed) computeCellHashLen(cell *cell, depth int) int { func (hph *HexPatriciaHashed) computeCellHash(cell *cell, depth int, buf []byte) ([]byte, error) { var err error var storageRootHash [length.Hash]byte - storageRootHashIsSet := false + var storageRootHashIsSet bool if cell.storageAddrLen > 0 { var hashedKeyOffset int if depth >= 64 { @@ -651,28 +759,68 @@ func (hph *HexPatriciaHashed) computeCellHash(cell *cell, depth int, buf []byte) // if account key is empty, then we need to hash storage key from the key beginning koffset = 0 } - if err := hashKey(hph.keccak, cell.storageAddr[koffset:cell.storageAddrLen], cell.hashedExtension[:], hashedKeyOffset); err != nil { + if err = hashKey(hph.keccak, cell.storageAddr[koffset:cell.storageAddrLen], cell.hashedExtension[:], hashedKeyOffset); err != nil { return nil, err } cell.hashedExtension[64-hashedKeyOffset] = 16 // Add terminator - if singleton { + + if cell.stateHashLen > 0 { + res := append([]byte{160}, cell.stateHash[:cell.stateHashLen]...) + hph.keccak.Reset() if hph.trace { - fmt.Printf("leafHashWithKeyVal(singleton) for [%x]=>[%x]\n", cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen]) - } - aux := make([]byte, 0, 33) - if aux, err = hph.leafHashWithKeyVal(aux, cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen], true); err != nil { - return nil, err + fmt.Printf("REUSED stateHash %x spk %x\n", res, cell.storageAddr[:cell.storageAddrLen]) } - if hph.trace { - fmt.Printf("leafHashWithKeyVal(singleton) storage hash [%x]\n", aux) + mxTrieStateSkipRate.Inc() + skippedLoad.Add(1) + if !singleton { + return res, nil + } else { + storageRootHashIsSet = true + storageRootHash = *(*[length.Hash]byte)(res[1:]) + //copy(storageRootHash[:], res[1:]) + //cell.stateHashLen = 0 } - storageRootHash = *(*[length.Hash]byte)(aux[1:]) - storageRootHashIsSet = true } else { - if hph.trace { - fmt.Printf("leafHashWithKeyVal for [%x]=>[%x]\n", cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen]) + if !cell.loaded.storage() { + update, err := hph.ctx.Storage(cell.storageAddr[:cell.storageAddrLen]) + if err != nil { + return nil, err + } + cell.setFromUpdate(update) + fmt.Printf("Storage %x was not loaded\n", cell.storageAddr[:cell.storageAddrLen]) + } + if singleton { + if hph.trace { + fmt.Printf("leafHashWithKeyVal(singleton) for [%x]=>[%x]\n", cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen]) + } + aux := make([]byte, 0, 33) + if aux, err = hph.leafHashWithKeyVal(aux, cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen], true); err != nil { + return nil, err + } + if hph.trace { + fmt.Printf("leafHashWithKeyVal(singleton) storage hash [%x]\n", aux) + } + storageRootHash = *(*[length.Hash]byte)(aux[1:]) + storageRootHashIsSet = true + cell.stateHashLen = 0 + hadToReset.Add(1) + } else { + if hph.trace { + fmt.Printf("leafHashWithKeyVal for [%x]=>[%x] %v\n", cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen], cell.String()) + } + leafHash, err := hph.leafHashWithKeyVal(buf, cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen], false) + if err != nil { + return nil, err + } + + copy(cell.stateHash[:], leafHash[1:]) + cell.stateHashLen = len(leafHash) - 1 + if hph.trace { + fmt.Printf("STATE HASH storage memoized %x spk %x\n", leafHash, cell.storageAddr[:cell.storageAddrLen]) + } + + return leafHash, nil } - return hph.leafHashWithKeyVal(buf, cell.hashedExtension[:64-hashedKeyOffset+1], cell.Storage[:cell.StorageLen], false) } } if cell.accountAddrLen > 0 { @@ -681,34 +829,66 @@ func (hph *HexPatriciaHashed) computeCellHash(cell *cell, depth int, buf []byte) } cell.hashedExtension[64-depth] = 16 // Add terminator if !storageRootHashIsSet { - if cell.extLen > 0 { - // Extension - if cell.hashLen > 0 { - if hph.trace { - fmt.Printf("extensionHash for [%x]=>[%x]\n", cell.extension[:cell.extLen], cell.hash[:cell.hashLen]) - } - if storageRootHash, err = hph.extensionHash(cell.extension[:cell.extLen], cell.hash[:cell.hashLen]); err != nil { - return nil, err - } - } else { + if cell.extLen > 0 { // Extension + if cell.hashLen == 0 { return nil, errors.New("computeCellHash extension without hash") } + if hph.trace { + fmt.Printf("extensionHash for [%x]=>[%x]\n", cell.extension[:cell.extLen], cell.hash[:cell.hashLen]) + } + if storageRootHash, err = hph.extensionHash(cell.extension[:cell.extLen], cell.hash[:cell.hashLen]); err != nil { + return nil, err + } + if hph.trace { + fmt.Printf("EXTENSION HASH %x DROPS stateHash\n", storageRootHash) + } + cell.stateHashLen = 0 + hadToReset.Add(1) } else if cell.hashLen > 0 { storageRootHash = cell.hash } else { storageRootHash = *(*[length.Hash]byte)(EmptyRootHash) } } + if !cell.loaded.account() { + if cell.stateHashLen > 0 { + res := append([]byte{160}, cell.stateHash[:cell.stateHashLen]...) + hph.keccak.Reset() + + mxTrieStateSkipRate.Inc() + skippedLoad.Add(1) + if hph.trace { + fmt.Printf("REUSED stateHash %x apk %x\n", res, cell.accountAddr[:cell.accountAddrLen]) + } + return res, nil + } + // storage root update or extension update could invalidate older stateHash, so we need to reload state + update, err := hph.ctx.Account(cell.accountAddr[:cell.accountAddrLen]) + if err != nil { + return nil, err + } + cell.setFromUpdate(update) + } + var valBuf [128]byte valLen := cell.accountForHashing(valBuf[:], storageRootHash) if hph.trace { fmt.Printf("accountLeafHashWithKey for [%x]=>[%x]\n", cell.hashedExtension[:65-depth], rlp.RlpEncodedBytes(valBuf[:valLen])) } - return hph.accountLeafHashWithKey(buf, cell.hashedExtension[:65-depth], rlp.RlpEncodedBytes(valBuf[:valLen])) + leafHash, err := hph.accountLeafHashWithKey(buf, cell.hashedExtension[:65-depth], rlp.RlpEncodedBytes(valBuf[:valLen])) + if err != nil { + return nil, err + } + if hph.trace { + fmt.Printf("STATE HASH account memoized %x\n", leafHash) + } + copy(cell.stateHash[:], leafHash[1:]) + cell.stateHashLen = len(leafHash) - 1 + return leafHash, nil } + buf = append(buf, 0x80+32) - if cell.extLen > 0 { - // Extension + if cell.extLen > 0 { // Extension if cell.hashLen > 0 { if hph.trace { fmt.Printf("extensionHash for [%x]=>[%x]\n", cell.extension[:cell.extLen], cell.hash[:cell.hashLen]) @@ -723,9 +903,10 @@ func (hph *HexPatriciaHashed) computeCellHash(cell *cell, depth int, buf []byte) } } else if cell.hashLen > 0 { buf = append(buf, cell.hash[:cell.hashLen]...) - //} else if storageRootHashIsSet { - // buf = append(buf, storageRootHash[:]...) - // copy(cell.h[:], storageRootHash[:]) + } else if storageRootHashIsSet { + buf = append(buf, storageRootHash[:]...) + copy(cell.hash[:], storageRootHash[:]) + cell.hashLen = len(storageRootHash) } else { buf = append(buf, EmptyRootHash...) } @@ -739,13 +920,22 @@ func (hph *HexPatriciaHashed) needUnfolding(hashedKey []byte) int { if hph.trace { fmt.Printf("needUnfolding root, rootChecked = %t\n", hph.rootChecked) } + if hph.root.hashedExtLen == 64 && hph.root.accountAddrLen > 0 && hph.root.storageAddrLen > 0 { + // in case if root is a leaf node with storage and account, we need to derive storage part of a key + if err := hph.root.deriveHashedKeys(depth, hph.keccak, hph.accountKeyLen); err != nil { + log.Warn("deriveHashedKeys for root with storage", "err", err, "cell", hph.root.FullString()) + return 0 + } + //copy(hph.currentKey[:], hph.root.hashedExtension[:]) + if hph.trace { + fmt.Printf("derived prefix %x\n", hph.currentKey[:hph.currentKeyLen]) + } + } if hph.root.hashedExtLen == 0 && hph.root.hashLen == 0 { if hph.rootChecked { - // Previously checked, empty root, no unfolding needed - return 0 + return 0 // Previously checked, empty root, no unfolding needed } - // Need to attempt to unfold the root - return 1 + return 1 // Need to attempt to unfold the root } cell = &hph.root } else { @@ -753,7 +943,7 @@ func (hph *HexPatriciaHashed) needUnfolding(hashedKey []byte) int { cell = &hph.grid[hph.activeRows-1][col] depth = hph.depths[hph.activeRows-1] if hph.trace { - fmt.Printf("needUnfolding cell (%d, %x), currentKey=[%x], depth=%d, cell.hash=[%x]\n", hph.activeRows-1, col, hph.currentKey[:hph.currentKeyLen], depth, cell.hash[:cell.hashLen]) + fmt.Printf("currentKey [%x] needUnfolding cell (%d, %x, depth=%d) cell.hash=[%x]\n", hph.currentKey[:hph.currentKeyLen], hph.activeRows-1, col, depth, cell.hash[:cell.hashLen]) } } if len(hashedKey) <= depth { @@ -769,36 +959,32 @@ func (hph *HexPatriciaHashed) needUnfolding(hashedKey []byte) int { } cpl := commonPrefixLen(hashedKey[depth:], cell.hashedExtension[:cell.hashedExtLen-1]) if hph.trace { - fmt.Printf("cpl=%d, cell.hashedExtension=[%x], depth=%d, hashedKey[depth:]=[%x]\n", cpl, cell.hashedExtension[:cell.hashedExtLen], depth, hashedKey[depth:]) + fmt.Printf("cpl=%d cell.hashedExtension=[%x] hashedKey[depth=%d:]=[%x]\n", cpl, cell.hashedExtension[:cell.hashedExtLen], depth, hashedKey[depth:]) } unfolding := cpl + 1 if depth < 64 && depth+unfolding > 64 { // This is to make sure that unfolding always breaks at the level where storage subtrees start unfolding = 64 - depth if hph.trace { - fmt.Printf("adjusted unfolding=%d\n", unfolding) + fmt.Printf("adjusted unfolding=%d <- %d\n", unfolding, cpl+1) } } return unfolding } -var temporalReplacementForEmpty = []byte("root") - // unfoldBranchNode returns true if unfolding has been done -func (hph *HexPatriciaHashed) unfoldBranchNode(row int, deleted bool, depth int) (bool, error) { +func (hph *HexPatriciaHashed) unfoldBranchNode(row, depth int, deleted bool) (bool, error) { key := hexToCompact(hph.currentKey[:hph.currentKeyLen]) - if len(key) == 0 { - key = temporalReplacementForEmpty - } - branchData, _, err := hph.ctx.Branch(key) + branchData, fileEndTxNum, err := hph.ctx.Branch(key) if err != nil { return false, err } + hph.depthsToTxNum[depth] = fileEndTxNum if len(branchData) >= 2 { - branchData = branchData[2:] // skip touch map and hold aftermap and rest + branchData = branchData[2:] // skip touch map and keep the rest } if hph.trace { - fmt.Printf("unfoldBranchNode prefix '%x', compacted [%x] depth %d row %d '%x'\n", key, hph.currentKey[:hph.currentKeyLen], depth, row, branchData) + fmt.Printf("unfoldBranchNode prefix '%x', nibbles [%x] depth %d row %d '%x'\n", key, hph.currentKey[:hph.currentKeyLen], depth, row, branchData) } if !hph.rootChecked && hph.currentKeyLen == 0 && len(branchData) == 0 { // Special case - empty or deleted root @@ -828,30 +1014,14 @@ func (hph *HexPatriciaHashed) unfoldBranchNode(row int, deleted bool, depth int) cell := &hph.grid[row][nibble] fieldBits := branchData[pos] pos++ - var err error if pos, err = cell.fillFromFields(branchData, pos, cellFields(fieldBits)); err != nil { - return false, fmt.Errorf("prefix [%x], branchData[%x]: %w", hph.currentKey[:hph.currentKeyLen], branchData, err) + return false, fmt.Errorf("prefix [%x] branchData[%x]: %w", hph.currentKey[:hph.currentKeyLen], branchData, err) } if hph.trace { - fmt.Printf("cell (%d, %x) depth=%d, hash=[%x], accountAddr=[%x], storageAddr=[%x], extension=[%x]\n", row, nibble, depth, cell.hash[:cell.hashLen], cell.accountAddr[:cell.accountAddrLen], cell.storageAddr[:cell.storageAddrLen], cell.extension[:cell.extLen]) - } - if cell.accountAddrLen > 0 { - update, err := hph.ctx.Account(cell.accountAddr[:cell.accountAddrLen]) - if err != nil { - return false, fmt.Errorf("unfoldBranchNode GetAccount: %w", err) - } - cell.setFromUpdate(update) - if hph.trace { - fmt.Printf("GetAccount[%x] return balance=%d, nonce=%d code=%x\n", cell.accountAddr[:cell.accountAddrLen], &cell.Balance, cell.Nonce, cell.CodeHash[:]) - } - } - if cell.storageAddrLen > 0 { - update, err := hph.ctx.Storage(cell.storageAddr[:cell.storageAddrLen]) - if err != nil { - return false, fmt.Errorf("unfoldBranchNode GetAccount: %w", err) - } - cell.setFromUpdate(update) + fmt.Printf("cell (%d, %x, depth=%d) %s\n", row, nibble, depth, cell.FullString()) } + + // relies on plain account/storage key so need to be dereferenced before hashing if err = cell.deriveHashedKeys(depth, hph.keccak, hph.accountKeyLen); err != nil { return false, err } @@ -866,7 +1036,6 @@ func (hph *HexPatriciaHashed) unfold(hashedKey []byte, unfolding int) error { } var upCell *cell var touched, present bool - var col byte var upDepth, depth int if hph.activeRows == 0 { if hph.rootChecked && hph.root.hashLen == 0 && hph.root.hashedExtLen == 0 { @@ -877,81 +1046,73 @@ func (hph *HexPatriciaHashed) unfold(hashedKey []byte, unfolding int) error { touched = hph.rootTouched present = hph.rootPresent if hph.trace { - fmt.Printf("unfold root, touched %t, present %t, column %d hashedExtension %x\n", touched, present, col, upCell.hashedExtension[:upCell.hashedExtLen]) + fmt.Printf("unfold root: touched: %t present: %t %s\n", touched, present, upCell.FullString()) } } else { upDepth = hph.depths[hph.activeRows-1] - col = hashedKey[upDepth-1] - upCell = &hph.grid[hph.activeRows-1][col] - touched = hph.touchMap[hph.activeRows-1]&(uint16(1)<= unfolding { - depth = upDepth + unfolding - nibble := upCell.hashedExtension[unfolding-1] - if touched { - hph.touchMap[row] = uint16(1) << nibble } - if present { - hph.afterMap[row] = uint16(1) << nibble + if unfolded { + hph.depths[hph.activeRows] = depth + hph.activeRows++ } - cell := &hph.grid[row][nibble] - cell.fillFromUpperCell(upCell, depth, unfolding) - if hph.trace { - fmt.Printf("cell (%d, %x) depth=%d\n", row, nibble, depth) - } - if row >= 64 { - cell.accountAddrLen = 0 - } - if unfolding > 1 { - copy(hph.currentKey[hph.currentKeyLen:], upCell.hashedExtension[:unfolding-1]) - } - hph.currentKeyLen += unfolding - 1 + // Return here to prevent activeRow from being incremented when !unfolded + return nil + } + + var nibble, copyLen int + if upCell.hashedExtLen >= unfolding { + depth = upDepth + unfolding + nibble = int(upCell.hashedExtension[unfolding-1]) + copyLen = unfolding - 1 } else { - // upCell.hashedExtLen < unfolding depth = upDepth + upCell.hashedExtLen - nibble := upCell.hashedExtension[upCell.hashedExtLen-1] - if touched { - hph.touchMap[row] = uint16(1) << nibble - } - if present { - hph.afterMap[row] = uint16(1) << nibble - } - cell := &hph.grid[row][nibble] - cell.fillFromUpperCell(upCell, depth, upCell.hashedExtLen) - if hph.trace { - fmt.Printf("cell (%d, %x) depth=%d\n", row, nibble, depth) - } - if row >= 64 { - cell.accountAddrLen = 0 - } - if upCell.hashedExtLen > 1 { - copy(hph.currentKey[hph.currentKeyLen:], upCell.hashedExtension[:upCell.hashedExtLen-1]) - } - hph.currentKeyLen += upCell.hashedExtLen - 1 + nibble = int(upCell.hashedExtension[upCell.hashedExtLen-1]) + copyLen = upCell.hashedExtLen - 1 + } + + if touched { + hph.touchMap[row] = uint16(1) << nibble + } + if present { + hph.afterMap[row] = uint16(1) << nibble + } + + cell := &hph.grid[row][nibble] + cell.fillFromUpperCell(upCell, depth, min(unfolding, upCell.hashedExtLen)) + if hph.trace { + fmt.Printf("unfolded cell (%d, %x, depth=%d) %s\n", row, nibble, depth, cell.FullString()) + } + + if row >= 64 { + cell.accountAddrLen = 0 } + if copyLen > 0 { + copy(hph.currentKey[hph.currentKeyLen:], upCell.hashedExtension[:copyLen]) + } + hph.currentKeyLen += copyLen + hph.depths[hph.activeRows] = depth hph.activeRows++ return nil @@ -961,6 +1122,28 @@ func (hph *HexPatriciaHashed) needFolding(hashedKey []byte) bool { return !bytes.HasPrefix(hashedKey, hph.currentKey[:hph.currentKeyLen]) } +var ( + hadToLoad atomic.Uint64 + skippedLoad atomic.Uint64 + hadToReset atomic.Uint64 +) + +type skipStat struct { + accLoaded, accSkipped, accReset, storReset, storLoaded, storSkipped uint64 +} + +func updatedNibs(num uint16) string { + var nibbles []string + for i := 0; i < 16; i++ { + if num&(1< 0 { - hph.currentKeyLen = upDepth - 1 - } else { - hph.currentKeyLen = 0 + hph.currentKeyLen = max(upDepth-1, 0) + if hph.trace { + fmt.Printf("formed leaf (%d %x, depth=%d) [%x] %s\n", row, nibble, depth, updateKey, cell.FullString()) } - default: - // Branch node - if hph.touchMap[row] != 0 { - // any modifications + default: // Branch node + if hph.touchMap[row] != 0 { // any modifications if row == 0 { hph.rootTouched = true + hph.rootPresent = true } else { - // Modifiction is propagated upwards - hph.touchMap[row-1] |= (uint16(1) << nibble) + // Modification is propagated upwards + hph.touchMap[row-1] |= uint16(1) << nibble } } bitmap := hph.touchMap[row] & hph.afterMap[row] @@ -1086,6 +1263,50 @@ func (hph *HexPatriciaHashed) fold() (err error) { bit := bitset & -bitset nibble := bits.TrailingZeros16(bit) cell := &hph.grid[row][nibble] + + /* memoization of state hashes*/ + counters := hph.hadToLoadL[hph.depthsToTxNum[depth]] + if cell.stateHashLen > 0 && (hph.touchMap[row]&hph.afterMap[row]&uint16(1< 0 || cell.stateHashLen != length.Hash) { + // drop state hash if updated or hashLen < 32 (corner case, may even not encode such leaf hashes) + if hph.trace { + fmt.Printf("DROP hash for (%d, %x, depth=%d) %s\n", row, nibble, depth, cell.FullString()) + } + cell.stateHashLen = 0 + hadToReset.Add(1) + if cell.accountAddrLen > 0 { + counters.accReset++ + } + if cell.storageAddrLen > 0 { + counters.storReset++ + } + } + + if cell.stateHashLen == 0 { // load state if needed + if !cell.loaded.account() && cell.accountAddrLen > 0 { + upd, err := hph.ctx.Account(cell.accountAddr[:cell.accountAddrLen]) + if err != nil { + return fmt.Errorf("failed to get account: %w", err) + } + cell.setFromUpdate(upd) + // if update is empty, loaded flag was not updated so do it manually + cell.loaded = cell.loaded.addFlag(cellLoadAccount) + counters.accLoaded++ + } + if !cell.loaded.storage() && cell.storageAddrLen > 0 { + upd, err := hph.ctx.Storage(cell.storageAddr[:cell.storageAddrLen]) + if err != nil { + return fmt.Errorf("failed to get storage: %w", err) + } + cell.setFromUpdate(upd) + // if update is empty, loaded flag was not updated so do it manually + cell.loaded = cell.loaded.addFlag(cellLoadStorage) + counters.storLoaded++ + } + // computeCellHash can reset hash as well so have to check if node has been skipped right after computeCellHash. + } + hph.hadToLoadL[hph.depthsToTxNum[depth]] = counters + /* end of memoization */ + totalBranchLen += hph.computeCellHashLen(cell, depth) bitset ^= bit } @@ -1103,18 +1324,56 @@ func (hph *HexPatriciaHashed) fold() (err error) { return nil, fmt.Errorf("failed to write empty nibble to hash: %w", err) } if hph.trace { - fmt.Printf("%x: empty(%d,%x)\n", nibble, row, nibble) + fmt.Printf(" %x: empty(%d, %x, depth=%d)\n", nibble, row, nibble, depth) } return nil, nil } cell := &hph.grid[row][nibble] + if cell.accountAddrLen > 0 && cell.stateHashLen == 0 && !cell.loaded.account() && !cell.Deleted() { + //panic("account not loaded" + fmt.Sprintf("%x", cell.accountAddr[:cell.accountAddrLen])) + log.Warn("account not loaded", "pref", updateKey, "c", fmt.Sprintf("(%d, %x, depth=%d", row, nibble, depth), "cell", cell.String()) + } + if cell.storageAddrLen > 0 && cell.stateHashLen == 0 && !cell.loaded.storage() && !cell.Deleted() { + //panic("storage not loaded" + fmt.Sprintf("%x", cell.storageAddr[:cell.storageAddrLen])) + log.Warn("storage not loaded", "pref", updateKey, "c", fmt.Sprintf("(%d, %x, depth=%d", row, nibble, depth), "cell", cell.String()) + } + + loadedBefore := cell.loaded + hashBefore := common.Copy(cell.stateHash[:cell.stateHashLen]) + cellHash, err := hph.computeCellHash(cell, depth, hph.hashAuxBuffer[:0]) if err != nil { return nil, err } if hph.trace { - fmt.Printf("%x: computeCellHash(%d,%x,depth=%d)=[%x]\n", nibble, row, nibble, depth, cellHash) + fmt.Printf(" %x: computeCellHash(%d, %x, depth=%d)=[%x]\n", nibble, row, nibble, depth, cellHash) + } + + if hashBefore != nil && (cell.accountAddrLen > 0 || cell.storageAddrLen > 0) { + counters := hph.hadToLoadL[hph.depthsToTxNum[depth]] + if !bytes.Equal(hashBefore, cell.stateHash[:cell.stateHashLen]) { + if cell.accountAddrLen > 0 { + counters.accReset++ + counters.accLoaded++ + } + if cell.storageAddrLen > 0 { + counters.storReset++ + counters.storLoaded++ + } + } else { + if cell.accountAddrLen > 0 && (!loadedBefore.account() && !cell.loaded.account()) { + counters.accSkipped++ + } + if cell.storageAddrLen > 0 && (!loadedBefore.storage() && !cell.loaded.storage()) { + counters.storSkipped++ + } + } + hph.hadToLoadL[hph.depthsToTxNum[depth]] = counters } + //if len(updateKey) > DepthWithoutNodeHashes { + // cell.hashLen = 0 // do not write hashes for storages in the branch node, should reset ext as well which can break unfolding.. - + // cell.extLen = 0 + //} if _, err := hph.keccak2.Write(cellHash); err != nil { return nil, err } @@ -1122,10 +1381,7 @@ func (hph *HexPatriciaHashed) fold() (err error) { return cell, nil } - var lastNibble int - var err error - - lastNibble, err = hph.branchEncoder.CollectUpdate(hph.ctx, updateKey, bitmap, hph.touchMap[row], hph.afterMap[row], cellGetter) + lastNibble, err := hph.branchEncoder.CollectUpdate(hph.ctx, updateKey, bitmap, hph.touchMap[row], hph.afterMap[row], cellGetter) if err != nil { return fmt.Errorf("failed to encode branch update: %w", err) } @@ -1134,7 +1390,7 @@ func (hph *HexPatriciaHashed) fold() (err error) { return err } if hph.trace { - fmt.Printf("%x: empty(%d,%x)\n", i, row, i) + fmt.Printf(" %x: empty(%d, %x, depth=%d)\n", i, row, i, depth) } } upCell.extLen = depth - upDepth - 1 @@ -1199,7 +1455,7 @@ func (hph *HexPatriciaHashed) deleteCell(hashedKey []byte) { cell.reset() } -// fetches cell by key and set touch/after maps +// fetches cell by key and set touch/after maps. Requires that prefix to be already unfolded func (hph *HexPatriciaHashed) updateCell(plainKey, hashedKey []byte, u *Update) (cell *cell) { if u.Deleted() { hph.deleteCell(hashedKey) @@ -1220,7 +1476,7 @@ func (hph *HexPatriciaHashed) updateCell(plainKey, hashedKey []byte, u *Update) hph.touchMap[row] |= col hph.afterMap[row] |= col if hph.trace { - fmt.Printf("updateCell setting (%d, %x), depth=%d\n", row, nibble, depth) + fmt.Printf("updateCell setting (%d, %x, depth=%d)\n", row, nibble, depth) } } if cell.hashedExtLen == 0 { @@ -1237,11 +1493,13 @@ func (hph *HexPatriciaHashed) updateCell(plainKey, hashedKey []byte, u *Update) if len(plainKey) == hph.accountKeyLen { cell.accountAddrLen = len(plainKey) copy(cell.accountAddr[:], plainKey) - copy(cell.CodeHash[:], EmptyCodeHash) + + copy(cell.CodeHash[:], EmptyCodeHash) // todo check } else { // set storage key cell.storageAddrLen = len(plainKey) copy(cell.storageAddr[:], plainKey) } + cell.stateHashLen = 0 cell.setFromUpdate(u) if hph.trace { @@ -1251,6 +1509,7 @@ func (hph *HexPatriciaHashed) updateCell(plainKey, hashedKey []byte, u *Update) } func (hph *HexPatriciaHashed) RootHash() ([]byte, error) { + hph.root.stateHashLen = 0 rootHash, err := hph.computeCellHash(&hph.root, 0, nil) if err != nil { return nil, err @@ -1265,9 +1524,11 @@ func (hph *HexPatriciaHashed) Process(ctx context.Context, updates *Updates, log update *Update updatesCount = updates.Size() + start = time.Now() logEvery = time.NewTicker(20 * time.Second) ) defer logEvery.Stop() + //hph.trace = true err = updates.HashSort(ctx, func(hashedKey, plainKey []byte, stateUpdate *Update) error { select { @@ -1319,7 +1580,7 @@ func (hph *HexPatriciaHashed) Process(ctx context.Context, updates *Updates, log } hph.updateCell(plainKey, hashedKey, update) - mxKeys.Inc() + mxTrieProcessedKeys.Inc() ki++ return nil }) @@ -1345,6 +1606,36 @@ func (hph *HexPatriciaHashed) Process(ctx context.Context, updates *Updates, log if err != nil { return nil, fmt.Errorf("branch update failed: %w", err) } + + log.Debug("commitment finished, counters updated (no reset)", + //"hadToLoad", common.PrettyCounter(hadToLoad.Load()), "skippedLoad", common.PrettyCounter(skippedLoad.Load()), + //"hadToReset", common.PrettyCounter(hadToReset.Load()), + "skip ratio", fmt.Sprintf("%.1f%%", 100*(float64(skippedLoad.Load())/float64(hadToLoad.Load()+skippedLoad.Load()))), + "reset ratio", fmt.Sprintf("%.1f%%", 100*(float64(hadToReset.Load())/float64(hadToLoad.Load()))), + "keys", common.PrettyCounter(ki), "spent", time.Since(start), + ) + ends := make([]uint64, 0, len(hph.hadToLoadL)) + for k := range hph.hadToLoadL { + ends = append(ends, k) + } + sort.Slice(ends, func(i, j int) bool { return ends[i] > ends[j] }) + var Li int + for _, k := range ends { + v := hph.hadToLoadL[k] + accs := fmt.Sprintf("load=%s skip=%s (%.1f%%) reset %.1f%%", common.PrettyCounter(v.accLoaded), common.PrettyCounter(v.accSkipped), 100*(float64(v.accSkipped)/float64(v.accLoaded+v.accSkipped)), 100*(float64(v.accReset)/float64(v.accReset+v.accSkipped))) + stors := fmt.Sprintf("load=%s skip=%s (%.1f%%) reset %.1f%%", common.PrettyCounter(v.storLoaded), common.PrettyCounter(v.storSkipped), 100*(float64(v.storSkipped)/float64(v.storLoaded+v.storSkipped)), 100*(float64(v.storReset)/float64(v.storReset+v.storSkipped))) + if k == 0 { + log.Debug("branchData memoization, new branches", "endStep", k, "accounts", accs, "storages", stors) + } else { + log.Debug("branchData memoization", "L", Li, "endStep", k, "accounts", accs, "storages", stors) + Li++ + + mxTrieStateLevelledSkipRatesAccount[min(Li, 5)].Add(float64(v.accSkipped)) + mxTrieStateLevelledSkipRatesStorage[min(Li, 5)].Add(float64(v.storSkipped)) + mxTrieStateLevelledLoadRatesAccount[min(Li, 5)].Add(float64(v.accLoaded)) + mxTrieStateLevelledLoadRatesStorage[min(Li, 5)].Add(float64(v.storLoaded)) + } + } return rootHash, nil } @@ -1690,6 +1981,77 @@ func (hph *HexPatriciaHashed) SetState(buf []byte) error { return nil } +func HexTrieExtractStateRoot(enc []byte) ([]byte, error) { + if len(enc) < 18 { // 8*2+2 + return nil, fmt.Errorf("invalid state length %x (min %d expected)", len(enc), 18) + } + + //txn := binary.BigEndian.Uint64(enc) + //bn := binary.BigEndian.Uint64(enc[8:]) + sl := binary.BigEndian.Uint16(enc[16:18]) + var s state + if err := s.Decode(enc[18 : 18+sl]); err != nil { + return nil, err + } + root := new(cell) + if err := root.Decode(s.Root); err != nil { + return nil, err + } + return root.hash[:], nil +} + +func HexTrieStateToString(enc []byte) (string, error) { + if len(enc) < 18 { + return "", fmt.Errorf("invalid state length %x (min %d expected)", len(enc), 18) + } + txn := binary.BigEndian.Uint64(enc) + bn := binary.BigEndian.Uint64(enc[8:]) + sl := binary.BigEndian.Uint16(enc[16:18]) + + var s state + sb := new(strings.Builder) + if err := s.Decode(enc[18 : 18+sl]); err != nil { + return "", err + } + fmt.Fprintf(sb, "block: %d txn: %d\n", bn, txn) + // fmt.Fprintf(sb, " touchMaps: %v\n", s.TouchMap) + // fmt.Fprintf(sb, " afterMaps: %v\n", s.AfterMap) + // fmt.Fprintf(sb, " depths: %v\n", s.Depths) + + printAfterMap := func(sb *strings.Builder, name string, list []uint16, depths []int, existedBefore []bool) { + fmt.Fprintf(sb, "\t::%s::\n\n", name) + lastNonZero := 0 + for i := len(list) - 1; i >= 0; i-- { + if list[i] != 0 { + lastNonZero = i + break + } + } + for i, v := range list { + newBranchSuf := "" + if !existedBefore[i] { + newBranchSuf = " NEW" + } + + fmt.Fprintf(sb, " d=%3d %016b%s\n", depths[i], v, newBranchSuf) + if i == lastNonZero { + break + } + } + } + fmt.Fprintf(sb, " rootNode: %x [touched=%t, present=%t, checked=%t]\n", s.Root, s.RootTouched, s.RootPresent, s.RootChecked) + + root := new(cell) + if err := root.Decode(s.Root); err != nil { + return "", err + } + + fmt.Fprintf(sb, "RootHash: %x\n", root.hash) + printAfterMap(sb, "afterMap", s.AfterMap[:], s.Depths[:], s.BranchBefore[:]) + + return sb.String(), nil +} + //func bytesToUint64(buf []byte) (x uint64) { // for i, b := range buf { // x = x<<8 + uint64(b) diff --git a/erigon-lib/commitment/hex_patricia_hashed_test.go b/erigon-lib/commitment/hex_patricia_hashed_test.go index fa31c1b5cbc..c251f98ad3b 100644 --- a/erigon-lib/commitment/hex_patricia_hashed_test.go +++ b/erigon-lib/commitment/hex_patricia_hashed_test.go @@ -369,9 +369,11 @@ func Test_HexPatriciaHashed_UniqueRepresentation(t *testing.T) { Balance("b13363d527cdc18173c54ac5d4a54af05dbec22e", 4*1e17). Balance("d995768ab23a0a333eb9584df006da740e66f0aa", 5). Balance("eabf041afbb6c6059fbd25eab0d3202db84e842d", 6). + Balance("8e5476fc5990638a4fb0b5fd3f61bb4b5c5f395e", 1237). Balance("93fe03620e4d70ea39ab6e8c0e04dd0d83e041f2", 7). Balance("ba7a3b7b095d3370c022ca655c790f0c0ead66f5", 5*1e17). Storage("ba7a3b7b095d3370c022ca655c790f0c0ead66f5", "0fa41642c48ecf8f2059c275353ce4fee173b3a8ce5480f040c4d2901603d14e", "050505"). + CodeHash("ba7a3b7b095d3370c022ca655c790f0c0ead66f5", "24f3a02dc65eda502dbf75919e795458413d3c45b38bb35b51235432707900ed"). Balance("a8f8d73af90eee32dc9729ce8d5bb762f30d21a4", 9*1e16). Storage("93fe03620e4d70ea39ab6e8c0e04dd0d83e041f2", "de3fea338c95ca16954e80eb603cd81a261ed6e2b10a03d0c86cf953fe8769a4", "060606"). Balance("14c4d3bba7f5009599257d3701785d34c7f2aa27", 6*1e18). @@ -381,7 +383,9 @@ func Test_HexPatriciaHashed_UniqueRepresentation(t *testing.T) { Build() trieSequential := NewHexPatriciaHashed(length.Addr, stateSeq, stateSeq.TempDir()) + trieSequential.trace = true trieBatch := NewHexPatriciaHashed(length.Addr, stateBatch, stateBatch.TempDir()) + trieBatch.trace = true plainKeys, updates = sortUpdatesByHashIncrease(t, trieSequential, plainKeys, updates) @@ -518,17 +522,7 @@ func Test_Cell_EncodeDecode(t *testing.T) { err := second.Decode(first.Encode()) require.NoError(t, err) - require.EqualValues(t, first.hashedExtLen, second.hashedExtLen) - require.EqualValues(t, first.hashedExtension[:], second.hashedExtension[:]) - require.EqualValues(t, first.accountAddrLen, second.accountAddrLen) - require.EqualValues(t, first.storageAddrLen, second.storageAddrLen) - require.EqualValues(t, first.hashLen, second.hashLen) - require.EqualValues(t, first.accountAddr[:], second.accountAddr[:]) - require.EqualValues(t, first.storageAddr[:], second.storageAddr[:]) - require.EqualValues(t, first.hash[:], second.hash[:]) - require.EqualValues(t, first.extension[:first.extLen], second.extension[:second.extLen]) - - // encode doesn't code Nonce, Balance, CodeHash and Storage, Delete fields + cellMustEqual(t, first, second) } func Test_HexPatriciaHashed_StateEncode(t *testing.T) { @@ -1159,3 +1153,188 @@ func TestCell_setFromUpdate(t *testing.T) { require.EqualValues(t, update.StorageLen, target.StorageLen) require.EqualValues(t, update.Storage[:update.StorageLen], target.Storage[:target.StorageLen]) } + +func TestCell_fillFromFields(t *testing.T) { + row, bm := generateCellRow(t, 16) + rnd := rand.New(rand.NewSource(0)) + + cg := func(nibble int, skip bool) (*cell, error) { + c := row[nibble] + if c.storageAddrLen > 0 || c.accountAddrLen > 0 { + rnd.Read(c.stateHash[:]) + c.stateHashLen = 32 + } + fmt.Printf("enc cell %x %v\n", nibble, c.FullString()) + + return c, nil + } + + be := NewBranchEncoder(1024, t.TempDir()) + enc, _, err := be.EncodeBranch(bm, bm, bm, cg) + require.NoError(t, err) + + //original := common.Copy(enc) + fmt.Printf("%s\n", enc.String()) + + tm, am, decRow, err := enc.decodeCells() + require.NoError(t, err) + require.EqualValues(t, bm, am) + require.EqualValues(t, bm, tm) + + for i := 0; i < len(decRow); i++ { + t.Logf("cell %d\n", i) + first, second := row[i], decRow[i] + // after decoding extension == hashedExtension, dhk will be derived from extension + require.EqualValues(t, second.extLen, second.hashedExtLen) + require.EqualValues(t, first.extLen, second.hashedExtLen) + require.EqualValues(t, second.extension[:second.extLen], second.hashedExtension[:second.hashedExtLen]) + + require.EqualValues(t, first.hashLen, second.hashLen) + require.EqualValues(t, first.hash[:first.hashLen], second.hash[:second.hashLen]) + require.EqualValues(t, first.accountAddrLen, second.accountAddrLen) + require.EqualValues(t, first.storageAddrLen, second.storageAddrLen) + require.EqualValues(t, first.accountAddr[:], second.accountAddr[:]) + require.EqualValues(t, first.storageAddr[:], second.storageAddr[:]) + require.EqualValues(t, first.extension[:first.extLen], second.extension[:second.extLen]) + require.EqualValues(t, first.stateHash[:first.stateHashLen], second.stateHash[:second.stateHashLen]) + } +} + +func cellMustEqual(tb testing.TB, first, second *cell) { + tb.Helper() + require.EqualValues(tb, first.hashedExtLen, second.hashedExtLen) + require.EqualValues(tb, first.hashedExtension[:first.hashedExtLen], second.hashedExtension[:second.hashedExtLen]) + require.EqualValues(tb, first.hashLen, second.hashLen) + require.EqualValues(tb, first.hash[:first.hashLen], second.hash[:second.hashLen]) + require.EqualValues(tb, first.accountAddrLen, second.accountAddrLen) + require.EqualValues(tb, first.storageAddrLen, second.storageAddrLen) + require.EqualValues(tb, first.accountAddr[:], second.accountAddr[:]) + require.EqualValues(tb, first.storageAddr[:], second.storageAddr[:]) + require.EqualValues(tb, first.extension[:first.extLen], second.extension[:second.extLen]) + require.EqualValues(tb, first.stateHash[:first.stateHashLen], second.stateHash[:second.stateHashLen]) + + // encode doesn't code Nonce, Balance, CodeHash and Storage, Delete fields +} + +func Test_HexPatriciaHashed_ProcessWithDozensOfStorageKeys(t *testing.T) { + ctx := context.Background() + msOne := NewMockState(t) + msTwo := NewMockState(t) + + plainKeys, updates := NewUpdateBuilder(). + Balance("00000000000000000000000000000000000000f5", 4). + Balance("00000000000000000000000000000000000000ff", 900234). + Balance("0000000000000000000000000000000000000004", 1233). + Storage("0000000000000000000000000000000000000004", "01", "0401"). + Balance("00000000000000000000000000000000000000ba", 065606). + Balance("0000000000000000000000000000000000000000", 4). + Balance("0000000000000000000000000000000000000001", 5). + Balance("0000000000000000000000000000000000000002", 6). + Balance("0000000000000000000000000000000000000003", 7). + Storage("0000000000000000000000000000000000000003", "56", "050505"). + Balance("0000000000000000000000000000000000000005", 9). + Storage("0000000000000000000000000000000000000003", "87", "060606"). + Balance("00000000000000000000000000000000000000b9", 6). + Nonce("00000000000000000000000000000000000000ff", 169356). + Storage("0000000000000000000000000000000000000005", "02", "8989"). + Storage("00000000000000000000000000000000000000f5", "04", "9898"). + Storage("00000000000000000000000000000000000000f5", "05", "1234"). + Storage("00000000000000000000000000000000000000f5", "06", "5678"). + Storage("00000000000000000000000000000000000000f5", "07", "9abc"). + Storage("00000000000000000000000000000000000000f5", "08", "def0"). + Storage("00000000000000000000000000000000000000f5", "09", "1111"). + Storage("00000000000000000000000000000000000000f5", "0a", "2222"). + Storage("00000000000000000000000000000000000000f5", "0b", "3333"). + Storage("00000000000000000000000000000000000000f5", "0c", "4444"). + Storage("00000000000000000000000000000000000000f5", "0d", "5555"). + Storage("00000000000000000000000000000000000000f5", "0e", "6666"). + Storage("00000000000000000000000000000000000000f5", "0f", "7777"). + Storage("00000000000000000000000000000000000000f5", "10", "8888"). + Storage("00000000000000000000000000000000000000f5", "11", "9999"). + Storage("00000000000000000000000000000000000000f5", "d680a8cdb8eeb05a00b8824165b597d7a2c2f608057537dd2cee058569114be0", "aaaa"). + Storage("00000000000000000000000000000000000000f5", "e9018287c0d9d38524c16f7450cf3ed7ca7b2a466a4746910462343626cb7e9b", "bbbb"). + Storage("00000000000000000000000000000000000000f5", "e5635458dccace734b0f3fe6bae307a6d23282dae083218bd0db7ecf8b784b41", "cccc"). + Storage("00000000000000000000000000000000000000f5", "0a1c82a16bce90d07e4aed8d44cb584b25f39d8d8dd61dea068f144e985326a2", "dddd"). + Storage("00000000000000000000000000000000000000f5", "778e0ba7ae9d62a62b883cfb447343673f37854d335595b4934b2c20ff936a5f", "eeee"). + Storage("00000000000000000000000000000000000000f5", "787ec6ab994586c0f3116e311c61479d4a171287ef1b4a97afcce56044d698dc", "ffff"). + Storage("00000000000000000000000000000000000000f5", "1bf6be2031cd9a8e204ffae1fea4dcfef0c85fb20d189a0a7b0880ef9b7bb3c7", "0000"). + Storage("00000000000000000000000000000000000000f5", "ab4756ebb7abc2631dddf5f362155e571c947465add47812794d8641ff04c283", "1111"). + Storage("00000000000000000000000000000000000000f5", "f094bf04ad37fc7aa047784f3346e12ed72b799fc7dc70c9d8eac296829c592e", "2222"). + Storage("00000000000000000000000000000000000000f5", "c88ebea9f05008643aa43f6f610eec0f81c3d736c3a85b12a09034359d744021", "4444"). + Storage("00000000000000000000000000000000000000f5", "58a60d4461d743243c8d77a05708351bde842bf3702dfb3276a6a948603dca7d", "ffff"). + Storage("00000000000000000000000000000000000000f5", "377c067adec6f257f25dff4bc98fd74800df84974189199801ed8b560c805a95", "aaaa"). + Storage("00000000000000000000000000000000000000f5", "c8a1d3e638914407d095a9a0f785d5dac4ad580bca47c924d6864e1431b74a23", "eeee"). + Storage("00000000000000000000000000000000000000f5", "1f00000000000000000000000000000000000000f5", "00000000000000000000000000000000000000f5"). + Build() + + trieOne := NewHexPatriciaHashed(length.Addr, msOne, msOne.TempDir()) + plainKeys, updates = sortUpdatesByHashIncrease(t, trieOne, plainKeys, updates) + + //rnd := rand.New(rand.NewSource(345)) + //noise := make([]byte, 32) + //prefixes := make(map[string][][]byte) + //prefixesCnt := make(map[string]int) + //for i := 0; i < 5000000; i++ { + // rnd.Read(noise) + // //hashed := trieOne.hashAndNibblizeKey(noise) + // trieOne.keccak.Reset() + // trieOne.keccak.Write(noise) + // hashed := make([]byte, 32) + // trieOne.keccak.Read(hashed) + // prefixesCnt[string(hashed[:5])]++ + // if c := prefixesCnt[string(hashed[:5])]; c < 5 { + // prefixes[string(hashed[:5])] = append(prefixes[string(hashed[:5])], common.Copy(noise)) + // } + //} + // + //count := 0 + //for pref, cnt := range prefixesCnt { + // if cnt > 1 { + // for _, noise := range prefixes[pref] { + // fmt.Printf("%x %x\n", pref, noise) + // count++ + // } + // } + //} + //fmt.Printf("total %d\n", count) + + trieTwo := NewHexPatriciaHashed(length.Addr, msTwo, msTwo.TempDir()) + + trieOne.SetTrace(true) + trieTwo.SetTrace(true) + + var rSeq, rBatch []byte + { + fmt.Printf("1. Trie sequential update (%d updates)\n", len(updates)) + for i := 0; i < len(updates); i++ { + err := msOne.applyPlainUpdates(plainKeys[i:i+1], updates[i:i+1]) + require.NoError(t, err) + + updsOne := WrapKeyUpdates(t, ModeDirect, trieOne.hashAndNibblizeKey, plainKeys[i:i+1], updates[i:i+1]) + + sequentialRoot, err := trieOne.Process(ctx, updsOne, "") + require.NoError(t, err) + + t.Logf("sequential root @%d hash %x\n", i, sequentialRoot) + rSeq = common.Copy(sequentialRoot) + + updsOne.Close() + } + } + { + err := msTwo.applyPlainUpdates(plainKeys, updates) + require.NoError(t, err) + + updsTwo := WrapKeyUpdates(t, ModeDirect, trieTwo.hashAndNibblizeKey, plainKeys, updates) + + fmt.Printf("\n2. Trie batch update (%d updates)\n", len(updates)) + rh, err := trieTwo.Process(ctx, updsTwo, "") + require.NoError(t, err) + t.Logf("batch of %d root hash %x\n", len(updates), rh) + + updsTwo.Close() + + rBatch = common.Copy(rh) + } + require.EqualValues(t, rBatch, rSeq, "sequential and batch root should match") +} diff --git a/erigon-lib/common/metrics/block_metrics.go b/erigon-lib/common/metrics/block_metrics.go index a5fa0ba043b..248662a5241 100644 --- a/erigon-lib/common/metrics/block_metrics.go +++ b/erigon-lib/common/metrics/block_metrics.go @@ -31,6 +31,7 @@ var ( BlockConsumerPreExecutionDelay = metrics.NewSummary(`block_consumer_delay{type="pre_execution"}`) BlockConsumerPostExecutionDelay = metrics.NewSummary(`block_consumer_delay{type="post_execution"}`) BlockProducerProductionDelay = metrics.NewSummary(`block_producer_delay{type="production"}`) + ChainTipMgasPerSec = metrics.NewGauge(`chain_tip_mgas_per_sec`) ) func UpdateBlockConsumerHeaderDownloadDelay(blockTime uint64, blockNumber uint64, log log.Logger) { diff --git a/erigon-lib/config3/config3.go b/erigon-lib/config3/config3.go index 050865ecf1e..43e089d9ede 100644 --- a/erigon-lib/config3/config3.go +++ b/erigon-lib/config3/config3.go @@ -18,7 +18,7 @@ package config3 // AggregationStep number of transactions in smallest static file const HistoryV3AggregationStep = 1_562_500 // = 100M / 64. Dividers: 2, 5, 10, 20, 50, 100, 500 -//const HistoryV3AggregationStep = 1_562_500 / 10 +// const HistoryV3AggregationStep = 1_562_500 / 10 const EnableHistoryV4InTest = true diff --git a/erigon-lib/downloader/webseed.go b/erigon-lib/downloader/webseed.go index 1619fa66ae4..69a9298768b 100644 --- a/erigon-lib/downloader/webseed.go +++ b/erigon-lib/downloader/webseed.go @@ -664,7 +664,7 @@ func (d *WebSeeds) callTorrentHttpProvider(ctx context.Context, url *url.URL, fi } res, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("webseed.downloadTorrentFile: host=%s, url=%s, %w", url.Hostname(), url.EscapedPath(), err) + return nil, fmt.Errorf("webseed.downloadTorrentFile: read body: host=%s, url=%s, %w", url.Hostname(), url.EscapedPath(), err) } if err = validateTorrentBytes(fileName, res, d.torrentsWhitelist); err != nil { return nil, fmt.Errorf("webseed.downloadTorrentFile: host=%s, url=%s, %w", url.Hostname(), url.EscapedPath(), err) diff --git a/erigon-lib/seg/decompress.go b/erigon-lib/seg/decompress.go index 0bed6a8e422..76553c0522a 100644 --- a/erigon-lib/seg/decompress.go +++ b/erigon-lib/seg/decompress.go @@ -566,6 +566,11 @@ func (g *Getter) Trace(t bool) { g.trace = t } func (g *Getter) FileName() string { return g.fName } func (g *Getter) nextPos(clean bool) (pos uint64) { + defer func() { + if rec := recover(); rec != nil { + panic(fmt.Sprintf("nextPos fails: file: %s, %s, %s", g.fName, rec, dbg.Stack())) + } + }() if clean && g.dataBit > 0 { g.dataP++ g.dataBit = 0 diff --git a/erigon-lib/state/aggregator.go b/erigon-lib/state/aggregator.go index 4931a5b7f35..6d8de00bb38 100644 --- a/erigon-lib/state/aggregator.go +++ b/erigon-lib/state/aggregator.go @@ -851,12 +851,13 @@ type flusher interface { } func (ac *AggregatorRoTx) minimaxTxNumInDomainFiles() uint64 { - return min( + m := min( ac.d[kv.AccountsDomain].files.EndTxNum(), ac.d[kv.CodeDomain].files.EndTxNum(), ac.d[kv.StorageDomain].files.EndTxNum(), ac.d[kv.CommitmentDomain].files.EndTxNum(), ) + return m } func (ac *AggregatorRoTx) CanPrune(tx kv.Tx, untilTx uint64) bool { @@ -1342,12 +1343,22 @@ func (a *Aggregator) DirtyFilesEndTxNumMinimax() uint64 { } func (a *Aggregator) dirtyFilesEndTxNumMinimax() uint64 { - return min( + m := min( a.d[kv.AccountsDomain].dirtyFilesEndTxNumMinimax(), a.d[kv.StorageDomain].dirtyFilesEndTxNumMinimax(), a.d[kv.CodeDomain].dirtyFilesEndTxNumMinimax(), - a.d[kv.CommitmentDomain].dirtyFilesEndTxNumMinimax(), + // a.d[kv.CommitmentDomain].dirtyFilesEndTxNumMinimax(), ) + // TODO(awskii) have two different functions including commitment/without it + // Usually its skipped because commitment either have MaxUint64 due to no history or equal to other domains + + //log.Warn("dirtyFilesEndTxNumMinimax", "min", m, + // "acc", a.d[kv.AccountsDomain].dirtyFilesEndTxNumMinimax(), + // "sto", a.d[kv.StorageDomain].dirtyFilesEndTxNumMinimax(), + // "cod", a.d[kv.CodeDomain].dirtyFilesEndTxNumMinimax(), + // "com", a.d[kv.CommitmentDomain].dirtyFilesEndTxNumMinimax(), + //) + return m } func (a *Aggregator) recalcVisibleFiles(toTxNum uint64) { @@ -1620,6 +1631,12 @@ func (a *Aggregator) BuildFilesInBackground(txNum uint64) chan struct{} { } step := a.visibleFilesMinimaxTxNum.Load() / a.StepSize() + lastInDB := max( + lastIdInDB(a.db, a.d[kv.AccountsDomain]), + lastIdInDB(a.db, a.d[kv.CodeDomain]), + lastIdInDB(a.db, a.d[kv.StorageDomain]), + lastIdInDBNoHistory(a.db, a.d[kv.CommitmentDomain])) + log.Info("BuildFilesInBackground", "step", step, "lastInDB", lastInDB) a.wg.Add(1) go func() { defer a.wg.Done() @@ -1634,8 +1651,14 @@ func (a *Aggregator) BuildFilesInBackground(txNum uint64) chan struct{} { defer a.snapshotBuildSema.Release(1) } + lastInDB := max( + lastIdInDB(a.db, a.d[kv.AccountsDomain]), + lastIdInDB(a.db, a.d[kv.CodeDomain]), + lastIdInDB(a.db, a.d[kv.StorageDomain]), + lastIdInDBNoHistory(a.db, a.d[kv.CommitmentDomain])) + // check if db has enough data (maybe we didn't commit them yet or all keys are unique so history is empty) - lastInDB := lastIdInDB(a.db, a.d[kv.AccountsDomain]) + //lastInDB := lastIdInDB(a.db, a.d[kv.AccountsDomain]) hasData := lastInDB > step // `step` must be fully-written - means `step+1` records must be visible if !hasData { close(fin) @@ -1646,7 +1669,7 @@ func (a *Aggregator) BuildFilesInBackground(txNum uint64) chan struct{} { // - to reduce amount of small merges // - to remove old data from db as early as possible // - during files build, may happen commit of new data. on each loop step getting latest id in db - for ; step < lastIdInDB(a.db, a.d[kv.AccountsDomain]); step++ { //`step` must be fully-written - means `step+1` records must be visible + for ; step < lastInDB; step++ { //`step` must be fully-written - means `step+1` records must be visible if err := a.buildFiles(a.ctx, step); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, common2.ErrStopped) { close(fin) @@ -1764,6 +1787,27 @@ func (ac *AggregatorRoTx) HistoryRange(name kv.History, fromTs, toTs int, asc or return stream.WrapKV(hr), nil } +func (ac *AggregatorRoTx) KeyCountInDomainRange(d kv.Domain, start, end uint64) (totalKeys uint64) { + if d >= kv.DomainLen { + return 0 + } + + for _, f := range ac.d[d].visible.files { + if f.startTxNum >= start && f.endTxNum <= end { + totalKeys += uint64(f.src.decompressor.Count() / 2) + } + } + return totalKeys +} + +func (ac *AggregatorRoTx) nastyFileRead(name kv.Domain, from, to uint64) (*seg.Reader, error) { + fi := ac.d[name].statelessFileIndex(from, to) + if fi < 0 { + return nil, fmt.Errorf("file not found") + } + return ac.d[name].statelessGetter(fi), nil +} + // AggregatorRoTx guarantee consistent View of files ("snapshots isolation" level https://en.wikipedia.org/wiki/Snapshot_isolation): // - long-living consistent view of all files (no limitations) // - hiding garbage and files overlaps @@ -1807,6 +1851,9 @@ func (ac *AggregatorRoTx) DomainRange(ctx context.Context, tx kv.Tx, domain kv.D func (ac *AggregatorRoTx) DomainRangeLatest(tx kv.Tx, domain kv.Domain, from, to []byte, limit int) (stream.KV, error) { return ac.d[domain].DomainRangeLatest(tx, from, to, limit) } +func (ac *AggregatorRoTx) DomainGetAsOfFile(name kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) { + return ac.d[name].GetAsOfFile(key, ts) +} func (ac *AggregatorRoTx) DomainGetAsOf(tx kv.Tx, name kv.Domain, key []byte, ts uint64) (v []byte, ok bool, err error) { return ac.d[name].GetAsOf(key, ts, tx) @@ -1917,3 +1964,14 @@ func lastIdInDB(db kv.RoDB, domain *Domain) (lstInDb uint64) { } return lstInDb } + +func lastIdInDBNoHistory(db kv.RoDB, domain *Domain) (lstInDb uint64) { + if err := db.View(context.Background(), func(tx kv.Tx) error { + //lstInDb = domain.maxStepInDB(tx) + lstInDb = domain.maxStepInDBNoHistory(tx) + return nil + }); err != nil { + log.Warn("[snapshots] lastIdInDB", "err", err) + } + return lstInDb +} diff --git a/erigon-lib/state/aggregator_test.go b/erigon-lib/state/aggregator_test.go index f2e1d3c7a9c..a70e619ea00 100644 --- a/erigon-lib/state/aggregator_test.go +++ b/erigon-lib/state/aggregator_test.go @@ -22,10 +22,12 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "github.com/erigontech/erigon-lib/commitment" "math" "math/rand" "os" "path" + "path/filepath" "strings" "sync/atomic" "testing" @@ -499,6 +501,9 @@ func TestAggregatorV3_PruneSmallBatches(t *testing.T) { err = tx.Commit() require.NoError(t, err) + err = agg.BuildFiles(maxTx) + require.NoError(t, err) + buildTx, err := db.BeginRw(context.Background()) require.NoError(t, err) defer func() { @@ -506,10 +511,6 @@ func TestAggregatorV3_PruneSmallBatches(t *testing.T) { buildTx.Rollback() } }() - - err = agg.BuildFiles(maxTx) - require.NoError(t, err) - ac = agg.BeginFilesRo() for i := 0; i < 10; i++ { _, err = ac.PruneSmallBatches(context.Background(), time.Second*3, buildTx) @@ -998,6 +999,7 @@ func Test_EncodeCommitmentState(t *testing.T) { require.EqualValues(t, cs.trieState, dec.trieState) } +// takes first 100k keys from file func pivotKeysFromKV(dataPath string) ([][]byte, error) { decomp, err := seg.NewDecompressor(dataPath) if err != nil { @@ -1264,3 +1266,110 @@ func Test_helper_decodeAccountv3Bytes(t *testing.T) { n, b, ch := types.DecodeAccountBytesV3(input) fmt.Printf("input %x nonce %d balance %d codeHash %d\n", input, n, b.Uint64(), ch) } + +func TestAggregator_RebuildCommitmentBasedOnFiles(t *testing.T) { + db, agg := testDbAndAggregatorv3(t, 20) + + ctx := context.Background() + agg.logger = log.Root().New() + + ac := agg.BeginFilesRo() + defer ac.Close() + + rwTx, err := db.BeginRw(context.Background()) + require.NoError(t, err) + defer rwTx.Rollback() + + domains, err := NewSharedDomains(WrapTxWithCtx(rwTx, ac), log.New()) + require.NoError(t, err) + defer domains.Close() + + txCount := 640 // will produce files up to step 31, good because covers different ranges (16, 8, 4, 2, 1) + + keys, vals := generateInputData(t, 20, 16, txCount) + t.Logf("keys %d vals %d\n", len(keys), len(vals)) + + for i := 0; i < len(vals); i++ { + domains.SetTxNum(uint64(i)) + + for j := 0; j < len(keys); j++ { + buf := types.EncodeAccountBytesV3(uint64(i), uint256.NewInt(uint64(i*100_000)), nil, 0) + prev, step, err := domains.DomainGet(kv.AccountsDomain, keys[j], nil) + require.NoError(t, err) + + err = domains.DomainPut(kv.AccountsDomain, keys[j], nil, buf, prev, step) + require.NoError(t, err) + } + if uint64(i+1)%agg.StepSize() == 0 { + rh, err := domains.ComputeCommitment(ctx, true, domains.BlockNum(), "") + require.NoError(t, err) + require.NotEmpty(t, rh) + } + } + + err = domains.Flush(context.Background(), rwTx) + require.NoError(t, err) + domains.Close() // closes ac + + require.NoError(t, rwTx.Commit()) + + // build files out of db + err = agg.BuildFiles(uint64(txCount)) + require.NoError(t, err) + + ac = agg.BeginFilesRo() + roots := make([]common.Hash, 0) + + // collect latest root from each available file + compression := ac.d[kv.CommitmentDomain].d.compression + fnames := []string{} + for _, f := range ac.d[kv.CommitmentDomain].files { + k, stateVal, _, found, err := f.src.bindex.Get(keyCommitmentState, seg.NewReader(f.src.decompressor.MakeGetter(), compression)) + require.NoError(t, err) + require.True(t, found) + require.EqualValues(t, keyCommitmentState, k) + rh, err := commitment.HexTrieExtractStateRoot(stateVal) + require.NoError(t, err) + + roots = append(roots, common.BytesToHash(rh)) + fmt.Printf("file %s root %x\n", filepath.Base(f.src.decompressor.FilePath()), rh) + fnames = append(fnames, f.src.decompressor.FilePath()) + } + ac.Close() + agg.d[kv.CommitmentDomain].closeFilesAfterStep(0) // close commitment files to remove + + // now clean all commitment files along with related db buckets + rwTx, err = db.BeginRw(context.Background()) + require.NoError(t, err) + defer rwTx.Rollback() + + buckets, err := rwTx.ListBuckets() + require.NoError(t, err) + for i, b := range buckets { + if strings.Contains(strings.ToLower(b), "commitment") { + size, err := rwTx.BucketSize(b) + require.NoError(t, err) + t.Logf("cleaned table #%d %s: %d keys", i, b, size) + + err = rwTx.ClearBucket(b) + require.NoError(t, err) + } + } + require.NoError(t, rwTx.Commit()) + + for _, fn := range fnames { + if strings.Contains(fn, "v1-commitment") { + require.NoError(t, os.Remove(fn)) + t.Logf("removed file %s", filepath.Base(fn)) + } + } + err = agg.OpenFolder() + require.NoError(t, err) + + finalRoot, err := agg.RebuildCommitmentFiles(ctx, nil, &rawdbv3.TxNums) + require.NoError(t, err) + require.NotEmpty(t, finalRoot) + require.NotEqualValues(t, commitment.EmptyRootHash, finalRoot) + + require.EqualValues(t, roots[len(roots)-1][:], finalRoot[:]) +} diff --git a/erigon-lib/state/bps_tree.go b/erigon-lib/state/bps_tree.go index 7011530f26e..d039f8ef5dd 100644 --- a/erigon-lib/state/bps_tree.go +++ b/erigon-lib/state/bps_tree.go @@ -246,7 +246,7 @@ func (b *BpsTree) WarmUp(kv *seg.Reader) (err error) { } log.Root().Debug("WarmUp finished", "file", kv.FileName(), "M", b.M, "N", common.PrettyCounter(N), - "cached", fmt.Sprintf("%d %.2f%%", len(b.mx), 100*(float64(len(b.mx))/float64(N)*100)), + "cached", fmt.Sprintf("%d %.2f%%", len(b.mx), 100*(float64(len(b.mx))/float64(N))), "cacheSize", datasize.ByteSize(cachedBytes).HR(), "fileSize", datasize.ByteSize(kv.Size()).HR(), "took", time.Since(t)) return nil diff --git a/erigon-lib/state/domain.go b/erigon-lib/state/domain.go index 7357753f27f..23554a3df69 100644 --- a/erigon-lib/state/domain.go +++ b/erigon-lib/state/domain.go @@ -185,6 +185,30 @@ func (d *Domain) maxStepInDB(tx kv.Tx) (lstInDb uint64) { } return binary.BigEndian.Uint64(lstIdx) / d.aggregationStep } + +// maxStepInDBNoHistory - return latest available step in db (at-least 1 value in such step) +// Does not use history table to find the latest step +func (d *Domain) maxStepInDBNoHistory(tx kv.Tx) (lstInDb uint64) { + lstIdx, err := kv.FirstKey(tx, d.valsTable) + if err != nil { + d.logger.Warn("Domain.maxStepInDBNoHistory:", "FirstKey", lstIdx, "err", err) + return 0 + } + if len(lstIdx) == 0 { + return 0 + } + if d.largeVals { + return (^binary.BigEndian.Uint64(lstIdx[len(lstIdx)-8:])) / d.aggregationStep + } + lstVal, err := tx.GetOne(d.valsTable, lstIdx) + if err != nil { + d.logger.Warn("Domain.maxStepInDBNoHistory:", "GetOne", lstIdx, "err", err) + return 0 + } + + return ^binary.BigEndian.Uint64(lstVal) +} + func (d *Domain) minStepInDB(tx kv.Tx) (lstInDb uint64) { lstIdx, _ := kv.FirstKey(tx, d.History.indexKeysTable) if len(lstIdx) == 0 { @@ -878,10 +902,9 @@ func (d *Domain) collectFilesStats() (datsz, idxsz, files uint64) { } func (d *Domain) BeginFilesRo() *DomainRoTx { - files := d._visible.files - for i := 0; i < len(files); i++ { - if !files[i].src.frozen { - files[i].src.refcount.Add(1) + for i := 0; i < len(d._visible.files); i++ { + if !d._visible.files[i].src.frozen { + d._visible.files[i].src.refcount.Add(1) } } @@ -909,6 +932,127 @@ func (c Collation) Close() { c.HistoryCollation.Close() } +func (d *Domain) DumpStepRangeOnDisk(ctx context.Context, stepFrom, stepTo, txnFrom, txnTo uint64, wal *domainBufferedWriter, vt valueTransformer) error { + if stepFrom == stepTo { + return nil + } + if stepFrom > stepTo { + panic(fmt.Errorf("assert: stepFrom=%d > stepTo=%d", stepFrom, stepTo)) + } + + coll, err := d.collateETL(ctx, stepFrom, stepTo, wal.values, vt) + defer wal.close() + if err != nil { + return err + } + wal.close() + + ps := background.NewProgressSet() + static, err := d.buildFileRange(ctx, stepFrom, stepTo, coll, ps) + if err != nil { + return err + } + + d.integrateDirtyFiles(static, txnFrom, txnTo) + // d.reCalcVisibleFiles(d.dirtyFilesEndTxNumMinimax()) + return nil +} + +// [stepFrom; stepTo) +// In contrast to collate function collateETL puts contents of wal into file. +func (d *Domain) collateETL(ctx context.Context, stepFrom, stepTo uint64, wal *etl.Collector, vt valueTransformer) (coll Collation, err error) { + started := time.Now() + closeCollation := true + defer func() { + if closeCollation { + coll.Close() + } + d.stats.LastCollationTook = time.Since(started) + mxCollateTook.ObserveDuration(started) + }() + + coll.valuesPath = d.kvFilePath(stepFrom, stepTo) + if coll.valuesComp, err = seg.NewCompressor(ctx, d.filenameBase+".domain.collate", coll.valuesPath, d.dirs.Tmp, d.compressCfg, log.LvlTrace, d.logger); err != nil { + return Collation{}, fmt.Errorf("create %s values compressor: %w", d.filenameBase, err) + } + + // Don't use `d.compress` config in collate. Because collat+build must be very-very fast (to keep db small). + // Compress files only in `merge` which ok to be slow. + //comp := seg.NewWriter(coll.valuesComp, seg.CompressNone) // + compress := seg.CompressNone + if stepTo-stepFrom > DomainMinStepsToCompress { + compress = d.compression + } + comp := seg.NewWriter(coll.valuesComp, compress) + + stepBytes := make([]byte, 8) + binary.BigEndian.PutUint64(stepBytes, ^stepTo) + + kvs := make([]struct { + k, v []byte + }, 0, 128) + var fromTxNum, endTxNum uint64 = 0, stepTo * d.aggregationStep + if stepFrom > 0 { + fromTxNum = (stepFrom - 1) * d.aggregationStep + } + + //var stepInDB []byte + err = wal.Load(nil, "", func(k, v []byte, table etl.CurrentTableReader, next etl.LoadNextFunc) error { + if d.largeVals { + kvs = append(kvs, struct { + k, v []byte + }{k[:len(k)-8], v}) + } else { + + if vt != nil { + v, err = vt(v[8:], fromTxNum, endTxNum) + if err != nil { + return fmt.Errorf("vt: %w", err) + } + } else { + v = v[8:] + } + if err = comp.AddWord(k); err != nil { + return fmt.Errorf("add %s values key [%x]: %w", d.filenameBase, k, err) + } + if err = comp.AddWord(v); err != nil { + return fmt.Errorf("add %s values [%x]=>[%x]: %w", d.filenameBase, k, v, err) + } + } + return nil + }, etl.TransformArgs{Quit: ctx.Done()}) + + sort.Slice(kvs, func(i, j int) bool { + return bytes.Compare(kvs[i].k, kvs[j].k) < 0 + }) + // check if any key is duplicated + for i := 1; i < len(kvs); i++ { + if bytes.Equal(kvs[i].k, kvs[i-1].k) { + return coll, fmt.Errorf("duplicate key [%x]", kvs[i].k) + } + } + for _, kv := range kvs { + if vt != nil { + kv.v, err = vt(kv.v, fromTxNum, endTxNum) + } + if err != nil { + return coll, fmt.Errorf("vt: %w", err) + } + if err = comp.AddWord(kv.k); err != nil { + return coll, fmt.Errorf("add %s values key [%x]: %w", d.filenameBase, kv.k, err) + } + if err = comp.AddWord(kv.v); err != nil { + return coll, fmt.Errorf("add %s values [%x]=>[%x]: %w", d.filenameBase, kv.k, kv.v, err) + } + } + // could also do key squeezing + + closeCollation = false + coll.valuesCount = coll.valuesComp.Count() / 2 + mxCollationSize.SetUint64(uint64(coll.valuesCount)) + return coll, nil +} + // collate gathers domain changes over the specified step, using read-only transaction, // and returns compressors, elias fano, and bitmaps // [txFrom; txTo) @@ -1052,6 +1196,100 @@ func (sf StaticFiles) CleanupOnError() { sf.HistoryFiles.CleanupOnError() } +// skips history files +func (d *Domain) buildFileRange(ctx context.Context, stepFrom, stepTo uint64, collation Collation, ps *background.ProgressSet) (StaticFiles, error) { + mxRunningFilesBuilding.Inc() + defer mxRunningFilesBuilding.Dec() + if traceFileLife != "" && d.filenameBase == traceFileLife { + d.logger.Warn("[agg.dbg] buildFilesRange", "step", fmt.Sprintf("%d-%d", stepFrom, stepTo), "domain", d.filenameBase) + } + + start := time.Now() + defer func() { + d.stats.LastFileBuildingTook = time.Since(start) + mxBuildTook.ObserveDuration(start) + }() + + valuesComp := collation.valuesComp + + var ( + valuesDecomp *seg.Decompressor + valuesIdx *recsplit.Index + bt *BtIndex + bloom *ExistenceFilter + err error + ) + closeComp := true + defer func() { + if closeComp { + if valuesComp != nil { + valuesComp.Close() + } + if valuesDecomp != nil { + valuesDecomp.Close() + } + if valuesIdx != nil { + valuesIdx.Close() + } + if bt != nil { + bt.Close() + } + if bloom != nil { + bloom.Close() + } + } + }() + if d.noFsync { + valuesComp.DisableFsync() + } + if err = valuesComp.Compress(); err != nil { + return StaticFiles{}, fmt.Errorf("compress %s values: %w", d.filenameBase, err) + } + valuesComp.Close() + valuesComp = nil + if valuesDecomp, err = seg.NewDecompressor(collation.valuesPath); err != nil { + return StaticFiles{}, fmt.Errorf("open %s values decompressor: %w", d.filenameBase, err) + } + + if !UseBpsTree { + if err = d.buildAccessor(ctx, stepFrom, stepTo, valuesDecomp, ps); err != nil { + return StaticFiles{}, fmt.Errorf("build %s values idx: %w", d.filenameBase, err) + } + valuesIdx, err = recsplit.OpenIndex(d.efAccessorFilePath(stepFrom, stepTo)) + if err != nil { + return StaticFiles{}, err + } + } + + { + btPath := d.kvBtFilePath(stepFrom, stepTo) + bt, err = CreateBtreeIndexWithDecompressor(btPath, DefaultBtreeM, valuesDecomp, d.compression, *d.salt, ps, d.dirs.Tmp, d.logger, d.noFsync) + if err != nil { + return StaticFiles{}, fmt.Errorf("build %s .bt idx: %w", d.filenameBase, err) + } + } + { + fPath := d.kvExistenceIdxFilePath(stepFrom, stepTo) + exists, err := dir.FileExist(fPath) + if err != nil { + return StaticFiles{}, fmt.Errorf("build %s .kvei: %w", d.filenameBase, err) + } + if exists { + bloom, err = OpenExistenceFilter(fPath) + if err != nil { + return StaticFiles{}, fmt.Errorf("build %s .kvei: %w", d.filenameBase, err) + } + } + } + closeComp = false + return StaticFiles{ + valuesDecomp: valuesDecomp, + valuesIdx: valuesIdx, + valuesBt: bt, + bloom: bloom, + }, nil +} + // buildFiles performs potentially resource intensive operations of creating // static files and their indices func (d *Domain) buildFiles(ctx context.Context, step uint64, collation Collation, ps *background.ProgressSet) (StaticFiles, error) { @@ -1416,18 +1654,24 @@ var ( UseBtree = true // if true, will use btree for all files ) -func (dt *DomainRoTx) getFromFiles(filekey []byte) (v []byte, found bool, fileStartTxNum uint64, fileEndTxNum uint64, err error) { +// getFromFiles doesn't provide same semantics as getLatestFromDB - it returns start/end tx +// of file where the value is stored (not exact step when kv has been set) +// maxTxNum, if > 0, filters out files with bigger txnums from search +func (dt *DomainRoTx) getFromFiles(filekey []byte, maxTxNum uint64) (v []byte, found bool, fileStartTxNum uint64, fileEndTxNum uint64, err error) { if len(dt.files) == 0 { return } + if maxTxNum == 0 { + maxTxNum = math.MaxUint64 + } useExistenceFilter := dt.d.indexList&withExistence != 0 - useCache := dt.name != kv.CommitmentDomain + useCache := dt.name != kv.CommitmentDomain && maxTxNum == math.MaxUint64 hi, _ := dt.ht.iit.hashKey(filekey) if useCache && dt.getFromFileCache == nil { dt.getFromFileCache = dt.visible.newGetFromFileCache() } - if dt.getFromFileCache != nil { + if dt.getFromFileCache != nil && maxTxNum == math.MaxUint64 { cv, ok := dt.getFromFileCache.Get(hi) if ok { if !cv.exists { @@ -1442,6 +1686,10 @@ func (dt *DomainRoTx) getFromFiles(filekey []byte) (v []byte, found bool, fileSt } for i := len(dt.files) - 1; i >= 0; i-- { + if maxTxNum != math.MaxUint64 && dt.files[i].endTxNum > maxTxNum { // skip partially matched files + continue + } + // fmt.Printf("getFromFiles: lim=%d %d %d %d %d\n", maxTxNum, dt.files[i].startTxNum, dt.files[i].endTxNum, dt.files[i].startTxNum/dt.d.aggregationStep, dt.files[i].endTxNum/dt.d.aggregationStep) if useExistenceFilter { if dt.files[i].src.existence != nil { if !dt.files[i].src.existence.ContainsHash(hi) { @@ -1491,6 +1739,26 @@ func (dt *DomainRoTx) getFromFiles(filekey []byte) (v []byte, found bool, fileSt return nil, false, 0, 0, nil } +func (dt *DomainRoTx) GetAsOfFile(key []byte, txNum uint64) ([]byte, bool, error) { + var v []byte + var foundStep uint64 + var found bool + var err error + + if traceGetLatest == dt.name { + defer func() { + fmt.Printf("GetAsOfFile(%s, '%x' -> '%x') (from db=%t; istep=%x stepInFiles=%d)\n", + dt.name.String(), key, v, found, foundStep, dt.files.EndTxNum()/dt.d.aggregationStep) + }() + } + + v, foundInFile, _, _, err := dt.getFromFiles(key, txNum) + if err != nil { + return nil, false, fmt.Errorf("getFromFiles: %w", err) + } + return v, foundInFile, nil +} + // GetAsOf does not always require usage of roTx. If it is possible to determine // historical value based only on static files, roTx will not be used. func (dt *DomainRoTx) GetAsOf(key []byte, txNum uint64, roTx kv.Tx) ([]byte, bool, error) { @@ -1542,6 +1810,16 @@ func (dt *DomainRoTx) Close() { dt.visible.returnGetFromFileCache(dt.getFromFileCache) } +// statelessFileIndex figures out ordinal of file within required range +func (dt *DomainRoTx) statelessFileIndex(txFrom uint64, txTo uint64) int { + for fi, f := range dt.files { + if f.startTxNum == txFrom && f.endTxNum == txTo { + return fi + } + } + return -1 +} + func (dt *DomainRoTx) statelessGetter(i int) *seg.Reader { if dt.getters == nil { dt.getters = make([]*seg.Reader, len(dt.files)) @@ -1662,7 +1940,7 @@ func (dt *DomainRoTx) GetLatest(key1, key2 []byte, roTx kv.Tx) ([]byte, uint64, return v, foundStep, true, nil } - v, foundInFile, _, endTxNum, err := dt.getFromFiles(key) + v, foundInFile, _, endTxNum, err := dt.getFromFiles(key, 0) if err != nil { return nil, 0, false, fmt.Errorf("getFromFiles: %w", err) } @@ -1910,6 +2188,36 @@ func (dt *DomainRoTx) Prune(ctx context.Context, rwTx kv.RwTx, step, txFrom, txT return stat, nil } +type SegStreamReader struct { + s *seg.Reader + + limit int +} + +// SegStreamReader implements stream.KV for segment reader. +// limit -1 means no limit. +func NewSegStreamReader(s *seg.Reader, limit int) *SegStreamReader { + s.Reset(0) + return &SegStreamReader{ + s: s, limit: limit, + } +} + +func (sr *SegStreamReader) HasNext() bool { return sr.s.HasNext() && (sr.limit == -1 || sr.limit > 0) } +func (sr *SegStreamReader) Close() { sr.s = nil } + +func (sr *SegStreamReader) Next() (k, v []byte, err error) { + k, _ = sr.s.Next(k) + if !sr.s.HasNext() { + return nil, nil, fmt.Errorf("key %x has no associated value: %s", k, sr.s.FileName()) + } + v, _ = sr.s.Next(v) + if sr.limit > 0 { + sr.limit-- + } + return k, v, nil +} + type DomainLatestIterFile struct { dc *DomainRoTx diff --git a/erigon-lib/state/domain_committed.go b/erigon-lib/state/domain_committed.go index 3b1d2154faa..76970502cc6 100644 --- a/erigon-lib/state/domain_committed.go +++ b/erigon-lib/state/domain_committed.go @@ -30,6 +30,11 @@ import ( type ValueMerger func(prev, current []byte) (merged []byte, err error) +// TODO revisit encoded commitmentState. +// - Add versioning +// - add trie variant marker +// - simplify decoding. Rn it's 3 embedded structure: RootNode encoded, Trie state encoded and commitmentState wrapper for search. +// | search through states seems mostly useless so probably commitmentState should become header of trie state. type commitmentState struct { txNum uint64 blockNum uint64 @@ -242,7 +247,7 @@ func (dt *DomainRoTx) commitmentValTransformDomain(rng MergeRange, accounts, sto mergedStorage = storage.lookupVisibleFileByItsRange(rng.from, rng.to) if mergedStorage == nil { // TODO may allow to merge, but storage keys will be stored as plainkeys - return nil, fmt.Errorf("merged v1-account.%d-%d.kv file not found", rng.from/dt.d.aggregationStep, rng.to/dt.d.aggregationStep) + return nil, fmt.Errorf("merged v1-storage.%d-%d.kv file not found", rng.from/dt.d.aggregationStep, rng.to/dt.d.aggregationStep) } } hadToLookupAccount := mergedAccount == nil diff --git a/erigon-lib/state/domain_shared.go b/erigon-lib/state/domain_shared.go index de026efc0eb..a32da3a0af5 100644 --- a/erigon-lib/state/domain_shared.go +++ b/erigon-lib/state/domain_shared.go @@ -21,6 +21,7 @@ import ( "container/heap" "context" "encoding/binary" + "encoding/hex" "fmt" "math" "path/filepath" @@ -241,6 +242,78 @@ func (sd *SharedDomains) rebuildCommitment(ctx context.Context, roTx kv.Tx, bloc return sd.ComputeCommitment(ctx, true, blockNum, "rebuild commit") } +// DiscardWrites disables updates collection for further flushing into db. +// Instead, it keeps them temporarily available until .ClearRam/.Close will make them unavailable. +func (sd *SharedDomains) DiscardWrites(d kv.Domain) { + if d >= kv.DomainLen { + return + } + sd.domainWriters[d].discard = true + sd.domainWriters[d].h.discard = true +} + +func (sd *SharedDomains) RebuildCommitmentShard(ctx context.Context, next func() (bool, []byte), cfg *RebuiltCommitment) (*RebuiltCommitment, error) { + sd.DiscardWrites(kv.AccountsDomain) + sd.DiscardWrites(kv.StorageDomain) + sd.DiscardWrites(kv.CodeDomain) + + visComFiles := sd.aggTx.d[kv.CommitmentDomain].Files() + sd.logger.Info("starting commitment", "shard", fmt.Sprintf("%d-%d", cfg.StepFrom, cfg.StepTo), + "totalKeys", common.PrettyCounter(cfg.Keys), "block", sd.BlockNum(), + "commitment files before dump step", cfg.StepTo, + "files", fmt.Sprintf("%d %v", len(visComFiles), visComFiles)) + + sf := time.Now() + var processed uint64 + for ok, key := next(); ; ok, key = next() { + sd.sdCtx.TouchKey(kv.AccountsDomain, string(key), nil) + processed++ + if !ok { + break + } + } + collectionSpent := time.Since(sf) + rh, err := sd.sdCtx.ComputeCommitment(ctx, true, sd.BlockNum(), fmt.Sprintf("%d-%d", cfg.StepFrom, cfg.StepTo)) + if err != nil { + return nil, err + } + sd.logger.Info("sealing", "shard", fmt.Sprintf("%d-%d", cfg.StepFrom, cfg.StepTo), + "root", hex.EncodeToString(rh), "commitment", time.Since(sf).String(), + "collection", collectionSpent.String()) + + sb := time.Now() + + // rng := MergeRange{from: cfg.TxnFrom, to: cfg.TxnTo} + // vt, err := sd.aggTx.d[kv.CommitmentDomain].commitmentValTransformDomain(rng, sd.aggTx.d[kv.AccountsDomain], sd.aggTx.d[kv.StorageDomain], nil, nil) + // if err != nil { + // return nil, err + // } + err = sd.aggTx.d[kv.CommitmentDomain].d.DumpStepRangeOnDisk(ctx, cfg.StepFrom, cfg.StepTo, cfg.TxnFrom, cfg.TxnTo, sd.domainWriters[kv.CommitmentDomain], nil) + if err != nil { + return nil, err + } + + sd.logger.Info("shard built", "shard", fmt.Sprintf("%d-%d", cfg.StepFrom, cfg.StepTo), "root", hex.EncodeToString(rh), "ETA", time.Since(sf).String(), "file dump", time.Since(sb).String()) + + return &RebuiltCommitment{ + RootHash: rh, + StepFrom: cfg.StepFrom, + StepTo: cfg.StepTo, + TxnFrom: cfg.TxnFrom, + TxnTo: cfg.TxnTo, + Keys: processed, + }, nil +} + +type RebuiltCommitment struct { + RootHash []byte + StepFrom uint64 + StepTo uint64 + TxnFrom uint64 + TxnTo uint64 + Keys uint64 +} + // SeekCommitment lookups latest available commitment and sets it as current func (sd *SharedDomains) SeekCommitment(ctx context.Context, tx kv.Tx) (txsFromBlockBeginning uint64, err error) { bn, txn, ok, err := sd.sdCtx.SeekCommitment(tx, sd.aggTx.d[kv.CommitmentDomain], 0, math.MaxUint64) @@ -373,9 +446,9 @@ func (sd *SharedDomains) LatestCommitment(prefix []byte) ([]byte, uint64, error) return v, step, nil } - // GetfromFiles doesn't provide same semantics as getLatestFromDB - it returns start/end tx + // getFromFiles doesn't provide same semantics as getLatestFromDB - it returns start/end tx // of file where the value is stored (not exact step when kv has been set) - v, _, startTx, endTx, err := sd.aggTx.d[kv.CommitmentDomain].getFromFiles(prefix) + v, _, startTx, endTx, err := sd.aggTx.d[kv.CommitmentDomain].getFromFiles(prefix, 0) if err != nil { return nil, 0, fmt.Errorf("commitment prefix %x read error: %w", prefix, err) } @@ -428,6 +501,16 @@ func (sd *SharedDomains) replaceShortenedKeysInBranch(prefix []byte, branch comm } storageGetter := seg.NewReader(storageItem.decompressor.MakeGetter(), sto.d.compression) accountGetter := seg.NewReader(accountItem.decompressor.MakeGetter(), acc.d.compression) + metricI := 0 + for i, f := range sd.aggTx.d[kv.CommitmentDomain].files { + if i > 5 { + metricI = 5 + break + } + if f.startTxNum == fStartTxNum && f.endTxNum == fEndTxNum { + metricI = i + } + } aux := make([]byte, 0, 256) return branch.ReplacePlainKeys(aux, func(key []byte, isStorage bool) ([]byte, error) { @@ -435,6 +518,9 @@ func (sd *SharedDomains) replaceShortenedKeysInBranch(prefix []byte, branch comm if len(key) == length.Addr+length.Hash { return nil, nil // save storage key as is } + if dbg.KVReadLevelledMetrics { + defer branchKeyDerefSpent[metricI].ObserveDuration(time.Now()) + } // Optimised key referencing a state file record (file number and offset within the file) storagePlainKey, found := sto.lookupByShortenedKey(key, storageGetter) if !found { @@ -450,6 +536,9 @@ func (sd *SharedDomains) replaceShortenedKeysInBranch(prefix []byte, branch comm return nil, nil // save account key as is } + if dbg.KVReadLevelledMetrics { + defer branchKeyDerefSpent[metricI].ObserveDuration(time.Now()) + } apkBuf, found := acc.lookupByShortenedKey(key, accountGetter) if !found { s0, s1 := fStartTxNum/sd.aggTx.a.StepSize(), fEndTxNum/sd.aggTx.a.StepSize() @@ -883,6 +972,25 @@ func (sd *SharedDomains) DomainGet(domain kv.Domain, k, k2 []byte) (v []byte, st return v, step, nil } +// DomainGetAsOfFile returns value from domain with respect to limit ofMaxTxnum +func (sd *SharedDomains) domainGetAsOfFile(domain kv.Domain, k, k2 []byte, ofMaxTxnum uint64) (v []byte, step uint64, err error) { + if domain == kv.CommitmentDomain { + return sd.LatestCommitment(k) + } + if k2 != nil { + k = append(k, k2...) + } + + v, ok, err := sd.aggTx.DomainGetAsOfFile(domain, k, ofMaxTxnum) + if err != nil { + return nil, 0, fmt.Errorf("domain '%s' %x txn=%d read error: %w", domain, k, ofMaxTxnum, err) + } + if !ok { + return nil, 0, nil + } + return v, step, nil +} + // DomainPut // Optimizations: // - user can provide `prevVal != nil` - then it will not read prev value from storage @@ -1000,6 +1108,12 @@ type SharedDomainsCommitmentContext struct { updates *commitment.Updates patriciaTrie commitment.Trie justRestored atomic.Bool + + limitReadAsOfTxNum uint64 +} + +func (sdc *SharedDomainsCommitmentContext) SetLimitReadAsOfTxNum(txNum uint64) { + sdc.limitReadAsOfTxNum = txNum } func NewSharedDomainsCommitmentContext(sd *SharedDomains, mode commitment.Mode, trieVariant commitment.TrieVariant) *SharedDomainsCommitmentContext { @@ -1063,12 +1177,21 @@ func (sdc *SharedDomainsCommitmentContext) PutBranch(prefix []byte, data []byte, return sdc.sharedDomains.updateCommitmentData(prefix, data, prevData, prevStep) } -func (sdc *SharedDomainsCommitmentContext) Account(plainKey []byte) (*commitment.Update, error) { - encAccount, _, err := sdc.sharedDomains.DomainGet(kv.AccountsDomain, plainKey, nil) - if err != nil { - return nil, fmt.Errorf("GetAccount failed: %w", err) +func (sdc *SharedDomainsCommitmentContext) Account(plainKey []byte) (u *commitment.Update, err error) { + var encAccount []byte + if sdc.limitReadAsOfTxNum == 0 { + encAccount, _, err = sdc.sharedDomains.DomainGet(kv.AccountsDomain, plainKey, nil) + if err != nil { + return nil, fmt.Errorf("GetAccount failed: %w", err) + } + } else { + encAccount, _, err = sdc.sharedDomains.domainGetAsOfFile(kv.AccountsDomain, plainKey, nil, sdc.limitReadAsOfTxNum) + if err != nil { + return nil, fmt.Errorf("GetAccount failed: %w", err) + } } - u := new(commitment.Update) + + u = new(commitment.Update) u.Reset() if len(encAccount) > 0 { @@ -1089,10 +1212,16 @@ func (sdc *SharedDomainsCommitmentContext) Account(plainKey []byte) (*commitment return u, nil } - code, _, err := sdc.sharedDomains.DomainGet(kv.CodeDomain, plainKey, nil) + var code []byte + if sdc.limitReadAsOfTxNum == 0 { + code, _, err = sdc.sharedDomains.DomainGet(kv.CodeDomain, plainKey, nil) + } else { + code, _, err = sdc.sharedDomains.domainGetAsOfFile(kv.CodeDomain, plainKey, nil, sdc.limitReadAsOfTxNum) + } if err != nil { return nil, fmt.Errorf("GetAccount/Code: failed to read latest code: %w", err) } + if len(code) > 0 { sdc.keccak.Reset() sdc.keccak.Write(code) @@ -1109,13 +1238,18 @@ func (sdc *SharedDomainsCommitmentContext) Account(plainKey []byte) (*commitment return u, nil } -func (sdc *SharedDomainsCommitmentContext) Storage(plainKey []byte) (*commitment.Update, error) { +func (sdc *SharedDomainsCommitmentContext) Storage(plainKey []byte) (u *commitment.Update, err error) { // Look in the summary table first - enc, _, err := sdc.sharedDomains.DomainGet(kv.StorageDomain, plainKey, nil) + var enc []byte + if sdc.limitReadAsOfTxNum == 0 { + enc, _, err = sdc.sharedDomains.DomainGet(kv.StorageDomain, plainKey, nil) + } else { + enc, _, err = sdc.sharedDomains.domainGetAsOfFile(kv.StorageDomain, plainKey, nil, sdc.limitReadAsOfTxNum) + } if err != nil { return nil, err } - u := new(commitment.Update) + u = new(commitment.Update) u.StorageLen = len(enc) if len(enc) == 0 { u.Flags = commitment.DeleteUpdate diff --git a/erigon-lib/state/domain_test.go b/erigon-lib/state/domain_test.go index bc8c8956bde..f8f0b8d0ee3 100644 --- a/erigon-lib/state/domain_test.go +++ b/erigon-lib/state/domain_test.go @@ -652,6 +652,39 @@ func TestDomain_Delete(t *testing.T) { } } +func TestNewSegStreamReader(t *testing.T) { + logger := log.New() + keyCount := 1000 + valSize := 4 + + fpath := generateKV(t, t.TempDir(), length.Addr, valSize, keyCount, logger, seg.CompressNone) + dec, err := seg.NewDecompressor(fpath) + require.NoError(t, err) + + defer dec.Close() + r := seg.NewReader(dec.MakeGetter(), seg.CompressNone) + + sr := NewSegStreamReader(r, -1) + require.NotNil(t, sr) + defer sr.Close() + + count := 0 + var prevK []byte + for sr.HasNext() { + k, v, err := sr.Next() + if prevK != nil { + require.True(t, bytes.Compare(prevK, k) < 0) + } + prevK = common.Copy(k) + + require.NoError(t, err) + require.NotEmpty(t, v) + + count++ + } + require.EqualValues(t, keyCount, count) +} + // firstly we write all the data to domain // then we collate-merge-prune // then check. @@ -2279,7 +2312,7 @@ func TestDomainContext_findShortenedKey(t *testing.T) { var ki int for key, updates := range data { - v, found, st, en, err := dc.getFromFiles([]byte(key)) + v, found, st, en, err := dc.getFromFiles([]byte(key), 0) require.True(t, found) require.NoError(t, err) for i := len(updates) - 1; i >= 0; i-- { diff --git a/erigon-lib/state/files_item.go b/erigon-lib/state/files_item.go index 92ad3220890..fd37ee035a4 100644 --- a/erigon-lib/state/files_item.go +++ b/erigon-lib/state/files_item.go @@ -202,14 +202,15 @@ func (i *visibleFile) isSubsetOf(j *visibleFile) bool { return i.src.isSubsetOf( func calcVisibleFiles(files *btree2.BTreeG[*filesItem], l idxList, trace bool, toTxNum uint64) (roItems []visibleFile) { newVisibleFiles := make([]visibleFile, 0, files.Len()) + // trace = true if trace { - log.Warn("[dbg] calcVisibleFiles", "amount", files.Len()) + log.Warn("[dbg] calcVisibleFiles", "amount", files.Len(), "toTxNum", toTxNum) } files.Walk(func(items []*filesItem) bool { for _, item := range items { if item.endTxNum > toTxNum { if trace { - log.Warn("[dbg] calcVisibleFiles: more than", "f", item.decompressor.FileName()) + log.Warn("[dbg] calcVisibleFiles: ends after limit", "f", item.decompressor.FileName(), "limitTxNum", toTxNum) } continue } @@ -259,6 +260,8 @@ func calcVisibleFiles(files *btree2.BTreeG[*filesItem], l idxList, trace bool, t newVisibleFiles[len(newVisibleFiles)-1].src = nil newVisibleFiles = newVisibleFiles[:len(newVisibleFiles)-1] } + + // log.Warn("willBeVisible", "newVisibleFile", item.decompressor.FileName()) newVisibleFiles = append(newVisibleFiles, visibleFile{ startTxNum: item.startTxNum, endTxNum: item.endTxNum, @@ -297,3 +300,14 @@ func (files visibleFiles) LatestMergedRange() MergeRange { } return MergeRange{} } + +func (files visibleFiles) MergedRanges() []MergeRange { + if len(files) == 0 { + return nil + } + res := make([]MergeRange, len(files)) + for i := len(files) - 1; i >= 0; i-- { + res[i] = MergeRange{from: files[i].startTxNum, to: files[i].endTxNum} + } + return res +} diff --git a/erigon-lib/state/history.go b/erigon-lib/state/history.go index 069197a7d52..c2b0ffd5d6e 100644 --- a/erigon-lib/state/history.go +++ b/erigon-lib/state/history.go @@ -1184,6 +1184,7 @@ func (ht *HistoryRoTx) historySeekInFiles(key []byte, txNum uint64) ([]byte, boo } historyItem, ok := ht.getFile(histTxNum) if !ok { + log.Warn("historySeekInFiles: file not found", "key", key, "txNum", txNum, "histTxNum", histTxNum, "ssize", ht.h.aggregationStep) return nil, false, fmt.Errorf("hist file not found: key=%x, %s.%d-%d", key, ht.h.filenameBase, histTxNum/ht.h.aggregationStep, histTxNum/ht.h.aggregationStep) } reader := ht.statelessIdxReader(historyItem.i) diff --git a/erigon-lib/state/inverted_index.go b/erigon-lib/state/inverted_index.go index 2f3e444ba13..9aba57d41e0 100644 --- a/erigon-lib/state/inverted_index.go +++ b/erigon-lib/state/inverted_index.go @@ -528,6 +528,10 @@ type MergeRange struct { to uint64 } +func (mr *MergeRange) FromTo() (uint64, uint64) { + return mr.from, mr.to +} + func (mr *MergeRange) String(prefix string, aggStep uint64) string { if prefix != "" { prefix += "=" diff --git a/erigon-lib/state/metrics.go b/erigon-lib/state/metrics.go index 918ae91f630..0e350cdbe9c 100644 --- a/erigon-lib/state/metrics.go +++ b/erigon-lib/state/metrics.go @@ -61,6 +61,17 @@ var ( mxCommitmentTook = metrics.GetOrCreateSummary("domain_commitment_took") ) +var ( + branchKeyDerefSpent = []metrics.Summary{ + metrics.GetOrCreateSummary(`branch_key_deref{level="L0"}`), + metrics.GetOrCreateSummary(`branch_key_deref{level="L1"}`), + metrics.GetOrCreateSummary(`branch_key_deref{level="L2"}`), + metrics.GetOrCreateSummary(`branch_key_deref{level="L3"}`), + metrics.GetOrCreateSummary(`branch_key_deref{level="L4"}`), + metrics.GetOrCreateSummary(`branch_key_deref{level="recent"}`), + } +) + var ( mxsKVGet = [kv.DomainLen][]metrics.Summary{ kv.AccountsDomain: { diff --git a/erigon-lib/state/sqeeze.go b/erigon-lib/state/sqeeze.go index a3067277c63..591d4ce6c13 100644 --- a/erigon-lib/state/sqeeze.go +++ b/erigon-lib/state/sqeeze.go @@ -3,17 +3,22 @@ package state import ( "bytes" "context" + "encoding/hex" "errors" "fmt" + "math" "os" "path/filepath" "strings" "time" "github.com/c2h5oh/datasize" + "github.com/erigontech/erigon-lib/common" "github.com/erigontech/erigon-lib/common/datadir" "github.com/erigontech/erigon-lib/common/dir" "github.com/erigontech/erigon-lib/kv" + "github.com/erigontech/erigon-lib/kv/rawdbv3" + "github.com/erigontech/erigon-lib/kv/stream" "github.com/erigontech/erigon-lib/log/v3" "github.com/erigontech/erigon-lib/seg" ) @@ -139,7 +144,7 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro } }() - log.Info("[sqeeze_migration] see target files", "acc", len(mergedAccountFiles), "st", len(mergedStorageFiles), "com", len(mergedCommitFiles)) + log.Info("[squeeze_migration] see target files", "acc", len(mergedAccountFiles), "st", len(mergedStorageFiles), "com", len(mergedCommitFiles)) getSizeDelta := func(a, b string) (datasize.ByteSize, float32, error) { ai, err := os.Stat(a) @@ -154,7 +159,6 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro } var ( - obsoleteFiles []string temporalFiles []string processedFiles int ai, si int @@ -177,7 +181,7 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro } } if ai == len(mergedAccountFiles) || si == len(mergedStorageFiles) { - ac.a.logger.Info("[sqeeze_migration] commitment file has no corresponding account or storage file", "commitment", cf.decompressor.FileName()) + ac.a.logger.Info("[squeeze_migration] commitment file has no corresponding account or storage file", "commitment", cf.decompressor.FileName()) continue } @@ -189,7 +193,7 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro if steps < DomainMinStepsToCompress { compression = seg.CompressNone } - ac.a.logger.Info("[sqeeze_migration] file start", "original", cf.decompressor.FileName(), + ac.a.logger.Info("[squeeze_migration] file start", "original", cf.decompressor.FileName(), "progress", fmt.Sprintf("%d/%d", ci+1, len(mergedAccountFiles)), "compress_cfg", commitment.d.compressCfg, "compress", compression) originalPath := cf.decompressor.FilePath() @@ -239,7 +243,7 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro select { case <-logEvery.C: - ac.a.logger.Info("[sqeeze_migration]", "file", cf.decompressor.FileName(), "k", fmt.Sprintf("%x", k), + ac.a.logger.Info("[squeeze_migration]", "file", cf.decompressor.FileName(), "k", fmt.Sprintf("%x", k), "progress", fmt.Sprintf("%d/%d", i, cf.decompressor.Count())) default: } @@ -250,30 +254,30 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro } writer.Close() - squeezedPath := originalPath + sqExt - if err = os.Rename(squeezedTmpPath, squeezedPath); err != nil { + delta, deltaP, err := getSizeDelta(originalPath, squeezedTmpPath) + if err != nil { return err } - temporalFiles = append(temporalFiles, squeezedPath) + sizeDelta += delta + cf.closeFilesAndRemove() - delta, deltaP, err := getSizeDelta(originalPath, squeezedPath) - if err != nil { + squeezedPath := originalPath + sqExt + if err = os.Rename(squeezedTmpPath, squeezedPath); err != nil { return err } - sizeDelta += delta + temporalFiles = append(temporalFiles, squeezedPath) ac.a.logger.Info("[sqeeze_migration] file done", "original", filepath.Base(originalPath), "sizeDelta", fmt.Sprintf("%s (%.1f%%)", delta.HR(), deltaP)) - fromStep, toStep := af.startTxNum/ac.a.StepSize(), af.endTxNum/ac.a.StepSize() - - // need to remove all indexes for commitment file as well - obsoleteFiles = append(obsoleteFiles, - originalPath, - commitment.d.kvBtFilePath(fromStep, toStep), - commitment.d.kvAccessorFilePath(fromStep, toStep), - commitment.d.kvExistenceIdxFilePath(fromStep, toStep), - ) + //fromStep, toStep := af.startTxNum/ac.a.StepSize(), af.endTxNum/ac.a.StepSize() + //// need to remove all indexes for commitment file as well + //obsoleteFiles = append(obsoleteFiles, + // originalPath, + // commitment.d.kvBtFilePath(fromStep, toStep), + // commitment.d.kvAccessorFilePath(fromStep, toStep), + // commitment.d.kvExistenceIdxFilePath(fromStep, toStep), + //) processedFiles++ return nil }() @@ -282,27 +286,283 @@ func (ac *AggregatorRoTx) SqueezeCommitmentFiles(mergedAgg *AggregatorRoTx) erro } } - ac.a.logger.Info("[sqeeze_migration] squeezed files has been produced, removing obsolete files", - "toRemove", len(obsoleteFiles), "processed", fmt.Sprintf("%d/%d", processedFiles, len(mergedCommitFiles))) - for _, path := range obsoleteFiles { - if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - ac.a.logger.Debug("[sqeeze_migration] obsolete file removal", "path", path) - } - ac.a.logger.Info("[sqeeze_migration] indices removed, renaming temporal files ") - for _, path := range temporalFiles { if err := os.Rename(path, strings.TrimSuffix(path, sqExt)); err != nil { return err } - ac.a.logger.Debug("[sqeeze_migration] temporal file renaming", "path", path) + ac.a.logger.Debug("[squeeze_migration] temporal file renaming", "path", path) } - ac.a.logger.Info("[sqeeze_migration] done", "sizeDelta", sizeDelta.HR(), "files", len(mergedAccountFiles)) + ac.a.logger.Info("[squeeze_migration] done", "sizeDelta", sizeDelta.HR(), "files", len(mergedAccountFiles)) return nil } +// wraps tx and aggregator for tests. (when temporal db is unavailable) +type wrappedTxWithCtx struct { + kv.Tx + ac *AggregatorRoTx +} + +func (w *wrappedTxWithCtx) AggTx() any { return w.ac } + +func wrapTxWithCtxForTest(tx kv.Tx, ctx *AggregatorRoTx) *wrappedTxWithCtx { + return &wrappedTxWithCtx{Tx: tx, ac: ctx} +} + +// RebuildCommitmentFiles recreates commitment files from existing accounts and storage kv files +// If some commitment exists, they will be accepted as correct and next kv range will be processed. +// DB expected to be empty, committed into db keys will be not processed. +func (a *Aggregator) RebuildCommitmentFiles(ctx context.Context, rwDb kv.RwDB, txNumsReader *rawdbv3.TxNumsReader) (latestRoot []byte, err error) { + acRo := a.BeginFilesRo() // this tx is used to read existing domain files and closed in the end + defer acRo.Close() + + rng := RangesV3{ + domain: [5]DomainRanges{ + kv.AccountsDomain: { + name: kv.AccountsDomain, + values: MergeRange{true, 0, math.MaxUint64}, + history: HistoryRanges{}, + aggStep: a.StepSize(), + }, + }, + invertedIndex: [4]*MergeRange{}, + } + sf, err := acRo.staticFilesInRange(rng) + if err != nil { + return nil, err + } + ranges := make([]MergeRange, 0) + for fi, f := range sf.d[kv.AccountsDomain] { + fmt.Printf("shard %d - %d-%d %s\n", fi, f.startTxNum, f.endTxNum, f.decompressor.FileName()) + ranges = append(ranges, MergeRange{ + from: f.startTxNum, + to: f.endTxNum, + }) + } + if len(ranges) == 0 { + return nil, errors.New("no account files found") + } + + for _, d := range acRo.d { + for _, f := range d.files { + f.src.decompressor.EnableMadvNormal() + } + } + + start := time.Now() + defer func() { + for _, d := range acRo.d { + for _, f := range d.files { + f.src.decompressor.DisableReadAhead() + } + } + a.logger.Info("Commitment DONE", "duration", time.Since(start)) + }() + + acRo.RestrictSubsetFileDeletions(true) + a.commitmentValuesTransform = false + + var totalKeysCommitted uint64 + + for i, r := range ranges { + a.logger.Info("scanning keys", "range", r.String("", a.StepSize()), "shards", fmt.Sprintf("%d/%d", i+1, len(ranges))) // + + fromTxNumRange, toTxNumRange := r.FromTo() + lastTxnumInShard := toTxNumRange + if acRo.minimaxTxNumInDomainFiles() >= toTxNumRange { + a.logger.Info("skipping existing range", "range", r.String("", a.StepSize())) + continue + } + + roTx, err := a.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer roTx.Rollback() + + _, blockNum, err := txNumsReader.FindBlockNum(roTx, toTxNumRange-1) + if err != nil { + a.logger.Warn("failed to find block number for txNum", "txNum", toTxNumRange, "err", err) + return nil, err + } + + txnRangeTo := int(toTxNumRange) + txnRangeFrom := int(fromTxNumRange) + + accReader, err := acRo.nastyFileRead(kv.AccountsDomain, fromTxNumRange, toTxNumRange) + if err != nil { + return nil, err + } + stoReader, err := acRo.nastyFileRead(kv.StorageDomain, fromTxNumRange, toTxNumRange) + if err != nil { + return nil, err + } + + streamAcc := NewSegStreamReader(accReader, -1) + streamSto := NewSegStreamReader(stoReader, -1) + keyIter := stream.UnionKV(streamAcc, streamSto, -1) + + totalKeys := acRo.KeyCountInDomainRange(kv.AccountsDomain, uint64(txnRangeFrom), uint64(txnRangeTo)) + + acRo.KeyCountInDomainRange(kv.StorageDomain, uint64(txnRangeFrom), uint64(txnRangeTo)) + + shardFrom, shardTo := fromTxNumRange/a.StepSize(), toTxNumRange/a.StepSize() + batchSize := totalKeys / (shardTo - shardFrom) + lastShard := shardTo + + shardSize := min(uint64(math.Pow(2, math.Log2(float64(totalKeys/batchSize)))), 128) + shardTo = shardFrom + shardSize + toTxNumRange = shardTo * a.StepSize() + + a.logger.Info("beginning commitment", "range", r.String("", a.StepSize()), "shardSize", shardSize, "batch", batchSize) + + var rebuiltCommit *RebuiltCommitment + var processed uint64 + + for shardFrom < lastShard { + nextKey := func() (ok bool, k []byte) { + if !keyIter.HasNext() { + return false, nil + } + if processed%1000 == 0 { + fmt.Printf("processed %12d/%d (%2.f%%) %x\r", processed, totalKeys, float64(processed)/float64(totalKeys)*100, k) + } + k, _, err := keyIter.Next() + if err != nil { + a.logger.Warn("nextKey", "err", err) + return false, nil + } + processed++ + if processed%(batchSize*shardSize) == 0 && shardTo != lastShard { + fmt.Println() + return false, k + } + return true, k + } + + var rwTx kv.RwTx + var domains *SharedDomains + var ac *AggregatorRoTx + + if rwDb != nil { + // regular case + rwTx, err = rwDb.BeginRw(ctx) + if err != nil { + return nil, err + } + defer rwTx.Rollback() + + domains, err = NewSharedDomains(rwTx, log.New()) + if err != nil { + return nil, err + } + + var ok bool + ac, ok = domains.AggTx().(*AggregatorRoTx) + if !ok { + return nil, errors.New("failed to get state aggregatorTx") + } + + } else { + // case when we do testing and temporal db with aggtx is not available + ac = a.BeginFilesRo() + + domains, err = NewSharedDomains(wrapTxWithCtxForTest(roTx, ac), log.New()) + if err != nil { + ac.Close() + return nil, err + } + } + defer ac.Close() + + ////domains, err := NewSharedDomains(wrapTxWithCtx(roTx, ac), log.New()) + //if err != nil { + // return nil, err + //} + + //ac, ok := domains.AggTx().(*AggregatorRoTx) + //if !ok { + // return nil, errors.New("failed to get state aggregatorTx") + //} + //defer ac.Close() + + domains.SetBlockNum(blockNum) + domains.SetTxNum(lastTxnumInShard - 1) + domains.sdCtx.SetLimitReadAsOfTxNum(domains.TxNum() + 1) // this helps to read state from correct file during commitment + + rebuiltCommit, err = domains.RebuildCommitmentShard(ctx, nextKey, &RebuiltCommitment{ + StepFrom: shardFrom, + StepTo: shardTo, + TxnFrom: fromTxNumRange, + TxnTo: toTxNumRange, + Keys: totalKeys, + }) + if err != nil { + return nil, err + } + a.logger.Info(fmt.Sprintf("shard %d-%d of range %s finished (%d%%)", shardFrom, shardTo, r.String("", a.StepSize()), processed*100/totalKeys), + "keys", fmt.Sprintf("%s/%s", common.PrettyCounter(processed), common.PrettyCounter(totalKeys))) + + ac.Close() + domains.Close() + + a.recalcVisibleFiles(a.dirtyFilesEndTxNumMinimax()) + if rwTx != nil { + rwTx.Rollback() + rwTx = nil + } + + if shardTo+shardSize > lastShard && shardSize > 1 { + shardSize /= 2 + } + shardFrom = shardTo + shardTo += shardSize + fromTxNumRange = toTxNumRange + toTxNumRange += shardSize * a.StepSize() + } + + roTx.Rollback() + totalKeysCommitted += processed + + rhx := "" + if rebuiltCommit != nil { + rhx = hex.EncodeToString(rebuiltCommit.RootHash) + latestRoot = rebuiltCommit.RootHash + } + a.logger.Info("finished commitment range", "stateRoot", rhx, "range", r.String("", a.StepSize()), + "block", blockNum, "totalKeysProcessed", common.PrettyCounter(totalKeysCommitted)) + + for { + smthDone, err := a.mergeLoopStep(ctx, toTxNumRange) + if err != nil { + return nil, err + } + if !smthDone { + break + } + } + + keyIter.Close() + } + a.logger.Info("Commitment rebuild", "duration", time.Since(start), "totalKeysProcessed", common.PrettyCounter(totalKeysCommitted)) + + a.logger.Info("Squeezing commitment files") + a.commitmentValuesTransform = true + + acRo.Close() + + a.recalcVisibleFiles(a.dirtyFilesEndTxNumMinimax()) + + fmt.Printf("latest root %x\n", latestRoot) + actx := a.BeginFilesRo() + defer actx.Close() + if err = actx.SqueezeCommitmentFiles(actx); err != nil { + a.logger.Warn("squeezeCommitmentFiles failed", "err", err) + fmt.Printf("rebuilt commitment files still available. Instead of re-run, you have to run 'erigon snapshots sqeeze' to finish squeezing") + return nil, err + } + return latestRoot, nil +} + func domainFiles(dirs datadir.Dirs, domain kv.Domain) []string { files, err := dir.ListFiles(dirs.SnapDomain, ".kv") if err != nil { diff --git a/eth/stagedsync/exec3.go b/eth/stagedsync/exec3.go index 57a56b38c3f..6ffde2a1cbb 100644 --- a/eth/stagedsync/exec3.go +++ b/eth/stagedsync/exec3.go @@ -973,6 +973,8 @@ Loop: doms.SetChangesetAccumulator(nil) } + mxExecBlocks.Add(1) + if offsetFromBlockBeginning > 0 { // after history execution no offset will be required offsetFromBlockBeginning = 0 diff --git a/eth/stagedsync/stage_commit_rebuild.go b/eth/stagedsync/stage_commit_rebuild.go new file mode 100644 index 00000000000..82ce0303181 --- /dev/null +++ b/eth/stagedsync/stage_commit_rebuild.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package stagedsync + +import ( + "context" + "errors" + "github.com/erigontech/erigon/turbo/snapshotsync/freezeblocks" + "github.com/erigontech/erigon/turbo/stages/headerdownload" + + "github.com/erigontech/erigon-lib/kv/rawdbv3" + "github.com/erigontech/erigon/turbo/services" + + libcommon "github.com/erigontech/erigon-lib/common" + "github.com/erigontech/erigon-lib/kv" + "github.com/erigontech/erigon-lib/state" + "github.com/erigontech/erigon/turbo/trie" +) + +type TrieCfg struct { + db kv.RwDB + checkRoot bool + badBlockHalt bool + tmpDir string + saveNewHashesToDB bool // no reason to save changes when calculating root for mining + blockReader services.FullBlockReader + hd *headerdownload.HeaderDownload + + historyV3 bool + agg *state.Aggregator +} + +func StageTrieCfg(db kv.RwDB, checkRoot, saveNewHashesToDB, badBlockHalt bool, tmpDir string, blockReader services.FullBlockReader, hd *headerdownload.HeaderDownload, historyV3 bool, agg *state.Aggregator) TrieCfg { + return TrieCfg{ + db: db, + checkRoot: checkRoot, + tmpDir: tmpDir, + saveNewHashesToDB: saveNewHashesToDB, + badBlockHalt: badBlockHalt, + blockReader: blockReader, + hd: hd, + + historyV3: historyV3, + agg: agg, + } +} + +var ErrInvalidStateRootHash = errors.New("invalid state root hash") + +func RebuildPatriciaTrieBasedOnFiles(ctx context.Context, cfg TrieCfg) (libcommon.Hash, error) { + txNumsReader := rawdbv3.TxNums.WithCustomReadTxNumFunc(freezeblocks.ReadTxNumFuncFromBlockReader(ctx, cfg.blockReader)) + rh, err := cfg.agg.RebuildCommitmentFiles(ctx, cfg.db, &txNumsReader) + if err != nil { + return trie.EmptyRoot, err + } + return libcommon.BytesToHash(rh), err +} diff --git a/eth/stagedsync/stage_trie3.go b/eth/stagedsync/stage_trie3.go deleted file mode 100644 index 9c78a4ed9f8..00000000000 --- a/eth/stagedsync/stage_trie3.go +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2024 The Erigon Authors -// This file is part of Erigon. -// -// Erigon is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Erigon is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with Erigon. If not, see . - -package stagedsync - -import ( - "bytes" - "context" - "encoding/hex" - "errors" - "fmt" - "sync/atomic" - - "github.com/erigontech/erigon-lib/common/datadir" - "github.com/erigontech/erigon-lib/kv/temporal" - "github.com/erigontech/erigon-lib/log/v3" - "github.com/erigontech/erigon/turbo/snapshotsync/freezeblocks" - "github.com/erigontech/erigon/turbo/stages/headerdownload" - - "github.com/erigontech/erigon-lib/commitment" - "github.com/erigontech/erigon-lib/kv/rawdbv3" - "github.com/erigontech/erigon/common/math" - "github.com/erigontech/erigon/turbo/services" - - libcommon "github.com/erigontech/erigon-lib/common" - "github.com/erigontech/erigon-lib/etl" - "github.com/erigontech/erigon-lib/kv" - "github.com/erigontech/erigon-lib/state" - "github.com/erigontech/erigon/core/types" - "github.com/erigontech/erigon/turbo/trie" -) - -func collectAndComputeCommitment(ctx context.Context, tx kv.RwTx, tmpDir string, toTxNum uint64) ([]byte, error) { - domains, err := state.NewSharedDomains(tx, log.New()) - if err != nil { - return nil, err - } - defer domains.Close() - ac := domains.AggTx().(*state.AggregatorRoTx) - - // has to set this value because it will be used during domain.Commit() call. - // If we do not, txNum of block beginning will be used, which will cause invalid txNum on restart following commitment rebuilding - domains.SetTxNum(toTxNum) - - logger := log.New("stage", "patricia_trie", "block", domains.BlockNum()) - logger.Info("Collecting account/storage keys") - collector := etl.NewCollector("collect_keys", tmpDir, etl.NewSortableBuffer(etl.BufferOptimalSize/2), logger) - defer collector.Close() - - var totalKeys atomic.Uint64 - it, err := ac.DomainRangeLatest(tx, kv.AccountsDomain, nil, nil, -1) - if err != nil { - return nil, err - } - for it.HasNext() { - k, _, err := it.Next() - if err != nil { - return nil, err - } - if err := collector.Collect(k, nil); err != nil { - return nil, err - } - totalKeys.Add(1) - } - - it, err = ac.DomainRangeLatest(tx, kv.CodeDomain, nil, nil, -1) - if err != nil { - return nil, err - } - for it.HasNext() { - k, _, err := it.Next() - if err != nil { - return nil, err - } - if err := collector.Collect(k, nil); err != nil { - return nil, err - } - totalKeys.Add(1) - } - - it, err = ac.DomainRangeLatest(tx, kv.StorageDomain, nil, nil, -1) - if err != nil { - return nil, err - } - for it.HasNext() { - k, _, err := it.Next() - if err != nil { - return nil, err - } - if err := collector.Collect(k, nil); err != nil { - return nil, err - } - totalKeys.Add(1) - } - - var ( - batchSize = uint64(10_000_000) - processed atomic.Uint64 - ) - - sdCtx := state.NewSharedDomainsCommitmentContext(domains, commitment.ModeDirect, commitment.VariantHexPatriciaTrie) - - loadKeys := func(k, v []byte, table etl.CurrentTableReader, next etl.LoadNextFunc) error { - if sdCtx.KeysCount() >= batchSize { - rh, err := sdCtx.ComputeCommitment(ctx, true, domains.BlockNum(), "") - if err != nil { - return err - } - logger.Info("Committing batch", - "processed", fmt.Sprintf("%s/%s (%.2f%%)", libcommon.PrettyCounter(processed.Load()), libcommon.PrettyCounter(totalKeys.Load()), - float64(processed.Load())/float64(totalKeys.Load())*100), - "intermediate root", hex.EncodeToString(rh)) - } - processed.Add(1) - sdCtx.TouchKey(kv.AccountsDomain, string(k), nil) - - return nil - } - err = collector.Load(nil, "", loadKeys, etl.TransformArgs{Quit: ctx.Done()}) - if err != nil { - return nil, err - } - collector.Close() - - rh, err := sdCtx.ComputeCommitment(ctx, true, domains.BlockNum(), "") - if err != nil { - return nil, err - } - logger.Info("Commitment has been reevaluated", - "tx", domains.TxNum(), - "root", hex.EncodeToString(rh), - "processed", processed.Load(), - "total", totalKeys.Load()) - - if err := domains.Flush(ctx, tx); err != nil { - return nil, err - } - - return rh, nil -} - -type blockBorders struct { - Number uint64 - FirstTx uint64 - CurrentTx uint64 - LastTx uint64 -} - -func (b blockBorders) Offset() uint64 { - if b.CurrentTx > b.FirstTx && b.CurrentTx < b.LastTx { - return b.CurrentTx - b.FirstTx - } - return 0 -} - -func countBlockByTxnum(ctx context.Context, tx kv.Tx, blockReader services.FullBlockReader, txNum uint64) (bb blockBorders, err error) { - var txCounter uint64 = 0 - - for i := uint64(0); i < math.MaxUint64; i++ { - if i%1000000 == 0 { - fmt.Printf("\r [%s] Counting block for txn %d: cur block %s cur txn %d\n", "restoreCommit", txNum, libcommon.PrettyCounter(i), txCounter) - } - - h, err := blockReader.HeaderByNumber(ctx, tx, i) - if err != nil { - return blockBorders{}, err - } - - bb.Number = i - bb.FirstTx = txCounter - txCounter++ - b, err := blockReader.BodyWithTransactions(ctx, tx, h.Hash(), i) - if err != nil { - return blockBorders{}, err - } - txCounter += uint64(len(b.Transactions)) - txCounter++ - bb.LastTx = txCounter - - if txCounter >= txNum { - bb.CurrentTx = txNum - return bb, nil - } - } - return blockBorders{}, fmt.Errorf("block with txn %x not found", txNum) -} - -type TrieCfg struct { - db kv.RwDB - checkRoot bool - badBlockHalt bool - tmpDir string - saveNewHashesToDB bool // no reason to save changes when calculating root for mining - blockReader services.FullBlockReader - hd *headerdownload.HeaderDownload - - historyV3 bool - agg *state.Aggregator -} - -func StageTrieCfg(db kv.RwDB, checkRoot, saveNewHashesToDB, badBlockHalt bool, tmpDir string, blockReader services.FullBlockReader, hd *headerdownload.HeaderDownload, historyV3 bool, agg *state.Aggregator) TrieCfg { - return TrieCfg{ - db: db, - checkRoot: checkRoot, - tmpDir: tmpDir, - saveNewHashesToDB: saveNewHashesToDB, - badBlockHalt: badBlockHalt, - blockReader: blockReader, - hd: hd, - - historyV3: historyV3, - agg: agg, - } -} - -type HashStateCfg struct { - db kv.RwDB - dirs datadir.Dirs -} - -func StageHashStateCfg(db kv.RwDB, dirs datadir.Dirs) HashStateCfg { - return HashStateCfg{ - db: db, - dirs: dirs, - } -} - -var ErrInvalidStateRootHash = errors.New("invalid state root hash") - -func RebuildPatriciaTrieBasedOnFiles(rwTx kv.RwTx, cfg TrieCfg, ctx context.Context, logger log.Logger) (libcommon.Hash, error) { - useExternalTx := rwTx != nil - if !useExternalTx { - var err error - rwTx, err = cfg.db.BeginRw(context.Background()) - if err != nil { - return trie.EmptyRoot, err - } - defer rwTx.Rollback() - } - txNumsReader := rawdbv3.TxNums.WithCustomReadTxNumFunc(freezeblocks.ReadTxNumFuncFromBlockReader(ctx, cfg.blockReader)) - var foundHash bool - toTxNum := rwTx.(*temporal.Tx).AggTx().(*state.AggregatorRoTx).EndTxNumNoCommitment() - ok, blockNum, err := txNumsReader.FindBlockNum(rwTx, toTxNum) - if err != nil { - return libcommon.Hash{}, err - } - if !ok { - bb, err := countBlockByTxnum(ctx, rwTx, cfg.blockReader, toTxNum) - if err != nil { - return libcommon.Hash{}, err - } - blockNum = bb.Number - foundHash = bb.Offset() != 0 - } else { - firstTxInBlock, err := txNumsReader.Min(rwTx, blockNum) - if err != nil { - return libcommon.Hash{}, fmt.Errorf("failed to find first txNum in block %d : %w", blockNum, err) - } - lastTxInBlock, err := txNumsReader.Max(rwTx, blockNum) - if err != nil { - return libcommon.Hash{}, fmt.Errorf("failed to find last txNum in block %d : %w", blockNum, err) - } - if firstTxInBlock == toTxNum || lastTxInBlock == toTxNum { - foundHash = true // state is in the beginning or end of block - } - } - - var expectedRootHash libcommon.Hash - var headerHash libcommon.Hash - var syncHeadHeader *types.Header - if foundHash && cfg.checkRoot { - syncHeadHeader, err = cfg.blockReader.HeaderByNumber(ctx, rwTx, blockNum) - if err != nil { - return trie.EmptyRoot, err - } - if syncHeadHeader == nil { - return trie.EmptyRoot, fmt.Errorf("no header found with number %d", blockNum) - } - expectedRootHash = syncHeadHeader.Root - headerHash = syncHeadHeader.Hash() - } - - rh, err := collectAndComputeCommitment(ctx, rwTx, cfg.tmpDir, toTxNum) - if err != nil { - return trie.EmptyRoot, err - } - - if foundHash && cfg.checkRoot && !bytes.Equal(rh, expectedRootHash[:]) { - logger.Error(fmt.Sprintf("[RebuildCommitment] Wrong trie root of block %d: %x, expected (from header): %x. Block hash: %x", blockNum, rh, expectedRootHash, headerHash)) - rwTx.Rollback() - - return trie.EmptyRoot, errors.New("wrong trie root") - } - logger.Info(fmt.Sprintf("[RebuildCommitment] Trie root of block %d txNum %d: %x. Could not verify with block hash because txnum of state is in the middle of the block.", blockNum, toTxNum, rh)) - - if !useExternalTx { - if err := rwTx.Commit(); err != nil { - return trie.EmptyRoot, err - } - } - return libcommon.BytesToHash(rh), err -} diff --git a/eth/stagedsync/stage_trie3_test.go b/eth/stagedsync/stage_trie3_test.go deleted file mode 100644 index f848859c56c..00000000000 --- a/eth/stagedsync/stage_trie3_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2024 The Erigon Authors -// This file is part of Erigon. -// -// Erigon is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Erigon is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with Erigon. If not, see . - -package stagedsync - -import ( - "context" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/erigontech/erigon-lib/kv/temporal/temporaltest" - "github.com/erigontech/erigon-lib/log/v3" - - "github.com/erigontech/erigon-lib/common/datadir" - "github.com/erigontech/erigon-lib/kv/rawdbv3" - "github.com/erigontech/erigon-lib/state" - "github.com/erigontech/erigon/eth/stagedsync/stages" -) - -func TestRebuildPatriciaTrieBasedOnFiles(t *testing.T) { - ctx := context.Background() - dirs := datadir.New(t.TempDir()) - db, agg := temporaltest.NewTestDB(t, dirs) - logger := log.New() - - tx, err := db.BeginRw(context.Background()) - require.NoError(t, err) - defer func() { - if tx != nil { - tx.Rollback() - tx = nil - } - if db != nil { - db.Close() - } - if agg != nil { - agg.Close() - } - }() - - before, after, writer := apply(tx, logger) - blocksTotal := uint64(100_000) - generateBlocks2(t, 1, blocksTotal, writer, before, after, staticCodeStaticIncarnations) - - err = stages.SaveStageProgress(tx, stages.Execution, blocksTotal) - require.NoError(t, err) - - for i := uint64(0); i <= blocksTotal; i++ { - err = rawdbv3.TxNums.Append(tx, i, i) - require.NoError(t, err) - } - - domains, err := state.NewSharedDomains(tx, logger) - require.NoError(t, err) - defer domains.Close() - domains.SetBlockNum(blocksTotal) - domains.SetTxNum(blocksTotal - 1) // generated 1tx per block - - expectedRoot, err := domains.ComputeCommitment(ctx, true, domains.BlockNum(), "") - require.NoError(t, err) - t.Logf("expected root is %x", expectedRoot) - - err = domains.Flush(context.Background(), tx) - require.NoError(t, err) - - domains.Close() - - require.NoError(t, tx.Commit()) - tx = nil - - // start another tx - tx, err = db.BeginRw(context.Background()) - require.NoError(t, err) - defer tx.Rollback() - - buckets, err := tx.ListBuckets() - require.NoError(t, err) - for i, b := range buckets { - if strings.Contains(strings.ToLower(b), "commitment") { - size, err := tx.BucketSize(b) - require.NoError(t, err) - t.Logf("cleaned table #%d %s: %d keys", i, b, size) - - err = tx.ClearBucket(b) - require.NoError(t, err) - } - } - - // checkRoot is false since we do not pass blockReader and want to check root manually afterwards. - historyV3 := true - cfg := StageTrieCfg(db, false /* checkRoot */, true /* saveHashesToDb */, false /* badBlockHalt */, dirs.Tmp, nil, nil /* hd */, historyV3, agg) - - rebuiltRoot, err := RebuildPatriciaTrieBasedOnFiles(tx, cfg, context.Background(), log.New()) - require.NoError(t, err) - - require.EqualValues(t, expectedRoot, rebuiltRoot) - t.Logf("rebuilt commitment %q", rebuiltRoot) -} diff --git a/polygon/heimdall/client.go b/polygon/heimdall/client.go index 5cffd3b1997..c39062473cd 100644 --- a/polygon/heimdall/client.go +++ b/polygon/heimdall/client.go @@ -486,6 +486,9 @@ func FetchWithRetryEx[T any]( ) (result *T, err error) { attempt := 0 // create a new ticker for retrying the request + if client.retryBackOff < apiHeimdallTimeout { + client.retryBackOff = apiHeimdallTimeout + time.Second*2 + } ticker := time.NewTicker(client.retryBackOff) defer ticker.Stop() diff --git a/tests/testdata b/tests/testdata index 92010754908..e46e1db503e 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit 9201075490807f58811078e9bb5ec895b4ac01a5 +Subproject commit e46e1db503ee2711ad02e1f5b3ea45d43e9cd8cb diff --git a/turbo/execution/eth1/forkchoice.go b/turbo/execution/eth1/forkchoice.go index d24203e57f9..87e5b6a3e80 100644 --- a/turbo/execution/eth1/forkchoice.go +++ b/turbo/execution/eth1/forkchoice.go @@ -25,6 +25,7 @@ import ( "github.com/erigontech/erigon-lib/common" "github.com/erigontech/erigon-lib/common/dbg" + "github.com/erigontech/erigon-lib/common/metrics" "github.com/erigontech/erigon-lib/gointerfaces" execution "github.com/erigontech/erigon-lib/gointerfaces/executionproto" "github.com/erigontech/erigon-lib/kv" @@ -519,7 +520,7 @@ func (e *EthereumExecutionModule) updateForkChoice(ctx context.Context, original } gasUsedMgas := float64(fcuHeader.GasUsed) / 1e6 mgasPerSec := gasUsedMgas / totalTime.Seconds() - e.avgMgasSec = ((e.avgMgasSec * (float64(e.recordedMgasSec))) + mgasPerSec) / float64(e.recordedMgasSec+1) + metrics.ChainTipMgasPerSec.Add(mgasPerSec) e.recordedMgasSec++ logArgs = append(logArgs, "number", fcuHeader.Number.Uint64(), "execution", blockTimings[engine_helpers.BlockTimingsValidationIndex], "mgas/s", fmt.Sprintf("%.2f", mgasPerSec), "average mgas/s", fmt.Sprintf("%.2f", e.avgMgasSec)) if !e.syncCfg.ParallelStateFlushing { diff --git a/turbo/snapshotsync/freezeblocks/block_reader.go b/turbo/snapshotsync/freezeblocks/block_reader.go index 656eae2f665..fb332ccd477 100644 --- a/turbo/snapshotsync/freezeblocks/block_reader.go +++ b/turbo/snapshotsync/freezeblocks/block_reader.go @@ -903,7 +903,11 @@ func (r *BlockReader) headerFromSnapshot(blockHeight uint64, sn *VisibleSegment, func (r *BlockReader) headerFromSnapshotByHash(hash common.Hash, sn *VisibleSegment, buf []byte) (*types.Header, error) { defer func() { if rec := recover(); rec != nil { - panic(fmt.Errorf("%+v, snapshot: %d-%d, trace: %s", rec, sn.from, sn.to, dbg.Stack())) + fname := "src=nil" + if sn.src != nil { + fname = sn.src.FileName() + } + panic(fmt.Errorf("%+v, snapshot: %d-%d fname: %s, trace: %s", rec, sn.from, sn.to, fname, dbg.Stack())) } }() // avoid crash because Erigon's core does many things diff --git a/turbo/stages/stageloop.go b/turbo/stages/stageloop.go index 52bc1f1c571..c87bedfa353 100644 --- a/turbo/stages/stageloop.go +++ b/turbo/stages/stageloop.go @@ -29,6 +29,7 @@ import ( libcommon "github.com/erigontech/erigon-lib/common" "github.com/erigontech/erigon-lib/common/datadir" "github.com/erigontech/erigon-lib/common/dbg" + "github.com/erigontech/erigon-lib/common/metrics" proto_downloader "github.com/erigontech/erigon-lib/gointerfaces/downloaderproto" "github.com/erigontech/erigon-lib/gointerfaces/sentryproto" "github.com/erigontech/erigon-lib/kv" @@ -153,7 +154,7 @@ func ProcessFrozenBlocks(ctx context.Context, db kv.RwDB, blockReader services.F if hook != nil { if err := db.View(ctx, func(tx kv.Tx) (err error) { - finishProgressBefore, _, _, err := stagesHeadersAndFinish(db, tx) + finishProgressBefore, _, _, _, err := stagesHeadersAndFinish(db, tx) if err != nil { return err } @@ -204,7 +205,7 @@ func StageLoopIteration(ctx context.Context, db kv.RwDB, txc wrap.TxContainer, s }() // avoid crash because Erigon's core does many things externalTx := txc.Tx != nil - finishProgressBefore, borProgressBefore, headersProgressBefore, err := stagesHeadersAndFinish(db, txc.Tx) + finishProgressBefore, borProgressBefore, headersProgressBefore, gasUsed, err := stagesHeadersAndFinish(db, txc.Tx) if err != nil { return err } @@ -276,6 +277,27 @@ func StageLoopIteration(ctx context.Context, db kv.RwDB, txc wrap.TxContainer, s //if len(logCtx) > 0 { // No printing of timings or table sizes if there were no progress var m runtime.MemStats dbg.ReadMemStats(&m) + if gasUsed > 0 { + var mgasPerSec float64 + // this is a bit hacky + for i, v := range logCtx { + if stringVal, ok := v.(string); ok && stringVal == "Execution" { + execTime := logCtx[i+1].(string) + // convert from ..ms to duration + execTimeDuration, err := time.ParseDuration(execTime) + if err != nil { + logger.Error("Failed to parse execution time", "err", err) + } else { + gasUsedMgas := float64(gasUsed) / 1e6 + mgasPerSec = gasUsedMgas / execTimeDuration.Seconds() + } + } + } + if mgasPerSec > 0 { + metrics.ChainTipMgasPerSec.Add(mgasPerSec) + logCtx = append(logCtx, "mgas/s", mgasPerSec) + } + } logCtx = append(logCtx, "alloc", libcommon.ByteCount(m.Alloc), "sys", libcommon.ByteCount(m.Sys)) logger.Info("Timings (slower than 50ms)", logCtx...) //if len(tableSizes) > 0 { @@ -297,24 +319,29 @@ func StageLoopIteration(ctx context.Context, db kv.RwDB, txc wrap.TxContainer, s return nil } -func stagesHeadersAndFinish(db kv.RoDB, tx kv.Tx) (head, bor, fin uint64, err error) { +func stagesHeadersAndFinish(db kv.RoDB, tx kv.Tx) (head, bor, fin uint64, gasUsed uint64, err error) { if tx != nil { if fin, err = stages.GetStageProgress(tx, stages.Finish); err != nil { - return head, bor, fin, err + return head, bor, fin, gasUsed, err } if head, err = stages.GetStageProgress(tx, stages.Headers); err != nil { - return head, bor, fin, err + return head, bor, fin, gasUsed, err } if bor, err = stages.GetStageProgress(tx, stages.BorHeimdall); err != nil { - return head, bor, fin, err + return head, bor, fin, gasUsed, err } var polygonSync uint64 if polygonSync, err = stages.GetStageProgress(tx, stages.PolygonSync); err != nil { - return head, bor, fin, err + return head, bor, fin, gasUsed, err } + // bor heimdall and polygon sync are mutually exclusive, bor heimdall will be removed soon bor = max(bor, polygonSync) - return head, bor, fin, nil + h := rawdb.ReadHeaderByNumber(tx, head) + if h != nil { + gasUsed = h.GasUsed + } + return head, bor, fin, gasUsed, nil } if err := db.View(context.Background(), func(tx kv.Tx) error { if fin, err = stages.GetStageProgress(tx, stages.Finish); err != nil { @@ -330,13 +357,18 @@ func stagesHeadersAndFinish(db kv.RoDB, tx kv.Tx) (head, bor, fin uint64, err er if polygonSync, err = stages.GetStageProgress(tx, stages.PolygonSync); err != nil { return err } - // bor heimdall and polygon sync are mutually exclusive, bor heimdall will be removed soon bor = max(bor, polygonSync) + + h := rawdb.ReadHeaderByNumber(tx, head) + if h != nil { + gasUsed = h.GasUsed + } + // bor heimdall and polygon sync are mutually exclusive, bor heimdall will be removed soon return nil }); err != nil { - return head, bor, fin, err + return head, bor, fin, gasUsed, err } - return head, bor, fin, nil + return head, bor, fin, gasUsed, nil } type Hook struct {