2020-07-03 05:46:53 +00:00
|
|
|
package blockchain
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-07-22 17:05:17 +00:00
|
|
|
"sync"
|
2020-07-03 05:46:53 +00:00
|
|
|
"testing"
|
2020-10-23 00:35:30 +00:00
|
|
|
"time"
|
2020-07-03 05:46:53 +00:00
|
|
|
|
2021-02-16 07:45:34 +00:00
|
|
|
types "github.com/prysmaticlabs/eth2-types"
|
2020-07-03 05:46:53 +00:00
|
|
|
blockchainTesting "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing"
|
|
|
|
testDB "github.com/prysmaticlabs/prysm/beacon-chain/db/testing"
|
|
|
|
"github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/protoarray"
|
|
|
|
"github.com/prysmaticlabs/prysm/beacon-chain/operations/attestations"
|
|
|
|
"github.com/prysmaticlabs/prysm/beacon-chain/operations/voluntaryexits"
|
|
|
|
"github.com/prysmaticlabs/prysm/beacon-chain/state/stategen"
|
2021-09-21 19:59:25 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/config/params"
|
2021-09-23 15:23:37 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
2021-07-21 21:34:07 +00:00
|
|
|
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
2021-07-28 21:23:44 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/block"
|
2021-07-21 21:34:07 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
|
2021-09-23 18:53:46 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/testing/assert"
|
|
|
|
"github.com/prysmaticlabs/prysm/testing/require"
|
|
|
|
"github.com/prysmaticlabs/prysm/testing/util"
|
2020-10-23 00:35:30 +00:00
|
|
|
logTest "github.com/sirupsen/logrus/hooks/test"
|
2020-07-03 05:46:53 +00:00
|
|
|
)
|
|
|
|
|
2020-07-09 23:50:48 +00:00
|
|
|
func TestService_ReceiveBlock(t *testing.T) {
|
2020-07-03 05:46:53 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
2021-09-23 18:53:46 +00:00
|
|
|
genesis, keys := util.DeterministicGenesisState(t, 64)
|
|
|
|
genFullBlock := func(t *testing.T, conf *util.BlockGenConfig, slot types.Slot) *ethpb.SignedBeaconBlock {
|
|
|
|
blk, err := util.GenerateFullBlock(genesis, keys, conf, slot)
|
2020-07-16 12:11:39 +00:00
|
|
|
assert.NoError(t, err)
|
2020-07-03 05:46:53 +00:00
|
|
|
return blk
|
|
|
|
}
|
2021-12-10 04:18:47 +00:00
|
|
|
params.SetupTestConfigCleanup(t)
|
2020-07-03 05:46:53 +00:00
|
|
|
bc := params.BeaconConfig()
|
|
|
|
bc.ShardCommitteePeriod = 0 // Required for voluntary exits test in reasonable time.
|
|
|
|
params.OverrideBeaconConfig(bc)
|
|
|
|
|
|
|
|
type args struct {
|
|
|
|
block *ethpb.SignedBeaconBlock
|
|
|
|
}
|
|
|
|
tests := []struct {
|
2020-08-25 15:23:06 +00:00
|
|
|
name string
|
|
|
|
args args
|
|
|
|
wantedErr string
|
|
|
|
check func(*testing.T, *Service)
|
2020-07-03 05:46:53 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "applies block with state transition",
|
|
|
|
args: args{
|
2021-09-23 18:53:46 +00:00
|
|
|
block: genFullBlock(t, util.DefaultBlockGenConfig(), 2 /*slot*/),
|
2020-07-03 05:46:53 +00:00
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
|
|
|
if hs := s.head.state.Slot(); hs != 2 {
|
|
|
|
t.Errorf("Unexpected state slot. Got %d but wanted %d", hs, 2)
|
|
|
|
}
|
2021-05-26 16:19:54 +00:00
|
|
|
if bs := s.head.block.Block().Slot(); bs != 2 {
|
2020-07-03 05:46:53 +00:00
|
|
|
t.Errorf("Unexpected head block slot. Got %d but wanted %d", bs, 2)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "saves attestations to pool",
|
|
|
|
args: args{
|
|
|
|
block: genFullBlock(t,
|
2021-09-23 18:53:46 +00:00
|
|
|
&util.BlockGenConfig{
|
2020-07-03 05:46:53 +00:00
|
|
|
NumProposerSlashings: 0,
|
|
|
|
NumAttesterSlashings: 0,
|
|
|
|
NumAttestations: 2,
|
|
|
|
NumDeposits: 0,
|
|
|
|
NumVoluntaryExits: 0,
|
|
|
|
},
|
|
|
|
1, /*slot*/
|
|
|
|
),
|
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
2021-03-17 18:36:56 +00:00
|
|
|
if baCount := len(s.cfg.AttPool.BlockAttestations()); baCount != 2 {
|
2020-07-03 05:46:53 +00:00
|
|
|
t.Errorf("Did not get the correct number of block attestations saved to the pool. "+
|
|
|
|
"Got %d but wanted %d", baCount, 2)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "updates exit pool",
|
|
|
|
args: args{
|
2021-09-23 18:53:46 +00:00
|
|
|
block: genFullBlock(t, &util.BlockGenConfig{
|
2020-07-03 05:46:53 +00:00
|
|
|
NumProposerSlashings: 0,
|
|
|
|
NumAttesterSlashings: 0,
|
|
|
|
NumAttestations: 0,
|
|
|
|
NumDeposits: 0,
|
|
|
|
NumVoluntaryExits: 3,
|
|
|
|
},
|
|
|
|
1, /*slot*/
|
|
|
|
),
|
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
2021-03-17 18:36:56 +00:00
|
|
|
pending := s.cfg.ExitPool.PendingExits(genesis, 1, true /* no limit */)
|
2020-11-11 06:52:58 +00:00
|
|
|
if len(pending) != 0 {
|
|
|
|
t.Errorf(
|
|
|
|
"Did not mark the correct number of exits. Got %d pending but wanted %d",
|
|
|
|
len(pending),
|
|
|
|
0,
|
|
|
|
)
|
2020-07-03 05:46:53 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "notifies block processed on state feed",
|
|
|
|
args: args{
|
2021-09-23 18:53:46 +00:00
|
|
|
block: genFullBlock(t, util.DefaultBlockGenConfig(), 1 /*slot*/),
|
2020-07-03 05:46:53 +00:00
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
2021-03-17 18:36:56 +00:00
|
|
|
if recvd := len(s.cfg.StateNotifier.(*blockchainTesting.MockStateNotifier).ReceivedEvents()); recvd < 1 {
|
2020-07-03 05:46:53 +00:00
|
|
|
t.Errorf("Received %d state notifications, expected at least 1", recvd)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2020-12-18 19:12:30 +00:00
|
|
|
beaconDB := testDB.SetupDB(t)
|
2020-07-03 05:46:53 +00:00
|
|
|
genesisBlockRoot := bytesutil.ToBytes32(nil)
|
2020-12-18 19:12:30 +00:00
|
|
|
require.NoError(t, beaconDB.SaveState(ctx, genesis, genesisBlockRoot))
|
2020-07-03 05:46:53 +00:00
|
|
|
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := []Option{
|
|
|
|
WithDatabase(beaconDB),
|
|
|
|
WithForkChoiceStore(protoarray.New(0, 0, genesisBlockRoot)),
|
|
|
|
WithAttestationPool(attestations.NewPool()),
|
|
|
|
WithExitPool(voluntaryexits.NewPool()),
|
|
|
|
WithStateNotifier(&blockchainTesting.MockStateNotifier{RecordEvents: true}),
|
|
|
|
WithStateGen(stategen.New(beaconDB)),
|
2020-07-03 05:46:53 +00:00
|
|
|
}
|
2021-11-19 15:59:26 +00:00
|
|
|
s, err := NewService(ctx, opts...)
|
2020-07-16 12:11:39 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.saveGenesisData(ctx, genesis))
|
2021-03-17 18:36:56 +00:00
|
|
|
gBlk, err := s.cfg.BeaconDB.GenesisBlock(ctx)
|
2020-07-16 12:11:39 +00:00
|
|
|
require.NoError(t, err)
|
2021-05-26 16:19:54 +00:00
|
|
|
gRoot, err := gBlk.Block().HashTreeRoot()
|
2020-10-01 18:53:36 +00:00
|
|
|
require.NoError(t, err)
|
2020-07-13 20:14:36 +00:00
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{Root: gRoot[:]}
|
2020-08-27 18:13:32 +00:00
|
|
|
root, err := tt.args.block.Block.HashTreeRoot()
|
2020-07-16 12:11:39 +00:00
|
|
|
require.NoError(t, err)
|
2021-07-06 15:34:05 +00:00
|
|
|
err = s.ReceiveBlock(ctx, wrapper.WrappedPhase0SignedBeaconBlock(tt.args.block), root)
|
2020-08-25 15:23:06 +00:00
|
|
|
if tt.wantedErr != "" {
|
|
|
|
assert.ErrorContains(t, tt.wantedErr, err)
|
2020-07-03 05:46:53 +00:00
|
|
|
} else {
|
2020-08-25 15:23:06 +00:00
|
|
|
assert.NoError(t, err)
|
2020-07-03 05:46:53 +00:00
|
|
|
tt.check(t, s)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-07-16 13:34:34 +00:00
|
|
|
|
2020-07-22 17:05:17 +00:00
|
|
|
func TestService_ReceiveBlockUpdateHead(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
2021-09-23 18:53:46 +00:00
|
|
|
genesis, keys := util.DeterministicGenesisState(t, 64)
|
|
|
|
b, err := util.GenerateFullBlock(genesis, keys, util.DefaultBlockGenConfig(), 1)
|
2020-07-22 17:05:17 +00:00
|
|
|
assert.NoError(t, err)
|
2020-12-18 19:12:30 +00:00
|
|
|
beaconDB := testDB.SetupDB(t)
|
2020-07-22 17:05:17 +00:00
|
|
|
genesisBlockRoot := bytesutil.ToBytes32(nil)
|
2020-12-18 19:12:30 +00:00
|
|
|
require.NoError(t, beaconDB.SaveState(ctx, genesis, genesisBlockRoot))
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := []Option{
|
|
|
|
WithDatabase(beaconDB),
|
|
|
|
WithForkChoiceStore(protoarray.New(0, 0, genesisBlockRoot)),
|
|
|
|
WithAttestationPool(attestations.NewPool()),
|
|
|
|
WithExitPool(voluntaryexits.NewPool()),
|
|
|
|
WithStateNotifier(&blockchainTesting.MockStateNotifier{RecordEvents: true}),
|
|
|
|
WithStateGen(stategen.New(beaconDB)),
|
2020-07-22 17:05:17 +00:00
|
|
|
}
|
2021-11-19 15:59:26 +00:00
|
|
|
|
|
|
|
s, err := NewService(ctx, opts...)
|
2020-07-22 17:05:17 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.saveGenesisData(ctx, genesis))
|
2021-03-17 18:36:56 +00:00
|
|
|
gBlk, err := s.cfg.BeaconDB.GenesisBlock(ctx)
|
2020-07-22 17:05:17 +00:00
|
|
|
require.NoError(t, err)
|
2021-05-26 16:19:54 +00:00
|
|
|
gRoot, err := gBlk.Block().HashTreeRoot()
|
2020-10-01 18:53:36 +00:00
|
|
|
require.NoError(t, err)
|
2020-07-22 17:05:17 +00:00
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{Root: gRoot[:]}
|
2020-08-27 18:13:32 +00:00
|
|
|
root, err := b.Block.HashTreeRoot()
|
2020-07-22 17:05:17 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
2021-07-06 15:34:05 +00:00
|
|
|
require.NoError(t, s.ReceiveBlock(ctx, wrapper.WrappedPhase0SignedBeaconBlock(b), root))
|
2020-07-22 17:05:17 +00:00
|
|
|
wg.Done()
|
|
|
|
}()
|
|
|
|
wg.Wait()
|
2021-03-17 18:36:56 +00:00
|
|
|
if recvd := len(s.cfg.StateNotifier.(*blockchainTesting.MockStateNotifier).ReceivedEvents()); recvd < 1 {
|
2020-07-22 17:05:17 +00:00
|
|
|
t.Errorf("Received %d state notifications, expected at least 1", recvd)
|
|
|
|
}
|
|
|
|
// Verify fork choice has processed the block. (Genesis block and the new block)
|
2021-03-17 18:36:56 +00:00
|
|
|
assert.Equal(t, 2, len(s.cfg.ForkChoiceStore.Nodes()))
|
2020-07-22 17:05:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 13:34:34 +00:00
|
|
|
func TestService_ReceiveBlockBatch(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
2021-09-23 18:53:46 +00:00
|
|
|
genesis, keys := util.DeterministicGenesisState(t, 64)
|
|
|
|
genFullBlock := func(t *testing.T, conf *util.BlockGenConfig, slot types.Slot) *ethpb.SignedBeaconBlock {
|
|
|
|
blk, err := util.GenerateFullBlock(genesis, keys, conf, slot)
|
2020-08-25 15:23:06 +00:00
|
|
|
assert.NoError(t, err)
|
2020-07-16 13:34:34 +00:00
|
|
|
return blk
|
|
|
|
}
|
|
|
|
|
|
|
|
type args struct {
|
|
|
|
block *ethpb.SignedBeaconBlock
|
|
|
|
}
|
|
|
|
tests := []struct {
|
2020-08-25 15:23:06 +00:00
|
|
|
name string
|
|
|
|
args args
|
|
|
|
wantedErr string
|
|
|
|
check func(*testing.T, *Service)
|
2020-07-16 13:34:34 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "applies block with state transition",
|
|
|
|
args: args{
|
2021-09-23 18:53:46 +00:00
|
|
|
block: genFullBlock(t, util.DefaultBlockGenConfig(), 2 /*slot*/),
|
2020-07-16 13:34:34 +00:00
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
2021-02-16 07:45:34 +00:00
|
|
|
assert.Equal(t, types.Slot(2), s.head.state.Slot(), "Incorrect head state slot")
|
2021-05-26 16:19:54 +00:00
|
|
|
assert.Equal(t, types.Slot(2), s.head.block.Block().Slot(), "Incorrect head block slot")
|
2020-07-16 13:34:34 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "notifies block processed on state feed",
|
|
|
|
args: args{
|
2021-09-23 18:53:46 +00:00
|
|
|
block: genFullBlock(t, util.DefaultBlockGenConfig(), 1 /*slot*/),
|
2020-07-16 13:34:34 +00:00
|
|
|
},
|
|
|
|
check: func(t *testing.T, s *Service) {
|
2021-03-17 18:36:56 +00:00
|
|
|
if recvd := len(s.cfg.StateNotifier.(*blockchainTesting.MockStateNotifier).ReceivedEvents()); recvd < 1 {
|
2020-07-16 13:34:34 +00:00
|
|
|
t.Errorf("Received %d state notifications, expected at least 1", recvd)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2020-12-18 19:12:30 +00:00
|
|
|
beaconDB := testDB.SetupDB(t)
|
2020-08-27 18:13:32 +00:00
|
|
|
genesisBlockRoot, err := genesis.HashTreeRoot(ctx)
|
|
|
|
require.NoError(t, err)
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := []Option{
|
|
|
|
WithDatabase(beaconDB),
|
|
|
|
WithForkChoiceStore(protoarray.New(0, 0, genesisBlockRoot)),
|
|
|
|
WithStateNotifier(&blockchainTesting.MockStateNotifier{RecordEvents: true}),
|
|
|
|
WithStateGen(stategen.New(beaconDB)),
|
2020-07-16 13:34:34 +00:00
|
|
|
}
|
2021-11-19 15:59:26 +00:00
|
|
|
s, err := NewService(ctx, opts...)
|
2020-07-16 13:34:34 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
err = s.saveGenesisData(ctx, genesis)
|
|
|
|
require.NoError(t, err)
|
2021-03-17 18:36:56 +00:00
|
|
|
gBlk, err := s.cfg.BeaconDB.GenesisBlock(ctx)
|
2020-07-16 13:34:34 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-05-26 16:19:54 +00:00
|
|
|
gRoot, err := gBlk.Block().HashTreeRoot()
|
2020-10-01 18:53:36 +00:00
|
|
|
require.NoError(t, err)
|
2020-07-16 13:34:34 +00:00
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{Root: gRoot[:]}
|
2020-08-27 18:13:32 +00:00
|
|
|
root, err := tt.args.block.Block.HashTreeRoot()
|
2020-07-16 13:34:34 +00:00
|
|
|
require.NoError(t, err)
|
2021-07-23 20:10:15 +00:00
|
|
|
blks := []block.SignedBeaconBlock{wrapper.WrappedPhase0SignedBeaconBlock(tt.args.block)}
|
2020-07-16 13:34:34 +00:00
|
|
|
roots := [][32]byte{root}
|
2020-08-25 15:23:06 +00:00
|
|
|
err = s.ReceiveBlockBatch(ctx, blks, roots)
|
|
|
|
if tt.wantedErr != "" {
|
|
|
|
assert.ErrorContains(t, tt.wantedErr, err)
|
2020-07-16 13:34:34 +00:00
|
|
|
} else {
|
2020-08-25 15:23:06 +00:00
|
|
|
assert.NoError(t, err)
|
2020-07-16 13:34:34 +00:00
|
|
|
tt.check(t, s)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestService_HasInitSyncBlock(t *testing.T) {
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := testServiceOptsNoDB()
|
|
|
|
opts = append(opts, WithStateNotifier(&blockchainTesting.MockStateNotifier{}))
|
|
|
|
s, err := NewService(context.Background(), opts...)
|
2020-07-16 13:34:34 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
r := [32]byte{'a'}
|
|
|
|
if s.HasInitSyncBlock(r) {
|
|
|
|
t.Error("Should not have block")
|
|
|
|
}
|
2021-09-23 18:53:46 +00:00
|
|
|
s.saveInitSyncBlock(r, wrapper.WrappedPhase0SignedBeaconBlock(util.NewBeaconBlock()))
|
2020-07-16 13:34:34 +00:00
|
|
|
if !s.HasInitSyncBlock(r) {
|
|
|
|
t.Error("Should have block")
|
|
|
|
}
|
|
|
|
}
|
2020-10-23 00:35:30 +00:00
|
|
|
|
|
|
|
func TestCheckSaveHotStateDB_Enabling(t *testing.T) {
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := testServiceOptsWithDB(t)
|
2020-10-23 00:35:30 +00:00
|
|
|
hook := logTest.NewGlobal()
|
2021-11-19 15:59:26 +00:00
|
|
|
s, err := NewService(context.Background(), opts...)
|
2020-10-23 00:35:30 +00:00
|
|
|
require.NoError(t, err)
|
2021-02-16 07:45:34 +00:00
|
|
|
st := params.BeaconConfig().SlotsPerEpoch.Mul(uint64(epochsSinceFinalitySaveHotStateDB))
|
2020-10-23 00:35:30 +00:00
|
|
|
s.genesisTime = time.Now().Add(time.Duration(-1*int64(st)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second)
|
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{}
|
|
|
|
|
|
|
|
require.NoError(t, s.checkSaveHotStateDB(context.Background()))
|
|
|
|
assert.LogsContain(t, hook, "Entering mode to save hot states in DB")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCheckSaveHotStateDB_Disabling(t *testing.T) {
|
|
|
|
hook := logTest.NewGlobal()
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := testServiceOptsWithDB(t)
|
|
|
|
s, err := NewService(context.Background(), opts...)
|
2020-10-23 00:35:30 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{}
|
|
|
|
require.NoError(t, s.checkSaveHotStateDB(context.Background()))
|
|
|
|
s.genesisTime = time.Now()
|
|
|
|
|
|
|
|
require.NoError(t, s.checkSaveHotStateDB(context.Background()))
|
|
|
|
assert.LogsContain(t, hook, "Exiting mode to save hot states in DB")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCheckSaveHotStateDB_Overflow(t *testing.T) {
|
|
|
|
hook := logTest.NewGlobal()
|
2021-11-19 15:59:26 +00:00
|
|
|
opts := testServiceOptsWithDB(t)
|
|
|
|
s, err := NewService(context.Background(), opts...)
|
2020-10-23 00:35:30 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
s.finalizedCheckpt = ðpb.Checkpoint{Epoch: 10000000}
|
|
|
|
s.genesisTime = time.Now()
|
|
|
|
|
|
|
|
require.NoError(t, s.checkSaveHotStateDB(context.Background()))
|
|
|
|
assert.LogsDoNotContain(t, hook, "Entering mode to save hot states in DB")
|
|
|
|
}
|