Skip to content

Commit 9599fba

Browse files
committed
core: fix removing old transfer data with RemoveUntraceableHeaders
Transfer data is timestamp-based, previously it always had and used headers, no we can go via a small cache (we don't want it to grow or be stored forever). Otherwise it's unable to do the job: 2024-12-13T12:55:15.056+0300 ERROR failed to find block header for transfer GC {"time": "19.066µs", "error": "key not found"} Signed-off-by: Roman Khimov <roman@nspcc.ru>
1 parent 7d89a53 commit 9599fba

File tree

4 files changed

+48
-18
lines changed

4 files changed

+48
-18
lines changed

pkg/core/blockchain.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sync/atomic"
1313
"time"
1414

15+
lru "github.com/hashicorp/golang-lru/v2"
1516
json "github.com/nspcc-dev/go-ordered-json"
1617
"github.com/nspcc-dev/neo-go/pkg/config"
1718
"github.com/nspcc-dev/neo-go/pkg/config/limits"
@@ -61,6 +62,14 @@ const (
6162
// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
6263
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
6364
defaultStateSyncInterval = 40000
65+
66+
// defaultBlockTimesCache should be sufficient for tryRunGC() to get in
67+
// sync with storeBlock(). Most of the time they differ by some thousands of
68+
// blocks and GC interval is more like 10K, so this is sufficient for 80K
69+
// deviation and should be sufficient. If it's not, it's not a big issue
70+
// either, the next cycle will still do the job (only transfers need this,
71+
// MPT won't notice at all).
72+
defaultBlockTimesCache = 8
6473
)
6574

6675
// stateChangeStage denotes the stage of state modification process.
@@ -156,6 +165,11 @@ type Blockchain struct {
156165
// Current persisted block count.
157166
persistedHeight uint32
158167

168+
// Index->Timestamp cache for garbage collector. Headers can be gone
169+
// by the time it runs, so we use a tiny little cache to sync block
170+
// removal (performed in storeBlock()) with transfer/MPT GC (tryRunGC())
171+
gcBlockTimes *lru.Cache[uint32, uint64]
172+
159173
// Stop synchronization mechanisms.
160174
stopCh chan struct{}
161175
runToExitCh chan struct{}
@@ -324,6 +338,7 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger) (*Bl
324338
contracts: *native.NewContracts(cfg.ProtocolConfiguration),
325339
}
326340

341+
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
327342
bc.stateRoot = stateroot.NewModule(cfg, bc.VerifyWitness, bc.log, bc.dao.Store)
328343
bc.contracts.Designate.StateRootService = bc.stateRoot
329344

@@ -606,7 +621,7 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) erro
606621
// After current state is updated, we need to remove outdated state-related data if so.
607622
// The only outdated data we might have is genesis-related data, so check it.
608623
if p-bc.config.MaxTraceableBlocks > 0 {
609-
err := cache.DeleteBlock(bc.GetHeaderHash(0), false)
624+
_, err := cache.DeleteBlock(bc.GetHeaderHash(0), false)
610625
if err != nil {
611626
return fmt.Errorf("failed to remove outdated state data for the genesis block: %w", err)
612627
}
@@ -800,7 +815,7 @@ func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage)
800815
keysCnt = new(int)
801816
)
802817
for i := height + 1; i <= currHeight; i++ {
803-
err := upperCache.DeleteBlock(bc.GetHeaderHash(i), false)
818+
_, err := upperCache.DeleteBlock(bc.GetHeaderHash(i), false)
804819
if err != nil {
805820
return fmt.Errorf("error while removing block %d: %w", i, err)
806821
}
@@ -1289,15 +1304,20 @@ func appendTokenTransferInfo(transferData *state.TokenTransferInfo,
12891304

12901305
func (bc *Blockchain) removeOldTransfers(index uint32) time.Duration {
12911306
bc.log.Info("starting transfer data garbage collection", zap.Uint32("index", index))
1292-
start := time.Now()
1293-
h, err := bc.GetHeader(bc.GetHeaderHash(index))
1294-
if err != nil {
1307+
var (
1308+
err error
1309+
kept int64
1310+
removed int64
1311+
start = time.Now()
1312+
ts, ok = bc.gcBlockTimes.Get(index)
1313+
)
1314+
1315+
if !ok {
12951316
dur := time.Since(start)
1296-
bc.log.Error("failed to find block header for transfer GC", zap.Duration("time", dur), zap.Error(err))
1317+
bc.log.Error("failed to get block timestamp transfer GC", zap.Duration("time", dur), zap.Uint32("index", index))
12971318
return dur
12981319
}
1299-
var removed, kept int64
1300-
var ts = h.Timestamp
1320+
13011321
prefixes := []byte{byte(storage.STNEP11Transfers), byte(storage.STNEP17Transfers)}
13021322

13031323
for i := range prefixes {
@@ -1623,7 +1643,10 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
16231643
stop = start + 1
16241644
}
16251645
for index := start; index < stop; index++ {
1626-
err := kvcache.DeleteBlock(bc.GetHeaderHash(index), bc.config.Ledger.RemoveUntraceableHeaders)
1646+
ts, err := kvcache.DeleteBlock(bc.GetHeaderHash(index), bc.config.Ledger.RemoveUntraceableHeaders)
1647+
if bc.config.Ledger.RemoveUntraceableHeaders && index%bc.config.Ledger.GarbageCollectionPeriod == 0 {
1648+
_ = bc.gcBlockTimes.Add(index, ts)
1649+
}
16271650
if err != nil {
16281651
bc.log.Warn("error while removing old block",
16291652
zap.Uint32("index", index),

pkg/core/blockchain_core_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func TestRemoveOldTransfers(t *testing.T) {
106106

107107
_, err = bc.dao.Persist()
108108
require.NoError(t, err)
109+
_ = bc.gcBlockTimes.Add(0, h.Timestamp)
109110
_ = bc.removeOldTransfers(0)
110111

111112
for i := range uint32(2) {

pkg/core/dao/dao.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -765,18 +765,19 @@ func (dao *Simple) StoreAsBlock(block *block.Block, aer1 *state.AppExecResult, a
765765
}
766766

767767
// DeleteBlock removes the block from dao. It's not atomic, so make sure you're
768-
// using private MemCached instance here.
769-
func (dao *Simple) DeleteBlock(h util.Uint256, dropHeader bool) error {
768+
// using private MemCached instance here. It returns block timestamp for GC
769+
// convenience.
770+
func (dao *Simple) DeleteBlock(h util.Uint256, dropHeader bool) (uint64, error) {
770771
key := dao.makeExecutableKey(h)
771772

772773
b, err := dao.getBlock(key)
773774
if err != nil {
774-
return err
775+
return 0, err
775776
}
776777
if !dropHeader {
777778
err = dao.storeHeader(key, &b.Header)
778779
if err != nil {
779-
return err
780+
return 0, err
780781
}
781782
} else {
782783
dao.Store.Delete(key)
@@ -791,7 +792,7 @@ func (dao *Simple) DeleteBlock(h util.Uint256, dropHeader bool) error {
791792

792793
v, err := dao.Store.Get(key)
793794
if err != nil {
794-
return fmt.Errorf("failed to retrieve conflict record stub for %s (height %d, conflict %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), err)
795+
return 0, fmt.Errorf("failed to retrieve conflict record stub for %s (height %d, conflict %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), err)
795796
}
796797
// It might be a block since we allow transactions to have block hash in the Conflicts attribute.
797798
if v[0] != storage.ExecTransaction {
@@ -809,7 +810,7 @@ func (dao *Simple) DeleteBlock(h util.Uint256, dropHeader bool) error {
809810
sKey := append(key, s.Account.BytesBE()...)
810811
v, err := dao.Store.Get(sKey)
811812
if err != nil {
812-
return fmt.Errorf("failed to retrieve conflict record for %s (height %d, conflict %s, signer %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), address.Uint160ToString(s.Account), err)
813+
return 0, fmt.Errorf("failed to retrieve conflict record for %s (height %d, conflict %s, signer %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), address.Uint160ToString(s.Account), err)
813814
}
814815
index = binary.LittleEndian.Uint32(v[1:])
815816
if index == b.Index {
@@ -819,7 +820,7 @@ func (dao *Simple) DeleteBlock(h util.Uint256, dropHeader bool) error {
819820
}
820821
}
821822

822-
return nil
823+
return b.Timestamp, nil
823824
}
824825

825826
// PurgeHeader completely removes specified header from dao. It differs from

pkg/core/dao/dao_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func TestPutGetBlock(t *testing.T) {
7474
dao := NewSimple(storage.NewMemoryStore(), false)
7575
b := &block.Block{
7676
Header: block.Header{
77+
Timestamp: 42,
7778
Script: transaction.Witness{
7879
VerificationScript: []byte{byte(opcode.PUSH1)},
7980
InvocationScript: []byte{byte(opcode.NOP)},
@@ -108,12 +109,16 @@ func TestPutGetBlock(t *testing.T) {
108109
require.Equal(t, *appExecResult1, gotAppExecResult[0])
109110
require.Equal(t, *appExecResult2, gotAppExecResult[1])
110111

111-
require.NoError(t, dao.DeleteBlock(hash, false))
112+
ts, err := dao.DeleteBlock(hash, false)
113+
require.NoError(t, err)
114+
require.Equal(t, uint64(42), ts)
112115
gotBlock, err = dao.GetBlock(hash) // It's just a header, but it's still there.
113116
require.NoError(t, err)
114117
require.NotNil(t, gotBlock)
115118

116-
require.NoError(t, dao.DeleteBlock(hash, true))
119+
ts, err = dao.DeleteBlock(hash, true)
120+
require.NoError(t, err)
121+
require.Equal(t, uint64(42), ts)
117122
_, err = dao.GetBlock(hash)
118123
require.Error(t, err)
119124
}

0 commit comments

Comments
 (0)