Skip to content
This repository was archived by the owner on Jan 20, 2026. It is now read-only.

Commit 5772e15

Browse files
authored
Add store tracer (#579)
## Describe your changes and provide context Add a tracer that can dump all relevant prestate in the db for a traced tx, which can then be used to construct past-scenario test cases. ## Testing performed to validate your change unit test
1 parent f575589 commit 5772e15

7 files changed

Lines changed: 275 additions & 26 deletions

File tree

store/gaskv/store.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,35 @@ import (
88
"github.com/cosmos/cosmos-sdk/telemetry"
99
)
1010

11+
type IStoreTracer interface {
12+
Get([]byte, []byte, string)
13+
Has([]byte, string)
14+
Set([]byte, []byte, string)
15+
Delete([]byte, string)
16+
DerivePrestateToJson() []byte
17+
Clear()
18+
}
19+
1120
var _ types.KVStore = &Store{}
1221

1322
// Store applies gas tracking to an underlying KVStore. It implements the
1423
// KVStore interface.
1524
type Store struct {
16-
gasMeter types.GasMeter
17-
gasConfig types.GasConfig
18-
parent types.KVStore
25+
gasMeter types.GasMeter
26+
gasConfig types.GasConfig
27+
parent types.KVStore
28+
moduleName string
29+
tracer IStoreTracer
1930
}
2031

2132
// NewStore returns a reference to a new GasKVStore.
22-
func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig types.GasConfig) *Store {
33+
func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig types.GasConfig, moduleName string, tracer IStoreTracer) *Store {
2334
kvs := &Store{
24-
gasMeter: gasMeter,
25-
gasConfig: gasConfig,
26-
parent: parent,
35+
gasMeter: gasMeter,
36+
gasConfig: gasConfig,
37+
parent: parent,
38+
moduleName: moduleName,
39+
tracer: tracer,
2740
}
2841
return kvs
2942
}
@@ -45,6 +58,9 @@ func (gs *Store) Get(key []byte) (value []byte) {
4558
// TODO overflow-safe math?
4659
gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasReadPerByteDesc)
4760
gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasReadPerByteDesc)
61+
if gs.tracer != nil {
62+
gs.tracer.Get(key, value, gs.moduleName)
63+
}
4864

4965
return value
5066
}
@@ -64,7 +80,11 @@ func (gs *Store) Set(key []byte, value []byte) {
6480
func (gs *Store) Has(key []byte) bool {
6581
defer telemetry.MeasureSince(time.Now(), "store", "gaskv", "has")
6682
gs.gasMeter.ConsumeGas(gs.gasConfig.HasCost, types.GasHasDesc)
67-
return gs.parent.Has(key)
83+
res := gs.parent.Has(key)
84+
if gs.tracer != nil && res {
85+
gs.tracer.Has(key, gs.moduleName)
86+
}
87+
return res
6888
}
6989

7090
// Implements KVStore.
@@ -113,7 +133,7 @@ func (gs *Store) iterator(start, end []byte, ascending bool) types.Iterator {
113133
parent = gs.parent.ReverseIterator(start, end)
114134
}
115135

116-
gi := newGasIterator(gs.gasMeter, gs.gasConfig, parent)
136+
gi := newGasIterator(gs.gasMeter, gs.gasConfig, parent, gs.moduleName, gs.tracer)
117137
defer func() {
118138
if err := recover(); err != nil {
119139
// if there is a panic, we close the iterator then reraise
@@ -139,16 +159,20 @@ func (gs *Store) GetAllKeyStrsInRange(start, end []byte) (res []string) {
139159
}
140160

141161
type gasIterator struct {
142-
gasMeter types.GasMeter
143-
gasConfig types.GasConfig
144-
parent types.Iterator
162+
gasMeter types.GasMeter
163+
gasConfig types.GasConfig
164+
parent types.Iterator
165+
moduleName string
166+
tracer IStoreTracer
145167
}
146168

147-
func newGasIterator(gasMeter types.GasMeter, gasConfig types.GasConfig, parent types.Iterator) types.Iterator {
169+
func newGasIterator(gasMeter types.GasMeter, gasConfig types.GasConfig, parent types.Iterator, moduleName string, tracer IStoreTracer) types.Iterator {
148170
return &gasIterator{
149-
gasMeter: gasMeter,
150-
gasConfig: gasConfig,
151-
parent: parent,
171+
gasMeter: gasMeter,
172+
gasConfig: gasConfig,
173+
parent: parent,
174+
moduleName: moduleName,
175+
tracer: tracer,
152176
}
153177
}
154178

@@ -174,13 +198,19 @@ func (gi *gasIterator) Next() {
174198
// not incur any gas cost.
175199
func (gi *gasIterator) Key() (key []byte) {
176200
key = gi.parent.Key()
201+
if gi.tracer != nil {
202+
gi.tracer.Has(key, gi.moduleName)
203+
}
177204
return key
178205
}
179206

180207
// Value implements the Iterator interface. It returns the current value and it
181208
// does not incur any gas cost.
182209
func (gi *gasIterator) Value() (value []byte) {
183210
value = gi.parent.Value()
211+
if gi.tracer != nil {
212+
gi.tracer.Get(gi.Key(), value, gi.moduleName)
213+
}
184214
return value
185215
}
186216

store/gaskv/store_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func valFmt(i int) []byte { return bz(fmt.Sprintf("value%0.8d", i)) }
2020
func TestGasKVStoreBasic(t *testing.T) {
2121
mem := dbadapter.Store{DB: dbm.NewMemDB()}
2222
meter := types.NewMultiplierGasMeter(10000, 1, 1)
23-
st := gaskv.NewStore(mem, meter, types.KVGasConfig())
23+
st := gaskv.NewStore(mem, meter, types.KVGasConfig(), "", nil)
2424

2525
require.Equal(t, types.StoreTypeDB, st.GetStoreType())
2626
require.Panics(t, func() { st.CacheWrap(nil) })
@@ -41,7 +41,7 @@ func TestGasKVStoreBasic(t *testing.T) {
4141
func TestGasKVStoreIterator(t *testing.T) {
4242
mem := dbadapter.Store{DB: dbm.NewMemDB()}
4343
meter := types.NewMultiplierGasMeter(100000, 1, 1)
44-
st := gaskv.NewStore(mem, meter, types.KVGasConfig())
44+
st := gaskv.NewStore(mem, meter, types.KVGasConfig(), "", nil)
4545
require.False(t, st.Has(keyFmt(1)))
4646
require.Empty(t, st.Get(keyFmt(1)), "Expected `key1` to be empty")
4747
require.Empty(t, st.Get(keyFmt(2)), "Expected `key2` to be empty")
@@ -109,14 +109,14 @@ func TestGasKVStoreIterator(t *testing.T) {
109109
func TestGasKVStoreOutOfGasSet(t *testing.T) {
110110
mem := dbadapter.Store{DB: dbm.NewMemDB()}
111111
meter := types.NewMultiplierGasMeter(0, 1, 1)
112-
st := gaskv.NewStore(mem, meter, types.KVGasConfig())
112+
st := gaskv.NewStore(mem, meter, types.KVGasConfig(), "", nil)
113113
require.Panics(t, func() { st.Set(keyFmt(1), valFmt(1)) }, "Expected out-of-gas")
114114
}
115115

116116
func TestGasKVStoreOutOfGasIterator(t *testing.T) {
117117
mem := dbadapter.Store{DB: dbm.NewMemDB()}
118118
meter := types.NewMultiplierGasMeter(20000, 1, 1)
119-
st := gaskv.NewStore(mem, meter, types.KVGasConfig())
119+
st := gaskv.NewStore(mem, meter, types.KVGasConfig(), "", nil)
120120
st.Set(keyFmt(1), valFmt(1))
121121
iterator := st.Iterator(nil, nil)
122122
iterator.Next()

store/prefix/store_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func TestIAVLStorePrefix(t *testing.T) {
100100
func TestPrefixKVStoreNoNilSet(t *testing.T) {
101101
meter := types.NewMultiplierGasMeter(100000000, 1, 1)
102102
mem := dbadapter.Store{DB: dbm.NewMemDB()}
103-
gasStore := gaskv.NewStore(mem, meter, types.KVGasConfig())
103+
gasStore := gaskv.NewStore(mem, meter, types.KVGasConfig(), "", nil)
104104
require.Panics(t, func() { gasStore.Set([]byte("key"), nil) }, "setting a nil value should panic")
105105
}
106106

types/context.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ type Context struct {
7070

7171
traceSpanContext context.Context
7272

73-
isTracing bool
73+
isTracing bool
74+
storeTracer gaskv.IStoreTracer
7475
}
7576

7677
// Proposed rename, not done to avoid API breakage
@@ -231,6 +232,13 @@ func (c Context) IsTracing() bool {
231232
return c.isTracing
232233
}
233234

235+
func (c Context) StoreTracer() gaskv.IStoreTracer {
236+
if c.storeTracer == nil {
237+
return nil
238+
}
239+
return c.storeTracer
240+
}
241+
234242
// WithEventManager returns a Context with an updated tx priority
235243
func (c Context) WithPriority(p int64) Context {
236244
c.priority = p
@@ -504,6 +512,9 @@ func (c Context) WithExpireTxHandler(expireTxHandler func()) Context {
504512

505513
func (c Context) WithIsTracing(it bool) Context {
506514
c.isTracing = it
515+
if it {
516+
c.storeTracer = NewStoreTracer()
517+
}
507518
return c
508519
}
509520

@@ -554,20 +565,20 @@ func (c Context) Value(key interface{}) interface{} {
554565
func (c Context) KVStore(key StoreKey) KVStore {
555566
if c.isTracing {
556567
if _, ok := c.nextStoreKeys[key.Name()]; ok {
557-
return gaskv.NewStore(c.nextMs.GetKVStore(key), c.GasMeter(), stypes.KVGasConfig())
568+
return gaskv.NewStore(c.nextMs.GetKVStore(key), c.GasMeter(), stypes.KVGasConfig(), key.Name(), c.StoreTracer())
558569
}
559570
}
560-
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.KVGasConfig())
571+
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.KVGasConfig(), key.Name(), c.StoreTracer())
561572
}
562573

563574
// TransientStore fetches a TransientStore from the MultiStore.
564575
func (c Context) TransientStore(key StoreKey) KVStore {
565576
if c.isTracing {
566577
if _, ok := c.nextStoreKeys[key.Name()]; ok {
567-
return gaskv.NewStore(c.nextMs.GetKVStore(key), c.GasMeter(), stypes.TransientGasConfig())
578+
return gaskv.NewStore(c.nextMs.GetKVStore(key), c.GasMeter(), stypes.TransientGasConfig(), key.Name(), c.StoreTracer())
568579
}
569580
}
570-
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.TransientGasConfig())
581+
return gaskv.NewStore(c.MultiStore().GetKVStore(key), c.GasMeter(), stypes.TransientGasConfig(), key.Name(), c.StoreTracer())
571582
}
572583

573584
// CacheContext returns a new Context with the multi-store cached and a new

types/tracer.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package types
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"sync"
7+
)
8+
9+
type StoreTracer struct {
10+
Modules map[string]*ModuleTrace
11+
mu *sync.Mutex
12+
}
13+
14+
type ModuleTrace struct {
15+
Accesses []Access
16+
}
17+
18+
type Access struct {
19+
Op OpType
20+
Key []byte
21+
Value []byte
22+
}
23+
24+
type OpType int
25+
26+
const (
27+
Get OpType = iota
28+
Has
29+
Set
30+
Delete
31+
)
32+
33+
func NewStoreTracer() *StoreTracer {
34+
return &StoreTracer{
35+
Modules: map[string]*ModuleTrace{},
36+
mu: &sync.Mutex{},
37+
}
38+
}
39+
40+
func (st *StoreTracer) Get(key []byte, value []byte, module string) {
41+
st.mu.Lock()
42+
defer st.mu.Unlock()
43+
accesses := st.getOrSetModuleTrace(module)
44+
accesses.Accesses = append(accesses.Accesses, Access{
45+
Op: Get,
46+
Key: key,
47+
Value: value,
48+
})
49+
}
50+
51+
func (st *StoreTracer) Set(key []byte, value []byte, module string) {
52+
st.mu.Lock()
53+
defer st.mu.Unlock()
54+
accesses := st.getOrSetModuleTrace(module)
55+
accesses.Accesses = append(accesses.Accesses, Access{
56+
Op: Set,
57+
Key: key,
58+
Value: value,
59+
})
60+
}
61+
62+
func (st *StoreTracer) Has(key []byte, module string) {
63+
st.mu.Lock()
64+
defer st.mu.Unlock()
65+
accesses := st.getOrSetModuleTrace(module)
66+
accesses.Accesses = append(accesses.Accesses, Access{
67+
Op: Has,
68+
Key: key,
69+
})
70+
}
71+
72+
func (st *StoreTracer) Delete(key []byte, module string) {
73+
st.mu.Lock()
74+
defer st.mu.Unlock()
75+
accesses := st.getOrSetModuleTrace(module)
76+
accesses.Accesses = append(accesses.Accesses, Access{
77+
Op: Delete,
78+
Key: key,
79+
})
80+
}
81+
82+
func (st *StoreTracer) getOrSetModuleTrace(module string) (mt *ModuleTrace) {
83+
if _, ok := st.Modules[module]; !ok {
84+
mt = &ModuleTrace{
85+
Accesses: []Access{},
86+
}
87+
st.Modules[module] = mt
88+
} else {
89+
mt = st.Modules[module]
90+
}
91+
return
92+
}
93+
94+
func (st *StoreTracer) Clear() {
95+
st.mu.Lock()
96+
defer st.mu.Unlock()
97+
st.Modules = map[string]*ModuleTrace{}
98+
}
99+
100+
type StoreTraceDump struct {
101+
Modules map[string]ModuleTraceDump `json:"modules"`
102+
}
103+
104+
type ModuleTraceDump struct {
105+
Reads map[string]string `json:"reads"`
106+
Has []string `json:"has"`
107+
}
108+
109+
func (st *StoreTracer) DerivePrestateToJson() []byte {
110+
st.mu.Lock()
111+
defer st.mu.Unlock()
112+
d := StoreTraceDump{
113+
Modules: make(map[string]ModuleTraceDump, len(st.Modules)),
114+
}
115+
for name, module := range st.Modules {
116+
mtd := ModuleTraceDump{
117+
Reads: make(map[string]string),
118+
Has: []string{},
119+
}
120+
// any read for key XYZ after a Set/Delete to XYZ is discarded
121+
// because the result doesn't represent prestate.
122+
writtenKey := map[string]struct{}{}
123+
hasMap := map[string]struct{}{}
124+
for _, a := range module.Accesses {
125+
switch a.Op {
126+
case Get:
127+
if _, ok := writtenKey[string(a.Key)]; ok {
128+
continue
129+
}
130+
// no need to check if it's already in dump because the value
131+
// must be the same if there is no preceding write
132+
mtd.Reads[hex.EncodeToString(a.Key)] = hex.EncodeToString(a.Value)
133+
case Has:
134+
if _, ok := writtenKey[string(a.Key)]; ok {
135+
continue
136+
}
137+
hasMap[hex.EncodeToString(a.Key)] = struct{}{}
138+
default:
139+
writtenKey[string(a.Key)] = struct{}{}
140+
}
141+
}
142+
for k := range hasMap {
143+
mtd.Has = append(mtd.Has, k)
144+
}
145+
d.Modules[name] = mtd
146+
}
147+
bz, err := json.Marshal(&d)
148+
if err != nil {
149+
panic(err)
150+
}
151+
return bz
152+
}

x/bank/keeper/keeper.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type Keeper interface {
5353
UndelegateCoins(ctx sdk.Context, moduleAccAddr, delegatorAddr sdk.AccAddress, amt sdk.Coins) error
5454

5555
GetStoreKey() sdk.StoreKey
56+
GetCdc() codec.BinaryCodec
5657

5758
types.QueryServer
5859
}
@@ -684,6 +685,11 @@ func (k BaseKeeper) GetStoreKey() sdk.StoreKey {
684685
return k.storeKey
685686
}
686687

688+
// for testing
689+
func (k BaseKeeper) GetCdc() codec.BinaryCodec {
690+
return k.cdc
691+
}
692+
687693
// IterateTotalSupply iterates over the total supply calling the given cb (callback) function
688694
// with the balance of each coin.
689695
// The iteration stops if the callback returns true.

0 commit comments

Comments
 (0)