Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

statesync: assert app version matches (backport #7856) #7885

Merged
merged 8 commits into from
Feb 23, 2022
35 changes: 22 additions & 13 deletions statesync/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,10 @@ func (s *syncer) Sync(snapshot *snapshot, chunks *chunkQueue) (sm.State, *types.
return sm.State{}, nil, err
}

// Verify app and update app version
appVersion, err := s.verifyApp(snapshot)
if err != nil {
// Verify app and app version
if err := s.verifyApp(snapshot, state.Version.Consensus.App); err != nil {
return sm.State{}, nil, err
}
state.Version.Consensus.App = appVersion

// Done! 🎉
s.logger.Info("Snapshot restored", "height", snapshot.Height, "format", snapshot.Format,
Expand Down Expand Up @@ -476,25 +474,36 @@ func (s *syncer) requestChunk(snapshot *snapshot, chunk uint32) {
}))
}

// verifyApp verifies the sync, checking the app hash and last block height. It returns the
// app version, which should be returned as part of the initial state.
func (s *syncer) verifyApp(snapshot *snapshot) (uint64, error) {
// verifyApp verifies the sync, checking the app hash, last block height and app version
func (s *syncer) verifyApp(snapshot *snapshot, appVersion uint64) error {
resp, err := s.connQuery.InfoSync(proxy.RequestInfo)
if err != nil {
return 0, fmt.Errorf("failed to query ABCI app for appHash: %w", err)
return fmt.Errorf("failed to query ABCI app for appHash: %w", err)
}

// sanity check that the app version in the block matches the application's own record
// of its version
if resp.AppVersion != appVersion {
// An error here most likely means that the app hasn't inplemented state sync
// or the Info call correctly
return fmt.Errorf("app version mismatch. Expected: %d, got: %d",
appVersion, resp.AppVersion)
}
if !bytes.Equal(snapshot.trustedAppHash, resp.LastBlockAppHash) {
s.logger.Error("appHash verification failed",
"expected", snapshot.trustedAppHash,
"actual", resp.LastBlockAppHash)
return 0, errVerifyFailed
return errVerifyFailed
}
if uint64(resp.LastBlockHeight) != snapshot.Height {
s.logger.Error("ABCI app reported unexpected last block height",
"expected", snapshot.Height, "actual", resp.LastBlockHeight)
return 0, errVerifyFailed
s.logger.Error(
"ABCI app reported unexpected last block height",
"expected", snapshot.Height,
"actual", resp.LastBlockHeight,
)
return errVerifyFailed
}

s.logger.Info("Verified ABCI app", "height", snapshot.Height, "appHash", snapshot.trustedAppHash)
return resp.AppVersion, nil
return nil
}
28 changes: 16 additions & 12 deletions statesync/syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"github.com/tendermint/tendermint/version"
)

const testAppVersion = 9

// Sets up a basic syncer that can be used to test OfferSnapshot requests
func setupOfferSyncer(t *testing.T) (*syncer, *proxymocks.AppConnSnapshot) {
connQuery := &proxymocks.AppConnQuery{}
Expand All @@ -51,7 +53,7 @@ func TestSyncer_SyncAny(t *testing.T) {
Version: tmstate.Version{
Consensus: tmversion.Consensus{
Block: version.BlockProtocol,
App: 0,
App: testAppVersion,
},
Software: version.TMCoreSemVer,
},
Expand Down Expand Up @@ -184,7 +186,7 @@ func TestSyncer_SyncAny(t *testing.T) {
Index: 2, Chunk: []byte{1, 1, 2},
}).Once().Return(&abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}, nil)
connQuery.On("InfoSync", proxy.RequestInfo).Return(&abci.ResponseInfo{
AppVersion: 9,
AppVersion: testAppVersion,
LastBlockHeight: 1,
LastBlockAppHash: []byte("app_hash"),
}, nil)
Expand All @@ -198,9 +200,7 @@ func TestSyncer_SyncAny(t *testing.T) {
assert.Equal(t, map[uint32]int{0: 1, 1: 2, 2: 1}, chunkRequests)
chunkRequestsMtx.Unlock()

// The syncer should have updated the state app version from the ABCI info response.
expectState := state
expectState.Version.Consensus.App = 9

assert.Equal(t, expectState, newState)
assert.Equal(t, commit, lastCommit)
Expand Down Expand Up @@ -613,6 +613,8 @@ func TestSyncer_applyChunks_RejectSenders(t *testing.T) {

func TestSyncer_verifyApp(t *testing.T) {
boom := errors.New("boom")
const appVersion = 9
appVersionMismatchErr := errors.New("app version mismatch. Expected: 9, got: 2")
s := &snapshot{Height: 3, Format: 1, Chunks: 5, Hash: []byte{1, 2, 3}, trustedAppHash: []byte("app_hash")}

testcases := map[string]struct {
Expand All @@ -623,17 +625,22 @@ func TestSyncer_verifyApp(t *testing.T) {
"verified": {&abci.ResponseInfo{
LastBlockHeight: 3,
LastBlockAppHash: []byte("app_hash"),
AppVersion: 9,
AppVersion: appVersion,
}, nil, nil},
"invalid app version": {&abci.ResponseInfo{
LastBlockHeight: 3,
LastBlockAppHash: []byte("app_hash"),
AppVersion: 2,
}, nil, appVersionMismatchErr},
"invalid height": {&abci.ResponseInfo{
LastBlockHeight: 5,
LastBlockAppHash: []byte("app_hash"),
AppVersion: 9,
AppVersion: appVersion,
}, nil, errVerifyFailed},
"invalid hash": {&abci.ResponseInfo{
LastBlockHeight: 3,
LastBlockAppHash: []byte("xxx"),
AppVersion: 9,
AppVersion: appVersion,
}, nil, errVerifyFailed},
"error": {nil, boom, boom},
}
Expand All @@ -648,15 +655,12 @@ func TestSyncer_verifyApp(t *testing.T) {
syncer := newSyncer(*cfg, log.NewNopLogger(), connSnapshot, connQuery, stateProvider, "")

connQuery.On("InfoSync", proxy.RequestInfo).Return(tc.response, tc.err)
version, err := syncer.verifyApp(s)
err := syncer.verifyApp(s, appVersion)
unwrapped := errors.Unwrap(err)
if unwrapped != nil {
err = unwrapped
}
assert.Equal(t, tc.expectErr, err)
if err == nil {
assert.Equal(t, tc.response.AppVersion, version)
}
require.Equal(t, tc.expectErr, err)
})
}
}
Expand Down