Skip to content

Commit 51b15af

Browse files
authored
feat(statesync): implement statesync spec for the new approach (#663)
* feat: add ordered_map.go * feat: introduce a new approach of a state sync * refactor: modify kvstore to be compatible with a new statesync approach
1 parent 6cf43f7 commit 51b15af

File tree

25 files changed

+2355
-2168
lines changed

25 files changed

+2355
-2168
lines changed

abci/example/kvstore/kvstore.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ func (app *Application) LoadSnapshotChunk(_ context.Context, req *abci.RequestLo
493493
app.mu.Lock()
494494
defer app.mu.Unlock()
495495

496-
chunk, err := app.snapshots.LoadChunk(req.Height, req.Format, req.Chunk)
496+
chunk, err := app.snapshots.LoadChunk(req.Height, req.Version, req.ChunkId)
497497
if err != nil {
498498
return &abci.ResponseLoadSnapshotChunk{}, err
499499
}
@@ -523,7 +523,11 @@ func (app *Application) ApplySnapshotChunk(_ context.Context, req *abci.RequestA
523523
if app.offerSnapshot == nil {
524524
return &abci.ResponseApplySnapshotChunk{}, fmt.Errorf("no restore in progress")
525525
}
526-
app.offerSnapshot.addChunk(int(req.Index), req.Chunk)
526+
527+
resp := &abci.ResponseApplySnapshotChunk{
528+
Result: abci.ResponseApplySnapshotChunk_ACCEPT,
529+
NextChunks: app.offerSnapshot.addChunk(req.ChunkId, req.Chunk),
530+
}
527531

528532
if app.offerSnapshot.isFull() {
529533
chunks := app.offerSnapshot.bytes()
@@ -538,11 +542,10 @@ func (app *Application) ApplySnapshotChunk(_ context.Context, req *abci.RequestA
538542
"snapshot_height", app.offerSnapshot.snapshot.Height,
539543
"snapshot_apphash", app.offerSnapshot.appHash,
540544
)
545+
resp.Result = abci.ResponseApplySnapshotChunk_COMPLETE_SNAPSHOT
541546
app.offerSnapshot = nil
542547
}
543548

544-
resp := &abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}
545-
546549
app.logger.Debug("ApplySnapshotChunk", "resp", resp)
547550
return resp, nil
548551
}
@@ -556,7 +559,9 @@ func (app *Application) createSnapshot() error {
556559
if err != nil {
557560
return fmt.Errorf("create snapshot: %w", err)
558561
}
559-
app.logger.Info("created state sync snapshot", "height", height, "apphash", app.LastCommittedState.GetAppHash())
562+
app.logger.Info("created state sync snapshot",
563+
"height", height,
564+
"apphash", app.LastCommittedState.GetAppHash())
560565
err = app.snapshots.Prune(maxSnapshotCount)
561566
if err != nil {
562567
return fmt.Errorf("prune snapshots: %w", err)

abci/example/kvstore/kvstore_test.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -493,24 +493,20 @@ func TestSnapshots(t *testing.T) {
493493
})
494494
require.NoError(t, err)
495495
assert.Equal(t, types.ResponseOfferSnapshot_ACCEPT, respOffer.Result)
496+
loaded, err := app.LoadSnapshotChunk(ctx, &types.RequestLoadSnapshotChunk{
497+
Height: recentSnapshot.Height,
498+
ChunkId: recentSnapshot.Hash,
499+
Version: recentSnapshot.Version,
500+
})
501+
require.NoError(t, err)
496502

497-
for chunk := uint32(0); chunk < recentSnapshot.Chunks; chunk++ {
498-
loaded, err := app.LoadSnapshotChunk(ctx, &types.RequestLoadSnapshotChunk{
499-
Height: recentSnapshot.Height,
500-
Chunk: chunk,
501-
Format: recentSnapshot.Format,
502-
})
503-
require.NoError(t, err)
504-
505-
applied, err := dstApp.ApplySnapshotChunk(ctx, &types.RequestApplySnapshotChunk{
506-
Index: chunk,
507-
Chunk: loaded.Chunk,
508-
Sender: "app",
509-
})
510-
require.NoError(t, err)
511-
assert.Equal(t, types.ResponseApplySnapshotChunk_ACCEPT, applied.Result)
512-
}
513-
503+
applied, err := dstApp.ApplySnapshotChunk(ctx, &types.RequestApplySnapshotChunk{
504+
ChunkId: recentSnapshot.Hash,
505+
Chunk: loaded.Chunk,
506+
Sender: "app",
507+
})
508+
require.NoError(t, err)
509+
assert.Equal(t, types.ResponseApplySnapshotChunk_COMPLETE_SNAPSHOT, applied.Result)
514510
infoResp, err := dstApp.Info(ctx, &types.RequestInfo{})
515511
require.NoError(t, err)
516512
assertRespInfo(t, int64(recentSnapshot.Height), appHashes[snapshotHeight], *infoResp)

abci/example/kvstore/snapshots.go

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package kvstore
33

44
import (
55
"bytes"
6+
"encoding/hex"
67
"encoding/json"
78
"errors"
89
"fmt"
9-
"math"
1010
"os"
1111
"path/filepath"
1212

@@ -15,6 +15,7 @@ import (
1515
abci "github.com/tendermint/tendermint/abci/types"
1616
"github.com/tendermint/tendermint/crypto"
1717
tmbytes "github.com/tendermint/tendermint/libs/bytes"
18+
"github.com/tendermint/tendermint/libs/ds"
1819
)
1920

2021
const (
@@ -27,11 +28,17 @@ const (
2728
// SnapshotStore stores state sync snapshots. Snapshots are stored simply as
2829
// JSON files, and chunks are generated on-the-fly by splitting the JSON data
2930
// into fixed-size chunks.
30-
type SnapshotStore struct {
31-
sync.RWMutex
32-
dir string
33-
metadata []abci.Snapshot
34-
}
31+
type (
32+
SnapshotStore struct {
33+
sync.RWMutex
34+
dir string
35+
metadata []abci.Snapshot
36+
}
37+
chunkItem struct {
38+
Data []byte `json:"data"`
39+
NextChunkIDs [][]byte `json:"nextChunkIDs"`
40+
}
41+
)
3542

3643
// NewSnapshotStore creates a new snapshot store.
3744
func NewSnapshotStore(dir string) (*SnapshotStore, error) {
@@ -49,7 +56,7 @@ func NewSnapshotStore(dir string) (*SnapshotStore, error) {
4956
// called internally on construction.
5057
func (s *SnapshotStore) loadMetadata() error {
5158
file := filepath.Join(s.dir, "metadata.json")
52-
metadata := []abci.Snapshot{}
59+
var metadata []abci.Snapshot
5360

5461
bz, err := os.ReadFile(file)
5562
switch {
@@ -96,10 +103,9 @@ func (s *SnapshotStore) Create(state State) (abci.Snapshot, error) {
96103
}
97104
height := state.GetHeight()
98105
snapshot := abci.Snapshot{
99-
Height: uint64(height),
100-
Format: 1,
101-
Hash: crypto.Checksum(bz),
102-
Chunks: byteChunks(bz),
106+
Height: uint64(height),
107+
Version: 1,
108+
Hash: crypto.Checksum(bz),
103109
}
104110
err = os.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)), bz, 0644)
105111
if err != nil {
@@ -152,16 +158,18 @@ func (s *SnapshotStore) List() ([]*abci.Snapshot, error) {
152158
}
153159

154160
// LoadChunk loads a snapshot chunk.
155-
func (s *SnapshotStore) LoadChunk(height uint64, format uint32, chunk uint32) ([]byte, error) {
161+
func (s *SnapshotStore) LoadChunk(height uint64, version uint32, chunkID []byte) ([]byte, error) {
156162
s.RLock()
157163
defer s.RUnlock()
158164
for _, snapshot := range s.metadata {
159-
if snapshot.Height == height && snapshot.Format == format {
160-
bz, err := os.ReadFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)))
165+
if snapshot.Height == height && snapshot.Version == version {
166+
bz, err := os.ReadFile(filepath.Join(s.dir, fmt.Sprintf("%d.json", height)))
161167
if err != nil {
162168
return nil, err
163169
}
164-
return byteChunk(bz, chunk), nil
170+
chunks := makeChunks(bz, snapshotChunkSize)
171+
item := makeChunkItem(chunks, chunkID)
172+
return json.Marshal(item)
165173
}
166174
}
167175
return nil, nil
@@ -170,54 +178,79 @@ func (s *SnapshotStore) LoadChunk(height uint64, format uint32, chunk uint32) ([
170178
type offerSnapshot struct {
171179
snapshot *abci.Snapshot
172180
appHash tmbytes.HexBytes
173-
chunks [][]byte
174-
chunkCnt int
181+
chunks *ds.OrderedMap[string, []byte]
175182
}
176183

177184
func newOfferSnapshot(snapshot *abci.Snapshot, appHash tmbytes.HexBytes) *offerSnapshot {
178185
return &offerSnapshot{
179186
snapshot: snapshot,
180187
appHash: appHash,
181-
chunks: make([][]byte, snapshot.Chunks),
182-
chunkCnt: 0,
188+
chunks: ds.NewOrderedMap[string, []byte](),
183189
}
184190
}
185191

186-
func (s *offerSnapshot) addChunk(index int, chunk []byte) {
187-
if s.chunks[index] != nil {
188-
return
192+
func (s *offerSnapshot) addChunk(chunkID tmbytes.HexBytes, data []byte) [][]byte {
193+
chunkIDStr := chunkID.String()
194+
if s.chunks.Has(chunkIDStr) {
195+
return nil
189196
}
190-
s.chunks[index] = chunk
191-
s.chunkCnt++
197+
var item chunkItem
198+
err := json.Unmarshal(data, &item)
199+
if err != nil {
200+
panic("failed to decode a chunk data: " + err.Error())
201+
}
202+
s.chunks.Put(chunkIDStr, item.Data)
203+
return item.NextChunkIDs
192204
}
193205

194206
func (s *offerSnapshot) isFull() bool {
195-
return s.chunkCnt == int(s.snapshot.Chunks)
207+
return bytes.Equal(crypto.Checksum(s.bytes()), s.snapshot.Hash)
196208
}
197209

198210
func (s *offerSnapshot) bytes() []byte {
211+
chunks := s.chunks.Values()
199212
buf := bytes.NewBuffer(nil)
200-
for _, chunk := range s.chunks {
213+
for _, chunk := range chunks {
201214
buf.Write(chunk)
202215
}
203216
return buf.Bytes()
204217
}
205218

206-
// byteChunk returns the chunk at a given index from the full byte slice.
207-
func byteChunk(bz []byte, index uint32) []byte {
208-
start := int(index * snapshotChunkSize)
209-
end := int((index + 1) * snapshotChunkSize)
210-
switch {
211-
case start >= len(bz):
212-
return nil
213-
case end >= len(bz):
214-
return bz[start:]
215-
default:
216-
return bz[start:end]
219+
// makeChunkItem returns the chunk at a given index from the full byte slice.
220+
func makeChunkItem(chunks *ds.OrderedMap[string, []byte], chunkID []byte) chunkItem {
221+
chunkIDStr := hex.EncodeToString(chunkID)
222+
val, ok := chunks.Get(chunkIDStr)
223+
if !ok {
224+
panic("chunk not found")
217225
}
226+
chunkIDs := chunks.Keys()
227+
ci := chunkItem{Data: val}
228+
i := 0
229+
for ; i < len(chunkIDs) && chunkIDs[i] != chunkIDStr; i++ {
230+
}
231+
if i+1 < len(chunkIDs) {
232+
data, err := hex.DecodeString(chunkIDs[i+1])
233+
if err != nil {
234+
panic(err)
235+
}
236+
ci.NextChunkIDs = [][]byte{data}
237+
}
238+
return ci
218239
}
219240

220-
// byteChunks calculates the number of chunks in the byte slice.
221-
func byteChunks(bz []byte) uint32 {
222-
return uint32(math.Ceil(float64(len(bz)) / snapshotChunkSize))
241+
func makeChunks(bz []byte, chunkSize int) *ds.OrderedMap[string, []byte] {
242+
chunks := ds.NewOrderedMap[string, []byte]()
243+
totalHash := hex.EncodeToString(crypto.Checksum(bz))
244+
key := totalHash
245+
for i := 0; i < len(bz); i += chunkSize {
246+
j := i + chunkSize
247+
if j > len(bz) {
248+
j = len(bz)
249+
}
250+
if i > 1 {
251+
key = hex.EncodeToString(crypto.Checksum(bz[i:j]))
252+
}
253+
chunks.Put(key, append([]byte(nil), bz[i:j]...))
254+
}
255+
return chunks
223256
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package kvstore
2+
3+
import (
4+
"encoding/hex"
5+
"math/rand"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestChunkItem(t *testing.T) {
12+
const size = 64
13+
chunks := makeChunks(makeBytes(1032), size)
14+
keys := chunks.Keys()
15+
values := chunks.Values()
16+
for i, key := range keys {
17+
chunkID, err := hex.DecodeString(key)
18+
require.NoError(t, err)
19+
item := makeChunkItem(chunks, chunkID)
20+
require.Equal(t, values[i], item.Data)
21+
if i+1 < len(keys) {
22+
nextChunkID, err := hex.DecodeString(keys[i+1])
23+
require.NoError(t, err)
24+
require.Equal(t, [][]byte{nextChunkID}, item.NextChunkIDs)
25+
} else {
26+
require.Nil(t, item.NextChunkIDs)
27+
}
28+
}
29+
}
30+
31+
func makeBytes(size int) []byte {
32+
bz := make([]byte, size)
33+
for i := 0; i < size; i++ {
34+
bz[i] = byte(rand.Int63n(256))
35+
}
36+
return bz
37+
}

0 commit comments

Comments
 (0)