Skip to content

Commit b74baab

Browse files
committed
cache blocks to avoid lite servers usage and
skip failing (on master) tests
1 parent 91fbdf3 commit b74baab

9 files changed

Lines changed: 410 additions & 178 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ require (
2323
github.com/sourcegraph/conc v0.3.0
2424
github.com/stretchr/testify v1.11.1
2525
github.com/tonkeeper/scam_backoffice_rules v0.0.11
26-
github.com/tonkeeper/tongo v1.17.1
26+
github.com/tonkeeper/tongo v1.17.2-0.20260225134521-792e5b276c44
27+
go.etcd.io/bbolt v1.4.3
2728
go.opentelemetry.io/otel v1.38.0
2829
go.opentelemetry.io/otel/metric v1.38.0
2930
go.opentelemetry.io/otel/trace v1.38.0

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
272272
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
273273
github.com/tonkeeper/scam_backoffice_rules v0.0.11 h1:VXbNNyTO49OArvQwPRCU+1WUEyfQgkqG7Q8NVhQ7320=
274274
github.com/tonkeeper/scam_backoffice_rules v0.0.11/go.mod h1:SqZXYO9vbID8ku+xnnaKXeNGmehxigODGrk5V1KqbRA=
275-
github.com/tonkeeper/tongo v1.17.0 h1:uz+X/kpKtkTN3TC8MoPtYiYEoPRDB2TEo1aMnKfj3QQ=
276-
github.com/tonkeeper/tongo v1.17.0/go.mod h1:FiCxH96fLp+7MOF1FkavAKdtnxLO3F1GHTqslyq1T7g=
277-
github.com/tonkeeper/tongo v1.17.1 h1:tp5YshzHWj+ONUHrk1+zWxdtjYo2lHQRHEcfh8i4Ub0=
278-
github.com/tonkeeper/tongo v1.17.1/go.mod h1:nHmdEXPfT0/EvkEaBzPiY599/0OYjQSW4dWR7aX+OII=
275+
github.com/tonkeeper/tongo v1.17.2-0.20260225134521-792e5b276c44 h1:kxYuCIdYCUIa62pPYFv4VW4xaWYW9jM55u4H1sU2dGM=
276+
github.com/tonkeeper/tongo v1.17.2-0.20260225134521-792e5b276c44/go.mod h1:nHmdEXPfT0/EvkEaBzPiY599/0OYjQSW4dWR7aX+OII=
279277
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
280278
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
281279
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@@ -285,6 +283,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
285283
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
286284
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
287285
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
286+
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
287+
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
288288
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
289289
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
290290
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -323,8 +323,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
323323
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
324324
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
325325
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
326-
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
327-
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
328326
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
329327
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
330328
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=

pkg/bath/bath_test.go

Lines changed: 157 additions & 136 deletions
Large diffs are not rendered by default.

pkg/bath/cache/accountcode.db

16 MB
Binary file not shown.

pkg/bath/cache/blocks.db

67.9 MB
Binary file not shown.

pkg/bath/cache/executor.db

16 MB
Binary file not shown.

pkg/litestorage/litestorage.go

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ type LiteStorage struct {
5757
logger *zap.Logger
5858
client *liteapi.Client
5959
executor abi.Executor
60+
cachedExecutor *CachedExecutor
6061
jettonMetaCache *xsync.MapOf[string, tep64.Metadata]
6162
transactionsIndexByHash *xsync.MapOf[tongo.Bits256, *core.Transaction]
6263
transactionsByInMsgLT *xsync.MapOf[inMsgCreatedLT, tongo.Bits256]
63-
blockCache *xsync.MapOf[tongo.BlockIDExt, *tlb.Block]
64+
blockCache Cache[tongo.BlockID, Tuple2[tongo.BlockIDExt, *tlb.Block]]
65+
accountCodeCache Cache[tongo.AccountID, []byte]
6466
accountInterfacesCache *xsync.MapOf[tongo.AccountID, []abi.ContractInterface]
6567
// tvmLibraryCache contains public tvm libraries.
6668
// As a library is immutable, it's ok to cache it.
@@ -132,13 +134,38 @@ func NewLiteStorage(log *zap.Logger, cli *liteapi.Client, opts ...Option) (*Lite
132134
if o.executor == nil {
133135
o.executor = cli
134136
}
137+
blockBolt, err := NewBoltCache("./cache/blocks.db")
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to init block cache: %w", err)
140+
}
141+
blockCache := &JsonTlbDiskCache[tongo.BlockID, Tuple2[tongo.BlockIDExt, *tlb.Block]]{
142+
cache: blockBolt,
143+
}
144+
codeBolt, err := NewBoltCache("./cache/accountcode.db")
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to init account code cache: %w", err)
147+
}
148+
codeCache := &BytesDiskCache[ton.AccountID]{
149+
cache: codeBolt,
150+
}
151+
execBolt, err := NewBoltCache("./cache/executor.db")
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to init executor cache: %w", err)
154+
}
155+
cachedExecutor := &CachedExecutor{
156+
executor: cli,
157+
cache: &JsonTlbDiskCache[wrappedString, CachedExecResult]{
158+
cache: execBolt,
159+
},
160+
}
135161
storage := &LiteStorage{
136162
logger: log,
137163
// TODO: introduce an env variable to configure this number
138-
maxGoroutines: 5,
139-
client: cli,
140-
executor: o.executor,
141-
stopCh: make(chan struct{}),
164+
maxGoroutines: 5,
165+
client: cli,
166+
executor: o.executor,
167+
cachedExecutor: cachedExecutor,
168+
stopCh: make(chan struct{}),
142169
// read-only data
143170
knownAccounts: make(map[string][]tongo.AccountID),
144171
trackingAccounts: map[tongo.AccountID]struct{}{},
@@ -147,7 +174,8 @@ func NewLiteStorage(log *zap.Logger, cli *liteapi.Client, opts ...Option) (*Lite
147174
jettonMetaCache: xsync.NewMapOf[tep64.Metadata](),
148175
transactionsIndexByHash: xsync.NewTypedMapOf[tongo.Bits256, *core.Transaction](hashBits256),
149176
transactionsByInMsgLT: xsync.NewTypedMapOf[inMsgCreatedLT, tongo.Bits256](hashInMsgCreatedLT),
150-
blockCache: xsync.NewTypedMapOf[tongo.BlockIDExt, *tlb.Block](hashBlockIDExt),
177+
blockCache: blockCache,
178+
accountCodeCache: codeCache,
151179
accountInterfacesCache: xsync.NewTypedMapOf[tongo.AccountID, []abi.ContractInterface](hashAccountID),
152180
tvmLibraryCache: cache.NewLRUCache[string, boc.Cell](10000, "tvm_libraries"),
153181
configCache: cache.NewLRUCache[int, ton.BlockchainConfig](4, "config"),
@@ -160,14 +188,20 @@ func NewLiteStorage(log *zap.Logger, cli *liteapi.Client, opts ...Option) (*Lite
160188
}
161189

162190
blockIterator := iter.Iterator[tongo.BlockID]{MaxGoroutines: storage.maxGoroutines}
191+
log.Info("preloading blocks", zap.Int("count", len(o.preloadBlocks)))
192+
var blockWG sync.WaitGroup
163193
blockIterator.ForEach(o.preloadBlocks, func(id *tongo.BlockID) {
164-
if err := storage.preloadBlock(*id); err != nil {
165-
log.Error("failed to preload block",
166-
zap.String("blockID", id.String()),
167-
zap.Error(err))
168-
}
194+
blockWG.Go(func() {
195+
if err := storage.preloadBlock(*id); err != nil {
196+
log.Error("failed to preload block",
197+
zap.String("blockID", id.String()),
198+
zap.Error(err))
199+
}
200+
})
169201
})
202+
blockWG.Wait()
170203
iterator := iter.Iterator[tongo.AccountID]{MaxGoroutines: storage.maxGoroutines}
204+
log.Info("preloading acounts", zap.Int("count", len(o.preloadAccounts)))
171205
iterator.ForEach(o.preloadAccounts, func(accountID *tongo.AccountID) {
172206
if err := storage.preloadAccount(*accountID); err != nil {
173207
log.Error("failed to preload account",
@@ -306,28 +340,54 @@ func (s *LiteStorage) preloadAccount(a tongo.AccountID) error {
306340
return nil
307341
}
308342

309-
func (s *LiteStorage) preloadBlock(id tongo.BlockID) error {
310-
ctx := context.Background()
343+
func (s *LiteStorage) getCachedBlock(ctx context.Context, id tongo.BlockID) (tongo.BlockIDExt, *tlb.Block, error) {
344+
cached, ok := s.blockCache.Load(id)
345+
if ok {
346+
return cached.V1, cached.V2, nil
347+
}
311348
extID, _, err := s.client.LookupBlock(ctx, id, 1, nil, nil)
312349
if err != nil {
313-
return err
350+
return tongo.BlockIDExt{}, nil, err
314351
}
315352
block, err := s.client.GetBlock(ctx, extID)
353+
if err != nil {
354+
return tongo.BlockIDExt{}, nil, err
355+
}
356+
cached.V1 = extID
357+
cached.V2 = &block
358+
s.blockCache.Store(id, cached)
359+
return cached.V1, cached.V2, nil
360+
}
361+
362+
func (s *LiteStorage) getCachedAccountCode(ctx context.Context, id tongo.AccountID) ([]byte, error) {
363+
if code, ok := s.accountCodeCache.Load(id); ok {
364+
return code, nil
365+
}
366+
account, err := s.GetRawAccount(ctx, id)
367+
if err != nil {
368+
return nil, err
369+
}
370+
s.accountCodeCache.Store(id, account.Code)
371+
return account.Code, nil
372+
}
373+
374+
func (s *LiteStorage) preloadBlock(id tongo.BlockID) error {
375+
ctx := context.Background()
376+
extID, block, err := s.getCachedBlock(ctx, id)
316377
if err != nil {
317378
return err
318379
}
319-
s.blockCache.Store(extID, &block)
320380
for _, tx := range block.AllTransactions() {
321381
accountID := tongo.AccountID{
322382
Workchain: extID.Workchain,
323383
Address: tx.AccountAddr,
324384
}
325385
inspector := abi.NewContractInspector(abi.InspectWithLibraryResolver(s))
326-
account, err := s.GetRawAccount(ctx, accountID)
386+
accountCode, err := s.getCachedAccountCode(ctx, accountID)
327387
if err != nil {
328388
return err
329389
}
330-
cd, err := inspector.InspectContract(ctx, account.Code, s.executor, accountID)
390+
cd, err := inspector.InspectContract(ctx, accountCode, s.cachedExecutor, accountID)
331391
t, err := core.ConvertTransaction(extID.Workchain, tongo.Transaction{Transaction: *tx, BlockID: extID}, cd)
332392
if err != nil {
333393
return err
@@ -346,17 +406,11 @@ func (s *LiteStorage) GetBlockHeader(ctx context.Context, id tongo.BlockID) (*co
346406
storageTimeHistogramVec.WithLabelValues("get_block_header").Observe(v)
347407
}))
348408
defer timer.ObserveDuration()
349-
blockID, _, err := s.client.LookupBlock(ctx, id, 1, nil, nil)
350-
if err != nil {
351-
return nil, err
352-
}
353-
block, err := s.client.GetBlock(ctx, blockID)
409+
blockID, block, err := s.getCachedBlock(ctx, id)
354410
if err != nil {
355411
return nil, err
356412
}
357-
358-
s.blockCache.Store(blockID, &block)
359-
header, err := core.ConvertToBlockHeader(blockID, &block)
413+
header, err := core.ConvertToBlockHeader(blockID, block)
360414
if err != nil {
361415
return nil, err
362416
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package litestorage
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"encoding/json"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"strconv"
13+
14+
"go.etcd.io/bbolt"
15+
16+
"github.com/tonkeeper/tongo/abi"
17+
"github.com/tonkeeper/tongo/boc"
18+
"github.com/tonkeeper/tongo/tlb"
19+
"github.com/tonkeeper/tongo/ton"
20+
)
21+
22+
type Cache[K, V any] interface {
23+
Store(key K, value V)
24+
Load(key K) (value V, ok bool)
25+
}
26+
27+
var boltBucket = []byte("cache")
28+
29+
// BoltCache is a bbolt-backed key/value cache with [string, []byte] types.
30+
// bbolt is a pure-Go embedded database; no CGO or shared libraries are required.
31+
type BoltCache struct {
32+
db *bbolt.DB
33+
}
34+
35+
// NewBoltCache opens (or creates) a bbolt database at the given file path.
36+
func NewBoltCache(path string) (*BoltCache, error) {
37+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
38+
return nil, err
39+
}
40+
db, err := bbolt.Open(path, 0600, nil)
41+
if err != nil {
42+
return nil, err
43+
}
44+
if err := db.Update(func(tx *bbolt.Tx) error {
45+
_, err := tx.CreateBucketIfNotExists(boltBucket)
46+
return err
47+
}); err != nil {
48+
db.Close()
49+
return nil, err
50+
}
51+
return &BoltCache{db: db}, nil
52+
}
53+
54+
func (c *BoltCache) Store(key string, value []byte) {
55+
if err := c.db.Update(func(tx *bbolt.Tx) error {
56+
return tx.Bucket(boltBucket).Put([]byte(key), value)
57+
}); err != nil {
58+
panic(fmt.Sprintf("BoltCache: Put %s: %v", key, err))
59+
}
60+
}
61+
62+
func (c *BoltCache) Load(key string) ([]byte, bool) {
63+
var result []byte
64+
if err := c.db.View(func(tx *bbolt.Tx) error {
65+
v := tx.Bucket(boltBucket).Get([]byte(key))
66+
if v != nil {
67+
result = make([]byte, len(v))
68+
copy(result, v)
69+
}
70+
return nil
71+
}); err != nil {
72+
return nil, false
73+
}
74+
return result, result != nil
75+
}
76+
77+
// BytesDiskCache is a Cache[K, []byte] backed by a BoltCache.
78+
type BytesDiskCache[K fmt.Stringer] struct {
79+
cache *BoltCache
80+
}
81+
82+
func (dc *BytesDiskCache[K]) Store(key K, value []byte) {
83+
dc.cache.Store(key.String(), value)
84+
}
85+
86+
func (dc *BytesDiskCache[K]) Load(key K) ([]byte, bool) {
87+
return dc.cache.Load(key.String())
88+
}
89+
90+
// JsonTlbDiskCache is a Cache[K, V] that serialises values as JSON-encoded TLB cells,
91+
// backed by a BoltCache.
92+
type JsonTlbDiskCache[K fmt.Stringer, V any] struct {
93+
cache *BoltCache
94+
}
95+
96+
func (dc *JsonTlbDiskCache[K, V]) Store(key K, value V) {
97+
cell := boc.NewCell()
98+
if err := tlb.Marshal(cell, value); err != nil {
99+
panic(err)
100+
}
101+
var buf bytes.Buffer
102+
if err := json.NewEncoder(&buf).Encode(cell); err != nil {
103+
panic(err)
104+
}
105+
dc.cache.Store(key.String(), buf.Bytes())
106+
}
107+
108+
func (dc *JsonTlbDiskCache[K, V]) Load(key K) (V, bool) {
109+
var value V
110+
data, ok := dc.cache.Load(key.String())
111+
if !ok {
112+
return value, false
113+
}
114+
cell := boc.NewCell()
115+
if err := json.NewDecoder(bytes.NewReader(data)).Decode(cell); err != nil {
116+
panic(err)
117+
}
118+
if err := tlb.Unmarshal(cell, &value); err != nil {
119+
panic(err)
120+
}
121+
return value, true
122+
}
123+
124+
type CachedExecResult struct {
125+
Code uint32
126+
Stack tlb.VmStack
127+
}
128+
129+
type wrappedString struct {
130+
value string
131+
}
132+
133+
func (w wrappedString) String() string {
134+
return w.value
135+
}
136+
137+
type CachedExecutor struct {
138+
executor abi.Executor
139+
cache *JsonTlbDiskCache[wrappedString, CachedExecResult]
140+
}
141+
142+
func (ce *CachedExecutor) RunSmcMethodByID(ctx context.Context, accountID ton.AccountID, methodID int, params tlb.VmStack) (uint32, tlb.VmStack, error) {
143+
argHash := sha256.New()
144+
argHash.Write([]byte(accountID.String()))
145+
argHash.Write([]byte(strconv.Itoa(methodID)))
146+
paramsBytes, err := params.MarshalTL()
147+
if err != nil {
148+
return 0, tlb.VmStack{}, err
149+
}
150+
argHash.Write(paramsBytes)
151+
argHashStr := hex.EncodeToString(argHash.Sum(nil))
152+
result, found := ce.cache.Load(wrappedString{argHashStr})
153+
if found {
154+
return result.Code, result.Stack, nil
155+
}
156+
code, stack, err := ce.executor.RunSmcMethodByID(ctx, accountID, methodID, params)
157+
if err != nil {
158+
return code, stack, err
159+
}
160+
ce.cache.Store(wrappedString{argHashStr}, CachedExecResult{Code: code, Stack: stack})
161+
return code, stack, err
162+
}
163+
164+
type Tuple2[V1, V2 any] struct {
165+
V1 V1
166+
V2 V2
167+
}

0 commit comments

Comments
 (0)