348 lines
12 KiB
Go
348 lines
12 KiB
Go
package baseapp_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
abci "github.com/cometbft/cometbft/api/cometbft/abci/v1"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
pruningtypes "cosmossdk.io/store/pruning/types"
|
|
snapshottypes "cosmossdk.io/store/snapshots/types"
|
|
)
|
|
|
|
func TestABCI_ListSnapshots(t *testing.T) {
|
|
ssCfg := SnapshotsConfig{
|
|
blocks: 5,
|
|
blockTxs: 4,
|
|
snapshotInterval: 2,
|
|
snapshotKeepRecent: 2,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
}
|
|
|
|
suite := NewBaseAppSuiteWithSnapshots(t, ssCfg)
|
|
|
|
resp, err := suite.baseApp.ListSnapshots(&abci.ListSnapshotsRequest{})
|
|
require.NoError(t, err)
|
|
for _, s := range resp.Snapshots {
|
|
require.NotEmpty(t, s.Hash)
|
|
require.NotEmpty(t, s.Metadata)
|
|
|
|
s.Hash = nil
|
|
s.Metadata = nil
|
|
}
|
|
|
|
require.Equal(t, &abci.ListSnapshotsResponse{Snapshots: []*abci.Snapshot{
|
|
{Height: 4, Format: snapshottypes.CurrentFormat, Chunks: 2},
|
|
{Height: 2, Format: snapshottypes.CurrentFormat, Chunks: 1},
|
|
}}, resp)
|
|
}
|
|
|
|
func TestABCI_SnapshotWithPruning(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
ssCfg SnapshotsConfig
|
|
expectedSnapshots []*abci.Snapshot
|
|
}{
|
|
"prune nothing with snapshot": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 20,
|
|
blockTxs: 2,
|
|
snapshotInterval: 5,
|
|
snapshotKeepRecent: 1,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{
|
|
{Height: 20, Format: snapshottypes.CurrentFormat, Chunks: 5},
|
|
},
|
|
},
|
|
"prune everything with snapshot": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 20,
|
|
blockTxs: 2,
|
|
snapshotInterval: 5,
|
|
snapshotKeepRecent: 1,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningEverything),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{
|
|
{Height: 20, Format: snapshottypes.CurrentFormat, Chunks: 5},
|
|
},
|
|
},
|
|
"default pruning with snapshot": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 20,
|
|
blockTxs: 2,
|
|
snapshotInterval: 5,
|
|
snapshotKeepRecent: 1,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningDefault),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{
|
|
{Height: 20, Format: snapshottypes.CurrentFormat, Chunks: 5},
|
|
},
|
|
},
|
|
"custom": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 25,
|
|
blockTxs: 2,
|
|
snapshotInterval: 5,
|
|
snapshotKeepRecent: 2,
|
|
pruningOpts: pruningtypes.NewCustomPruningOptions(12, 12),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{
|
|
{Height: 25, Format: snapshottypes.CurrentFormat, Chunks: 6},
|
|
{Height: 20, Format: snapshottypes.CurrentFormat, Chunks: 5},
|
|
},
|
|
},
|
|
"no snapshots": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 10,
|
|
blockTxs: 2,
|
|
snapshotInterval: 0, // 0 implies disable snapshots
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{},
|
|
},
|
|
"keep all snapshots": {
|
|
ssCfg: SnapshotsConfig{
|
|
blocks: 10,
|
|
blockTxs: 2,
|
|
snapshotInterval: 3,
|
|
snapshotKeepRecent: 0, // 0 implies keep all snapshots
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
},
|
|
expectedSnapshots: []*abci.Snapshot{
|
|
{Height: 9, Format: snapshottypes.CurrentFormat, Chunks: 2},
|
|
{Height: 6, Format: snapshottypes.CurrentFormat, Chunks: 2},
|
|
{Height: 3, Format: snapshottypes.CurrentFormat, Chunks: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
suite := NewBaseAppSuiteWithSnapshots(t, tc.ssCfg)
|
|
|
|
resp, err := suite.baseApp.ListSnapshots(&abci.ListSnapshotsRequest{})
|
|
require.NoError(t, err)
|
|
for _, s := range resp.Snapshots {
|
|
require.NotEmpty(t, s.Hash)
|
|
require.NotEmpty(t, s.Metadata)
|
|
|
|
s.Hash = nil
|
|
s.Metadata = nil
|
|
}
|
|
|
|
require.Equal(t, &abci.ListSnapshotsResponse{Snapshots: tc.expectedSnapshots}, resp)
|
|
|
|
// Validate that heights were pruned correctly by querying the state at the last height that should be present relative to latest
|
|
// and the first height that should be pruned.
|
|
//
|
|
// Exceptions:
|
|
// * Prune nothing: should be able to query all heights (we only test first and latest)
|
|
// * Prune default: should be able to query all heights (we only test first and latest)
|
|
// * The reason for default behaving this way is that we only commit 20 heights but default has 100_000 keep-recent
|
|
var lastExistingHeight int64
|
|
if tc.ssCfg.pruningOpts.GetPruningStrategy() == pruningtypes.PruningNothing || tc.ssCfg.pruningOpts.GetPruningStrategy() == pruningtypes.PruningDefault {
|
|
lastExistingHeight = 1
|
|
} else {
|
|
// Integer division rounds down so by multiplying back we get the last height at which we pruned
|
|
lastExistingHeight = int64((tc.ssCfg.blocks/tc.ssCfg.pruningOpts.Interval)*tc.ssCfg.pruningOpts.Interval - tc.ssCfg.pruningOpts.KeepRecent)
|
|
}
|
|
|
|
// Query 1
|
|
res, err := suite.baseApp.Query(context.TODO(), &abci.QueryRequest{Path: fmt.Sprintf("/store/%s/key", capKey2.Name()), Data: []byte("0"), Height: lastExistingHeight})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res, "height: %d", lastExistingHeight)
|
|
require.NotNil(t, res.Value, "height: %d", lastExistingHeight)
|
|
|
|
// Query 2
|
|
res, err = suite.baseApp.Query(context.TODO(), &abci.QueryRequest{Path: fmt.Sprintf("/store/%s/key", capKey2.Name()), Data: []byte("0"), Height: lastExistingHeight - 1})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res, "height: %d", lastExistingHeight-1)
|
|
|
|
if tc.ssCfg.pruningOpts.GetPruningStrategy() == pruningtypes.PruningNothing || tc.ssCfg.pruningOpts.GetPruningStrategy() == pruningtypes.PruningDefault {
|
|
// With prune nothing or default, we query height 0 which translates to the latest height.
|
|
require.NotNil(t, res.Value, "height: %d", lastExistingHeight-1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestABCI_LoadSnapshotChunk(t *testing.T) {
|
|
ssCfg := SnapshotsConfig{
|
|
blocks: 2,
|
|
blockTxs: 5,
|
|
snapshotInterval: 2,
|
|
snapshotKeepRecent: snapshottypes.CurrentFormat,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
}
|
|
suite := NewBaseAppSuiteWithSnapshots(t, ssCfg)
|
|
|
|
testCases := map[string]struct {
|
|
height uint64
|
|
format uint32
|
|
chunk uint32
|
|
expectEmpty bool
|
|
}{
|
|
"Existing snapshot": {2, snapshottypes.CurrentFormat, 1, false},
|
|
"Missing height": {100, snapshottypes.CurrentFormat, 1, true},
|
|
"Missing format": {2, snapshottypes.CurrentFormat + 1, 1, true},
|
|
"Missing chunk": {2, snapshottypes.CurrentFormat, 9, true},
|
|
"Zero height": {0, snapshottypes.CurrentFormat, 1, true},
|
|
"Zero format": {2, 0, 1, true},
|
|
"Zero chunk": {2, snapshottypes.CurrentFormat, 0, false},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
resp, _ := suite.baseApp.LoadSnapshotChunk(&abci.LoadSnapshotChunkRequest{
|
|
Height: tc.height,
|
|
Format: tc.format,
|
|
Chunk: tc.chunk,
|
|
})
|
|
if tc.expectEmpty {
|
|
require.Equal(t, &abci.LoadSnapshotChunkResponse{}, resp)
|
|
return
|
|
}
|
|
|
|
require.NotEmpty(t, resp.Chunk)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestABCI_OfferSnapshot_Errors(t *testing.T) {
|
|
ssCfg := SnapshotsConfig{
|
|
blocks: 0,
|
|
blockTxs: 0,
|
|
snapshotInterval: 2,
|
|
snapshotKeepRecent: 2,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
}
|
|
suite := NewBaseAppSuiteWithSnapshots(t, ssCfg)
|
|
|
|
m := snapshottypes.Metadata{ChunkHashes: [][]byte{{1}, {2}, {3}}}
|
|
metadata, err := m.Marshal()
|
|
require.NoError(t, err)
|
|
|
|
hash := []byte{1, 2, 3}
|
|
|
|
testCases := map[string]struct {
|
|
snapshot *abci.Snapshot
|
|
result abci.OfferSnapshotResult
|
|
}{
|
|
"nil snapshot": {nil, abci.OFFER_SNAPSHOT_RESULT_REJECT},
|
|
"invalid format": {&abci.Snapshot{
|
|
Height: 1, Format: 9, Chunks: 3, Hash: hash, Metadata: metadata,
|
|
}, abci.OFFER_SNAPSHOT_RESULT_REJECT_FORMAT},
|
|
"incorrect chunk count": {&abci.Snapshot{
|
|
Height: 1, Format: snapshottypes.CurrentFormat, Chunks: 2, Hash: hash, Metadata: metadata,
|
|
}, abci.OFFER_SNAPSHOT_RESULT_REJECT},
|
|
"no chunks": {&abci.Snapshot{
|
|
Height: 1, Format: snapshottypes.CurrentFormat, Chunks: 0, Hash: hash, Metadata: metadata,
|
|
}, abci.OFFER_SNAPSHOT_RESULT_REJECT},
|
|
"invalid metadata serialization": {&abci.Snapshot{
|
|
Height: 1, Format: snapshottypes.CurrentFormat, Chunks: 0, Hash: hash, Metadata: []byte{3, 1, 4},
|
|
}, abci.OFFER_SNAPSHOT_RESULT_REJECT},
|
|
}
|
|
for name, tc := range testCases {
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
resp, err := suite.baseApp.OfferSnapshot(&abci.OfferSnapshotRequest{Snapshot: tc.snapshot})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.result, resp.Result)
|
|
})
|
|
}
|
|
|
|
// Offering a snapshot after one has been accepted should error
|
|
resp, err := suite.baseApp.OfferSnapshot(&abci.OfferSnapshotRequest{Snapshot: &abci.Snapshot{
|
|
Height: 1,
|
|
Format: snapshottypes.CurrentFormat,
|
|
Chunks: 3,
|
|
Hash: []byte{1, 2, 3},
|
|
Metadata: metadata,
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, &abci.OfferSnapshotResponse{Result: abci.OFFER_SNAPSHOT_RESULT_ACCEPT}, resp)
|
|
|
|
resp, err = suite.baseApp.OfferSnapshot(&abci.OfferSnapshotRequest{Snapshot: &abci.Snapshot{
|
|
Height: 2,
|
|
Format: snapshottypes.CurrentFormat,
|
|
Chunks: 3,
|
|
Hash: []byte{1, 2, 3},
|
|
Metadata: metadata,
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, &abci.OfferSnapshotResponse{Result: abci.OFFER_SNAPSHOT_RESULT_ABORT}, resp)
|
|
}
|
|
|
|
func TestABCI_ApplySnapshotChunk(t *testing.T) {
|
|
srcCfg := SnapshotsConfig{
|
|
blocks: 4,
|
|
blockTxs: 10,
|
|
snapshotInterval: 2,
|
|
snapshotKeepRecent: 2,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
}
|
|
srcSuite := NewBaseAppSuiteWithSnapshots(t, srcCfg)
|
|
|
|
targetCfg := SnapshotsConfig{
|
|
blocks: 0,
|
|
blockTxs: 0,
|
|
snapshotInterval: 2,
|
|
snapshotKeepRecent: 2,
|
|
pruningOpts: pruningtypes.NewPruningOptions(pruningtypes.PruningNothing),
|
|
}
|
|
targetSuite := NewBaseAppSuiteWithSnapshots(t, targetCfg)
|
|
|
|
// fetch latest snapshot to restore
|
|
respList, err := srcSuite.baseApp.ListSnapshots(&abci.ListSnapshotsRequest{})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, respList.Snapshots)
|
|
snapshot := respList.Snapshots[0]
|
|
|
|
// make sure the snapshot has at least 3 chunks
|
|
require.GreaterOrEqual(t, snapshot.Chunks, uint32(3), "Not enough snapshot chunks")
|
|
|
|
// begin a snapshot restoration in the target
|
|
respOffer, err := targetSuite.baseApp.OfferSnapshot(&abci.OfferSnapshotRequest{Snapshot: snapshot})
|
|
require.NoError(t, err)
|
|
require.Equal(t, &abci.OfferSnapshotResponse{Result: abci.OFFER_SNAPSHOT_RESULT_ACCEPT}, respOffer)
|
|
|
|
// We should be able to pass an invalid chunk and get a verify failure, before
|
|
// reapplying it.
|
|
respApply, err := targetSuite.baseApp.ApplySnapshotChunk(&abci.ApplySnapshotChunkRequest{
|
|
Index: 0,
|
|
Chunk: []byte{9},
|
|
Sender: "sender",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, &abci.ApplySnapshotChunkResponse{
|
|
Result: abci.APPLY_SNAPSHOT_CHUNK_RESULT_RETRY,
|
|
RefetchChunks: []uint32{0},
|
|
RejectSenders: []string{"sender"},
|
|
}, respApply)
|
|
|
|
// fetch each chunk from the source and apply it to the target
|
|
for index := uint32(0); index < snapshot.Chunks; index++ {
|
|
respChunk, err := srcSuite.baseApp.LoadSnapshotChunk(&abci.LoadSnapshotChunkRequest{
|
|
Height: snapshot.Height,
|
|
Format: snapshot.Format,
|
|
Chunk: index,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, respChunk.Chunk)
|
|
|
|
respApply, err := targetSuite.baseApp.ApplySnapshotChunk(&abci.ApplySnapshotChunkRequest{
|
|
Index: index,
|
|
Chunk: respChunk.Chunk,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, &abci.ApplySnapshotChunkResponse{
|
|
Result: abci.APPLY_SNAPSHOT_CHUNK_RESULT_ACCEPT,
|
|
}, respApply)
|
|
}
|
|
|
|
// the target should now have the same hash as the source
|
|
require.Equal(t, srcSuite.baseApp.LastCommitID(), targetSuite.baseApp.LastCommitID())
|
|
}
|