Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integrity check for commitment state #13992

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
save
  • Loading branch information
awskii committed Feb 27, 2025
commit 58f23ccbb872c96f508a6369cbb52ada68a92324
10 changes: 6 additions & 4 deletions cmd/commitment-prefix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,13 @@
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)
stat, err := commitment.DecodeBranchAndCollectStat(key, val, tv)
if err != nil {
// show and continue
fmt.Printf("\n\n%w: failed to decode branch: %x %x\n", err, key, val)

Check failure on line 226 in cmd/commitment-prefix/main.go

View workflow job for this annotation

GitHub Actions / lint

printf: fmt.Printf does not support error-wrapping directive %w (govet)

Check failure on line 226 in cmd/commitment-prefix/main.go

View workflow job for this annotation

GitHub Actions / tests-windows (windows-2022)

fmt.Printf does not support error-wrapping directive %w

Check failure on line 226 in cmd/commitment-prefix/main.go

View workflow job for this annotation

GitHub Actions / tests-windows (windows-2022)

fmt.Printf does not support error-wrapping directive %w
} else {
keysSink <- *stat
}
keysSink <- *stat
}
return nil
}
Expand Down
45 changes: 38 additions & 7 deletions erigon-lib/commitment/commitment.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,25 @@ func (branchData BranchData) IsComplete() bool {
return ^touchMap&afterMap == 0
}

func (branchData BranchData) Verify(itsPrefix []byte) error {
if bytes.Equal(itsPrefix, []byte("state")) {
return nil
}

// most checks happen during decoding
_, am, cells, err := branchData.decodeCells()
if err != nil {
return err
}
if am == 0 && len(branchData) > 4 {
fmt.Printf("tombstone %x contains %d trailing bytes\n", itsPrefix, len(branchData)-4)
return fmt.Errorf("tombstone %x contains %d trailing bytes\n", itsPrefix, len(branchData)-4)
}
_ = cells

return nil
}

// MergeHexBranches combines two branchData, number 2 coming after (and potentially shadowing) number 1
func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData []byte) (BranchData, error) {
if branchData2 == nil {
Expand Down Expand Up @@ -640,23 +659,35 @@ func (branchData BranchData) MergeHexBranches(branchData2 BranchData, newData []
}

func (branchData BranchData) decodeCells() (touchMap, afterMap uint16, row [16]*cell, err error) {
pos, decCount := 4, 0
if len(branchData) < pos {
return 0, 0, row, fmt.Errorf("branch is shorter than 4 bytes")
}
touchMap = binary.BigEndian.Uint16(branchData[0:])
afterMap = binary.BigEndian.Uint16(branchData[2:])
pos := 4
for bitset, j := touchMap, 0; bitset != 0; j++ {
bit := bitset & -bitset
nibble := bits.TrailingZeros16(bit)
if afterMap&bit != 0 {
if pos >= len(branchData) {
return 0, 0, row, fmt.Errorf("branch ended at %d without nibble %x", nibble)
}
fields := cellFields(branchData[pos])
pos++
row[nibble] = new(cell)
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
return 0, 0, row, fmt.Errorf("failed to read cell at nibble %x: %w", nibble, err)
}
decCount++
}
bitset ^= bit
}
if exp := bits.OnesCount16(afterMap); exp != decCount {
if afterMap != touchMap && decCount == bits.OnesCount16(touchMap) {
return 0, 0, row, fmt.Errorf("decoded partial update for %d cells, expected %d", decCount, exp)
}
return 0, 0, row, fmt.Errorf("decoded %d cells, expected %d", decCount, exp)
}
return
}

Expand Down Expand Up @@ -839,10 +870,10 @@ func (bs *BranchStat) Collect(other *BranchStat) {
bs.LeafHashCount += other.LeafHashCount
}

func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat {
func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) (*BranchStat, error) {
stat := &BranchStat{}
if len(key) == 0 {
return nil
return nil, nil
}

stat.KeySize = uint64(len(key))
Expand All @@ -855,7 +886,7 @@ func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat

tm, am, cells, err := BranchData(branch).decodeCells()
if err != nil {
return nil
return nil, err
}
stat.TAMapsSize = uint64(2 + 2) // touchMap + afterMap
stat.CellCount = uint64(bits.OnesCount16(tm & am))
Expand Down Expand Up @@ -919,7 +950,7 @@ func DecodeBranchAndCollectStat(key, branch []byte, tv TrieVariant) *BranchStat
}
}
}
return stat
return stat, nil
}

// Defines how to evaluate commitments
Expand Down
1 change: 1 addition & 0 deletions erigon-lib/state/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ func (ac *AggregatorRoTx) StepsInFiles(entitySet ...kv.Domain) uint64 {
return txNumInFiles / ac.a.StepSize()
}

// TxNumsInFiles - returns minimal TxNum in files across domains in entitySet
func (ac *AggregatorRoTx) TxNumsInFiles(entitySet ...kv.Domain) (minTxNum uint64) {
if len(entitySet) == 0 {
panic("assert: missed arguments")
Expand Down
105 changes: 105 additions & 0 deletions eth/integrity/commitment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2025 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 <http://www.gnu.org/licenses/>.

package integrity

import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"

"golang.org/x/sync/errgroup"

"github.com/erigontech/erigon-lib/commitment"
"github.com/erigontech/erigon-lib/common"
"github.com/erigontech/erigon-lib/kv"
"github.com/erigontech/erigon-lib/log/v3"
"github.com/erigontech/erigon-lib/state"
"github.com/erigontech/erigon/turbo/services"
)

// E3 History - usually don't have anything attributed to 1-st system txs (except genesis)
func CommitmentFilesSanity(ctx context.Context, db kv.TemporalRwDB, blockReader services.FullBlockReader, agg *state.Aggregator) error {
logEvery := time.NewTicker(20 * time.Second)
defer logEvery.Stop()
start := time.Now()
var count atomic.Uint64
g := new(errgroup.Group)
info := sync.Once{}

for j := 0; j < 256; j++ {
j := j
for jj := 0; jj < 255; jj++ {
jj := jj
g.Go(func() error {
tx, err := db.BeginTemporalRo(ctx)
if err != nil {
return err
}
defer tx.Rollback()

aggTx := tx.(state.HasAggTx).AggTx().(*state.AggregatorRoTx)
info.Do(func() {
log.Info("Checking commitment files", "domain", kv.CommitmentDomain, "txn", aggTx.TxNumsInFiles(kv.CommitmentDomain))
})
keys, err := aggTx.RangeLatest(tx, kv.CommitmentDomain, []byte{byte(j), byte(jj)}, []byte{byte(j), byte(jj + 1)}, -1)
if err != nil {
return err
}
defer keys.Close()

for keys.HasNext() {
prefix, branchData, err := keys.Next()
if err != nil {
return err
}

err = commitment.BranchData(branchData).Verify(prefix)
if err != nil {
return err
}
// if aggTx.DbgDomain(kv.CommitmentDomain).()
// during fetching latest all branches are dereferenced so plain keys for nodes are available
bdDereferenced, _, _, err := aggTx.GetLatest(kv.CommitmentDomain, prefix, tx)
if err != nil {
return err
}
if len(branchData) > len(bdDereferenced) {
return fmt.Errorf("defererenced branch %x is shorter than referenced", prefix)
}
if err = commitment.BranchData(bdDereferenced).Verify(prefix); err != nil {
return err
}

select {
case <-logEvery.C:
log.Warn(fmt.Sprintf("[dbg] checked=%s prefixes", common.PrettyCounter(count.Load())))
default:
}
count.Add(1)
}
return nil
})
}
}
if err := g.Wait(); err != nil {
return err
}
log.Info("finished checking commitment sanity", "prefixes", common.PrettyCounter(count.Load()), "time spent", time.Since(start))
return nil
}
3 changes: 2 additions & 1 deletion eth/integrity/integrity_action_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ const (
BorSpans Check = "BorSpans"
BorCheckpoints Check = "BorCheckpoints"
BorMilestones Check = "BorMilestones" // this check is informational, and we don't run it by default (e.g. gaps may exist but that is ok)
Commitment Check = "Commitment"
)

var AllChecks = []Check{
Blocks, BlocksTxnID, InvertedIndex, HistoryNoSystemTxs, ReceiptsNoDups, BorEvents, BorSpans, BorCheckpoints,
Blocks, BlocksTxnID, InvertedIndex, HistoryNoSystemTxs, ReceiptsNoDups, BorEvents, BorSpans, BorCheckpoints, Commitment,
}

var NonDefaultChecks = []Check{
Expand Down
4 changes: 4 additions & 0 deletions turbo/app/snapshots_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,10 @@ func doIntegrity(cliCtx *cli.Context) error {
if err := integrity.ReceiptsNoDuplicates(ctx, db, blockReader, failFast); err != nil {
return err
}
case integrity.Commitment:
if err := integrity.CommitmentFilesSanity(ctx, db, blockReader, agg); err != nil {
return err
}
default:
return fmt.Errorf("unknown check: %s", chk)
}
Expand Down
Loading