diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 55640b08d..ea41e3eed 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -45,6 +45,18 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [proto] \#9356 Migrate from `gogo/protobuf` to `cosmos/gogoproto` (@julienrbrt) - [rpc] \#9276 Added `header` and `header_by_hash` queries to the RPC client (@samricotta) - [abci] \#5706 Added `AbciVersion` to `RequestInfo` allowing applications to check ABCI version when connecting to Tendermint. (@marbar3778) +<<<<<<< HEAD +======= +- [node] \#6059 Validate and complete genesis doc before saving to state store (@silasdavis) + +- [crypto/ed25519] \#5632 Adopt zip215 `ed25519` verification. (@marbar3778) +- [crypto/ed25519] \#6526 Use [curve25519-voi](https://github.com/oasisprotocol/curve25519-voi) for `ed25519` signing and verification. (@Yawning) +- [crypto/sr25519] \#6526 Use [curve25519-voi](https://github.com/oasisprotocol/curve25519-voi) for `sr25519` signing and verification. (@Yawning) +- [crypto] \#6120 Implement batch verification interface for ed25519 and sr25519. (@marbar3778 & @Yawning) +- [types] \#6120 use batch verification for verifying commits signatures. (@marbar3778 & @cmwaters & @Yawning) + - If the key type supports the batch verification API it will try to batch verify. If the verification fails we will single verify each signature. +- [state] \#9505 Added logic so when pruning, the evidence period is taken into consideration and only deletes unecessary data (@samricotta) +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) ### BUG FIXES diff --git a/consensus/replay_test.go b/consensus/replay_test.go index d9ec4d954..383a01e52 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -735,7 +735,7 @@ func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uin // Prune block store if requested expectError := false if mode == 3 { - pruned, err := store.PruneBlocks(2) + pruned, _, err := store.PruneBlocks(2, state) require.NoError(t, err) require.EqualValues(t, 1, pruned) expectError = int64(nBlocks) < 2 @@ -1184,7 +1184,8 @@ func (bs *mockBlockStore) LoadSeenCommit(height int64) *types.Commit { return bs.commits[height-1] } -func (bs *mockBlockStore) PruneBlocks(height int64) (uint64, error) { +func (bs *mockBlockStore) PruneBlocks(height int64, state sm.State) (uint64, int64, error) { + evidencePoint := height pruned := uint64(0) for i := int64(0); i < height-1; i++ { bs.chain[i] = nil @@ -1192,7 +1193,7 @@ func (bs *mockBlockStore) PruneBlocks(height int64) (uint64, error) { pruned++ } bs.base = height - return pruned, nil + return pruned, evidencePoint, nil } //--------------------------------------- diff --git a/evidence/verify.go b/evidence/verify.go index c20cb0a2d..528589421 100644 --- a/evidence/verify.go +++ b/evidence/verify.go @@ -21,7 +21,6 @@ func (evpool *Pool) verify(evidence types.Evidence) error { state = evpool.State() height = state.LastBlockHeight evidenceParams = state.ConsensusParams.Evidence - ageNumBlocks = height - evidence.Height() ) // verify the time of the evidence @@ -34,10 +33,9 @@ func (evpool *Pool) verify(evidence types.Evidence) error { return fmt.Errorf("evidence has a different time to the block it is associated with (%v != %v)", evidence.Time(), evTime) } - ageDuration := state.LastBlockTime.Sub(evTime) - // check that the evidence hasn't expired - if ageDuration > evidenceParams.MaxAgeDuration && ageNumBlocks > evidenceParams.MaxAgeNumBlocks { + // checking if evidence is expired calculated using the block evidence time and height + if IsEvidenceExpired(height, state.LastBlockTime, evidence.Height(), evTime, evidenceParams) { return fmt.Errorf( "evidence from height %d (created at: %v) is too old; min height is %d and evidence can not be older than %v", evidence.Height(), @@ -284,3 +282,14 @@ func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, Commit: commit, }, nil } + +// check that the evidence hasn't expired +func IsEvidenceExpired(heightNow int64, timeNow time.Time, heightEv int64, timeEv time.Time, evidenceParams types.EvidenceParams) bool { + ageDuration := timeNow.Sub(timeEv) + ageNumBlocks := heightNow - heightEv + + if ageDuration > evidenceParams.MaxAgeDuration && ageNumBlocks > evidenceParams.MaxAgeNumBlocks { + return true + } + return false +} diff --git a/go.sum b/go.sum index 90c4424fd..998021d1b 100644 --- a/go.sum +++ b/go.sum @@ -56,7 +56,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +<<<<<<< HEAD github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= +======= +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -226,7 +229,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/gogoproto v1.4.1 h1:WoyH+0/jbCTzpKNvyav5FL1ZTWsp1im1MxEpJEzKUB8= github.com/cosmos/gogoproto v1.4.1/go.mod h1:Ac9lzL4vFpBMcptJROQ6dQ4M3pOEK5Z/l0Q9p+LoCr4= @@ -533,9 +535,13 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +<<<<<<< HEAD github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +======= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -731,7 +737,10 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +<<<<<<< HEAD github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= +======= +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= @@ -794,6 +803,11 @@ github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3L github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +<<<<<<< HEAD +======= +github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae h1:FatpGJD2jmJfhZiFDElaC0QhZUDQnxUeAwTGkfAHN3I= +github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= diff --git a/state/execution.go b/state/execution.go index e2ed1c8d8..458192300 100644 --- a/state/execution.go +++ b/state/execution.go @@ -256,6 +256,19 @@ func (blockExec *BlockExecutor) ApplyBlock( fail.Fail() // XXX +<<<<<<< HEAD +======= + // Prune old heights, if requested by ABCI app. + if retainHeight > 0 { + pruned, err := blockExec.pruneBlocks(retainHeight, state) + if err != nil { + blockExec.logger.Error("failed to prune blocks", "retain_height", retainHeight, "err", err) + } else { + blockExec.logger.Debug("pruned blocks", "pruned", pruned, "retain_height", retainHeight) + } + } + +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) // Events are fired after everything else. // NOTE: if we crash between Commit and Save, events wont be fired during replay fireEvents(blockExec.logger, blockExec.eventBus, block, abciResponses, validatorUpdates) @@ -625,3 +638,24 @@ func ExecCommitBlock( // ResponseCommit has no error or log, just data return res.Data, nil } +<<<<<<< HEAD +======= + +func (blockExec *BlockExecutor) pruneBlocks(retainHeight int64, state State) (uint64, error) { + base := blockExec.blockStore.Base() + if retainHeight <= base { + return 0, nil + } + + amountPruned, prunedHeaderHeight, err := blockExec.blockStore.PruneBlocks(retainHeight, state) + if err != nil { + return 0, fmt.Errorf("failed to prune block store: %w", err) + } + + err = blockExec.Store().PruneStates(base, retainHeight, prunedHeaderHeight) + if err != nil { + return 0, fmt.Errorf("failed to prune state store: %w", err) + } + return amountPruned, nil +} +>>>>>>> abbeb919d (Use evidence period when pruning (#9505)) diff --git a/state/mocks/block_store.go b/state/mocks/block_store.go index f93f45447..f78e86f86 100644 --- a/state/mocks/block_store.go +++ b/state/mocks/block_store.go @@ -4,6 +4,7 @@ package mocks import ( mock "github.com/stretchr/testify/mock" + state "github.com/tendermint/tendermint/state" types "github.com/tendermint/tendermint/types" ) @@ -169,25 +170,32 @@ func (_m *BlockStore) LoadSeenCommit(height int64) *types.Commit { return r0 } -// PruneBlocks provides a mock function with given fields: height -func (_m *BlockStore) PruneBlocks(height int64) (uint64, error) { - ret := _m.Called(height) +// PruneBlocks provides a mock function with given fields: height, _a1 +func (_m *BlockStore) PruneBlocks(height int64, _a1 state.State) (uint64, int64, error) { + ret := _m.Called(height, _a1) var r0 uint64 - if rf, ok := ret.Get(0).(func(int64) uint64); ok { - r0 = rf(height) + if rf, ok := ret.Get(0).(func(int64, state.State) uint64); ok { + r0 = rf(height, _a1) } else { r0 = ret.Get(0).(uint64) } - var r1 error - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(height) + var r1 int64 + if rf, ok := ret.Get(1).(func(int64, state.State) int64); ok { + r1 = rf(height, _a1) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(int64) } - return r0, r1 + var r2 error + if rf, ok := ret.Get(2).(func(int64, state.State) error); ok { + r2 = rf(height, _a1) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // SaveBlock provides a mock function with given fields: block, blockParts, seenCommit diff --git a/state/mocks/store.go b/state/mocks/store.go index 47b5579a7..a89cf6f04 100644 --- a/state/mocks/store.go +++ b/state/mocks/store.go @@ -197,13 +197,13 @@ func (_m *Store) LoadValidators(_a0 int64) (*types.ValidatorSet, error) { return r0, r1 } -// PruneStates provides a mock function with given fields: _a0, _a1 -func (_m *Store) PruneStates(_a0 int64, _a1 int64) error { - ret := _m.Called(_a0, _a1) +// PruneStates provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Store) PruneStates(_a0 int64, _a1 int64, _a2 int64) error { + ret := _m.Called(_a0, _a1, _a2) var r0 error - if rf, ok := ret.Get(0).(func(int64, int64) error); ok { - r0 = rf(_a0, _a1) + if rf, ok := ret.Get(0).(func(int64, int64, int64) error); ok { + r0 = rf(_a0, _a1, _a2) } else { r0 = ret.Error(0) } diff --git a/state/services.go b/state/services.go index 6e24af036..196ef0f23 100644 --- a/state/services.go +++ b/state/services.go @@ -26,7 +26,7 @@ type BlockStore interface { SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) - PruneBlocks(height int64) (uint64, error) + PruneBlocks(height int64, state State) (uint64, int64, error) LoadBlockByHash(hash []byte) *types.Block LoadBlockMetaByHash(hash []byte) *types.BlockMeta diff --git a/state/store.go b/state/store.go index fe0aae988..8503ce687 100644 --- a/state/store.go +++ b/state/store.go @@ -68,10 +68,10 @@ type Store interface { Save(State) error // SaveABCIResponses saves ABCIResponses for a given height SaveABCIResponses(int64, *tmstate.ABCIResponses) error - // Bootstrap is used for bootstrapping state when not starting from a initial height. + // Bootstrap is used for bootstrapping state when not starting from a initial height Bootstrap(State) error - // PruneStates takes the height from which to start prning and which height stop at - PruneStates(int64, int64) error + // PruneStates takes the height from which to start pruning and which height stop at + PruneStates(int64, int64, int64) error // Close closes the connection with the database Close() error } @@ -237,14 +237,15 @@ func (store dbStore) Bootstrap(state State) error { // encoding not preserving ordering: https://github.com/tendermint/tendermint/issues/4567 // This will cause some old states to be left behind when doing incremental partial prunes, // specifically older checkpoints and LastHeightChanged targets. -func (store dbStore) PruneStates(from int64, to int64) error { +func (store dbStore) PruneStates(from int64, to int64, evidenceThresholdHeight int64) error { if from <= 0 || to <= 0 { return fmt.Errorf("from height %v and to height %v must be greater than 0", from, to) } if from >= to { return fmt.Errorf("from height %v must be lower than to height %v", from, to) } - valInfo, err := loadValidatorsInfo(store.db, to) + + valInfo, err := loadValidatorsInfo(store.db, min(to, evidenceThresholdHeight)) if err != nil { return fmt.Errorf("validators at height %v not found: %w", to, err) } @@ -298,12 +299,14 @@ func (store dbStore) PruneStates(from int64, to int64) error { return err } } - } else { + } else if h < evidenceThresholdHeight { err = batch.Delete(calcValidatorsKey(h)) if err != nil { return err } } + // else we keep the validator set because we might need + // it later on for evidence verification if keepParams[h] { p, err := store.loadConsensusParamsInfo(h) @@ -661,3 +664,10 @@ func (store dbStore) saveConsensusParamsInfo(nextHeight, changeHeight int64, par func (store dbStore) Close() error { return store.db.Close() } + +func min(a int64, b int64) int64 { + if a < b { + return a + } + return b +} diff --git a/state/store_test.go b/state/store_test.go index 8b3a6c4bd..fd1fabc2e 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -88,23 +88,25 @@ func BenchmarkLoadValidators(b *testing.B) { func TestPruneStates(t *testing.T) { testcases := map[string]struct { - makeHeights int64 - pruneFrom int64 - pruneTo int64 - expectErr bool - expectVals []int64 - expectParams []int64 - expectABCI []int64 + makeHeights int64 + pruneFrom int64 + pruneTo int64 + evidenceThresholdHeight int64 + expectErr bool + expectVals []int64 + expectParams []int64 + expectABCI []int64 }{ - "error on pruning from 0": {100, 0, 5, true, nil, nil, nil}, - "error when from > to": {100, 3, 2, true, nil, nil, nil}, - "error when from == to": {100, 3, 3, true, nil, nil, nil}, - "error when to does not exist": {100, 1, 101, true, nil, nil, nil}, - "prune all": {100, 1, 100, false, []int64{93, 100}, []int64{95, 100}, []int64{100}}, - "prune some": {10, 2, 8, false, []int64{1, 3, 8, 9, 10}, + "error on pruning from 0": {100, 0, 5, 100, true, nil, nil, nil}, + "error when from > to": {100, 3, 2, 2, true, nil, nil, nil}, + "error when from == to": {100, 3, 3, 3, true, nil, nil, nil}, + "error when to does not exist": {100, 1, 101, 101, true, nil, nil, nil}, + "prune all": {100, 1, 100, 100, false, []int64{93, 100}, []int64{95, 100}, []int64{100}}, + "prune some": {10, 2, 8, 8, false, []int64{1, 3, 8, 9, 10}, []int64{1, 5, 8, 9, 10}, []int64{1, 8, 9, 10}}, - "prune across checkpoint": {100001, 1, 100001, false, []int64{99993, 100000, 100001}, + "prune across checkpoint": {100001, 1, 100001, 100001, false, []int64{99993, 100000, 100001}, []int64{99995, 100001}, []int64{100001}}, + "prune when evidence height < height": {20, 1, 18, 17, false, []int64{13, 17, 18, 19, 20}, []int64{15, 18, 19, 20}, []int64{18, 19, 20}}, } for name, tc := range testcases { tc := tc @@ -163,7 +165,7 @@ func TestPruneStates(t *testing.T) { } // Test assertions - err := stateStore.PruneStates(tc.pruneFrom, tc.pruneTo) + err := stateStore.PruneStates(tc.pruneFrom, tc.pruneTo, tc.evidenceThresholdHeight) if tc.expectErr { require.Error(t, err) return diff --git a/store/store.go b/store/store.go index 866965ac6..5276ede7e 100644 --- a/store/store.go +++ b/store/store.go @@ -7,9 +7,11 @@ import ( "github.com/cosmos/gogoproto/proto" dbm "github.com/tendermint/tm-db" + "github.com/tendermint/tendermint/evidence" tmsync "github.com/tendermint/tendermint/libs/sync" tmstore "github.com/tendermint/tendermint/proto/tendermint/store" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -264,20 +266,20 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit { return commit } -// PruneBlocks removes block up to (but not including) a height. It returns number of blocks pruned. -func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) { +// PruneBlocks removes block up to (but not including) a height. It returns number of blocks pruned and the evidence retain height - the height at which data needed to prove evidence must not be removed. +func (bs *BlockStore) PruneBlocks(height int64, state sm.State) (uint64, int64, error) { if height <= 0 { - return 0, fmt.Errorf("height must be greater than 0") + return 0, -1, fmt.Errorf("height must be greater than 0") } bs.mtx.RLock() if height > bs.height { bs.mtx.RUnlock() - return 0, fmt.Errorf("cannot prune beyond the latest height %v", bs.height) + return 0, -1, fmt.Errorf("cannot prune beyond the latest height %v", bs.height) } base := bs.base bs.mtx.RUnlock() if height < base { - return 0, fmt.Errorf("cannot prune to height %v, it is lower than base height %v", + return 0, -1, fmt.Errorf("cannot prune to height %v, it is lower than base height %v", height, base) } @@ -300,26 +302,42 @@ func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) { return nil } + evidencePoint := height for h := base; h < height; h++ { + meta := bs.LoadBlockMeta(h) if meta == nil { // assume already deleted continue } - if err := batch.Delete(calcBlockMetaKey(h)); err != nil { - return 0, err + + // This logic is in place to protect data that proves malicious behavior. + // If the height is within the evidence age, we continue to persist the header and commit data. + + if evidencePoint == height && !evidence.IsEvidenceExpired(state.LastBlockHeight, state.LastBlockTime, h, meta.Header.Time, state.ConsensusParams.Evidence) { + evidencePoint = h + } + + // if height is beyond the evidence point we dont delete the header + if h < evidencePoint { + if err := batch.Delete(calcBlockMetaKey(h)); err != nil { + return 0, -1, err + } } if err := batch.Delete(calcBlockHashKey(meta.BlockID.Hash)); err != nil { - return 0, err + return 0, -1, err } - if err := batch.Delete(calcBlockCommitKey(h)); err != nil { - return 0, err + // if height is beyond the evidence point we dont delete the commit data + if h < evidencePoint { + if err := batch.Delete(calcBlockCommitKey(h)); err != nil { + return 0, -1, err + } } if err := batch.Delete(calcSeenCommitKey(h)); err != nil { - return 0, err + return 0, -1, err } for p := 0; p < int(meta.BlockID.PartSetHeader.Total); p++ { if err := batch.Delete(calcBlockPartKey(h, p)); err != nil { - return 0, err + return 0, -1, err } } pruned++ @@ -328,7 +346,7 @@ func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) { if pruned%1000 == 0 && pruned > 0 { err := flush(batch, h) if err != nil { - return 0, err + return 0, -1, err } batch = bs.db.NewBatch() defer batch.Close() @@ -337,9 +355,9 @@ func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) { err := flush(batch, height) if err != nil { - return 0, err + return 0, -1, err } - return pruned, nil + return pruned, evidencePoint, nil } // SaveBlock persists the given block, blockParts, and seenCommit to the underlying db. diff --git a/store/store_test.go b/store/store_test.go index 06dab4767..18ce886a3 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -382,7 +382,7 @@ func TestLoadBaseMeta(t *testing.T) { bs.SaveBlock(block, partSet, seenCommit) } - _, err = bs.PruneBlocks(4) + _, _, err = bs.PruneBlocks(4, state) require.NoError(t, err) baseBlock := bs.LoadBaseMeta() @@ -438,10 +438,10 @@ func TestPruneBlocks(t *testing.T) { assert.EqualValues(t, 0, bs.Size()) // pruning an empty store should error, even when pruning to 0 - _, err = bs.PruneBlocks(1) + _, _, err = bs.PruneBlocks(1, state) require.Error(t, err) - _, err = bs.PruneBlocks(0) + _, _, err = bs.PruneBlocks(0, state) require.Error(t, err) // make more than 1000 blocks, to test batch deletions @@ -457,27 +457,30 @@ func TestPruneBlocks(t *testing.T) { assert.EqualValues(t, 1500, bs.Height()) assert.EqualValues(t, 1500, bs.Size()) - prunedBlock := bs.LoadBlock(1199) + state.LastBlockTime = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC) + state.LastBlockHeight = 1500 + + state.ConsensusParams.Evidence.MaxAgeNumBlocks = 400 + state.ConsensusParams.Evidence.MaxAgeDuration = 1 * time.Second // Check that basic pruning works - pruned, err := bs.PruneBlocks(1200) + pruned, evidenceRetainHeight, err := bs.PruneBlocks(1200, state) require.NoError(t, err) assert.EqualValues(t, 1199, pruned) assert.EqualValues(t, 1200, bs.Base()) assert.EqualValues(t, 1500, bs.Height()) assert.EqualValues(t, 301, bs.Size()) - assert.EqualValues(t, tmstore.BlockStoreState{ - Base: 1200, - Height: 1500, - }, LoadBlockStoreState(db)) + assert.EqualValues(t, 1100, evidenceRetainHeight) require.NotNil(t, bs.LoadBlock(1200)) require.Nil(t, bs.LoadBlock(1199)) - require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash())) - require.Nil(t, bs.LoadBlockCommit(1199)) - require.Nil(t, bs.LoadBlockMeta(1199)) - require.Nil(t, bs.LoadBlockMetaByHash(prunedBlock.Hash())) - require.Nil(t, bs.LoadBlockPart(1199, 1)) + + // The header and commit for heights 1100 onwards + // need to remain to verify evidence + require.NotNil(t, bs.LoadBlockMeta(1100)) + require.Nil(t, bs.LoadBlockMeta(1099)) + require.NotNil(t, bs.LoadBlockCommit(1100)) + require.Nil(t, bs.LoadBlockCommit(1099)) for i := int64(1); i < 1200; i++ { require.Nil(t, bs.LoadBlock(i)) @@ -487,26 +490,33 @@ func TestPruneBlocks(t *testing.T) { } // Pruning below the current base should error - _, err = bs.PruneBlocks(1199) + _, _, err = bs.PruneBlocks(1199, state) require.Error(t, err) // Pruning to the current base should work - pruned, err = bs.PruneBlocks(1200) + pruned, _, err = bs.PruneBlocks(1200, state) require.NoError(t, err) assert.EqualValues(t, 0, pruned) // Pruning again should work - pruned, err = bs.PruneBlocks(1300) + pruned, _, err = bs.PruneBlocks(1300, state) require.NoError(t, err) assert.EqualValues(t, 100, pruned) assert.EqualValues(t, 1300, bs.Base()) + // we should still have the header and the commit + // as they're needed for evidence + require.NotNil(t, bs.LoadBlockMeta(1100)) + require.Nil(t, bs.LoadBlockMeta(1099)) + require.NotNil(t, bs.LoadBlockCommit(1100)) + require.Nil(t, bs.LoadBlockCommit(1099)) + // Pruning beyond the current height should error - _, err = bs.PruneBlocks(1501) + _, _, err = bs.PruneBlocks(1501, state) require.Error(t, err) // Pruning to the current height should work - pruned, err = bs.PruneBlocks(1500) + pruned, _, err = bs.PruneBlocks(1500, state) require.NoError(t, err) assert.EqualValues(t, 200, pruned) assert.Nil(t, bs.LoadBlock(1499))