package v2 import ( "testing" "github.com/stretchr/testify/assert" "github.com/tendermint/tendermint/p2p" tdState "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) // pcBlock is a test helper structure with simple types. Its purpose is to help with test readability. type pcBlock struct { pid string height int64 } // params is a test structure used to create processor state. type params struct { height int64 items []pcBlock blocksSynced int64 verBL []int64 appBL []int64 draining bool } // makePcBlock makes an empty block. func makePcBlock(height int64) *types.Block { return &types.Block{Header: types.Header{Height: height}} } // makeState takes test parameters and creates a specific processor state. func makeState(p *params) *pcState { var ( tdState = tdState.State{} context = newMockProcessorContext(p.verBL, p.appBL) ) state := newPcState(p.height, tdState, "test", context) for _, item := range p.items { _ = state.enqueue(p2p.ID(item.pid), makePcBlock(item.height), item.height) } state.blocksSynced = p.blocksSynced state.draining = p.draining return state } func mBlockResponse(peerID p2p.ID, height int64) *scBlockReceived { return &scBlockReceived{ peerID: peerID, block: makePcBlock(height), } } type pcFsmMakeStateValues struct { currentState *params event Event wantState *params wantNextEvent Event wantErr error wantPanic bool } type testFields struct { name string steps []pcFsmMakeStateValues } func executeProcessorTests(t *testing.T, tests []testFields) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { var state *pcState for _, step := range tt.steps { defer func() { r := recover() if (r != nil) != step.wantPanic { t.Errorf("recover = %v, wantPanic = %v", r, step.wantPanic) } }() // First step must always initialise the currentState as state. if step.currentState != nil { state = makeState(step.currentState) } if state == nil { panic("Bad (initial?) step") } nextEvent, err := state.handle(step.event) t.Log(state) assert.Equal(t, step.wantErr, err) assert.Equal(t, makeState(step.wantState), state) assert.Equal(t, step.wantNextEvent, nextEvent) // Next step may use the wantedState as their currentState. state = makeState(step.wantState) } }) } } func TestPcBlockResponse(t *testing.T) { tests := []testFields{ { name: "add one block", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{}, event: mBlockResponse("P1", 1), wantState: ¶ms{items: []pcBlock{{"P1", 1}}}, wantNextEvent: noOp, }, }, }, { name: "add two blocks", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{}, event: mBlockResponse("P1", 3), wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, }, { // use previous wantState as currentState, event: mBlockResponse("P1", 4), wantState: ¶ms{items: []pcBlock{{"P1", 3}, {"P1", 4}}}, wantNextEvent: noOp, }, }, }, { name: "add duplicate block from same peer", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{}, event: mBlockResponse("P1", 3), wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, }, { // use previous wantState as currentState, event: mBlockResponse("P1", 3), wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcDuplicateBlock{}, }, }, }, { name: "add duplicate block from different peer", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{}, event: mBlockResponse("P1", 3), wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, }, { // use previous wantState as currentState, event: mBlockResponse("P2", 3), wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcDuplicateBlock{}, }, }, }, { name: "attempt to add block with height equal to state.height", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, event: mBlockResponse("P1", 2), wantState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcShortBlock{}, }, }, }, { name: "attempt to add block with height smaller than state.height", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, event: mBlockResponse("P1", 1), wantState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcShortBlock{}, }, }, }, } executeProcessorTests(t, tests) } func TestPcProcessBlockSuccess(t *testing.T) { tests := []testFields{ { name: "noop - no blocks over current height", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{}, event: pcProcessBlock{}, wantState: ¶ms{}, wantNextEvent: noOp, }, }, }, { name: "noop - high new blocks", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, event: pcProcessBlock{}, wantState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, wantNextEvent: noOp, }, }, }, { name: "blocks H+1 and H+2 present", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: pcProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}}, blocksSynced: 1}, wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, }, }, }, { name: "blocks H+1 and H+2 present after draining", steps: []pcFsmMakeStateValues{ { // some contiguous blocks - on stop check draining is set currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}}, event: pcStop{}, wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}, draining: true}, wantNextEvent: noOp, }, { event: pcProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, }, { // finish when H+1 or/and H+2 are missing event: pcProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, wantNextEvent: noOp, wantErr: pcFinished{height: 1}, }, }, }, } executeProcessorTests(t, tests) } func TestPcProcessBlockFailures(t *testing.T) { tests := []testFields{ { name: "blocks H+1 and H+2 present from different peers - H+1 verification fails ", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, verBL: []int64{1}}, event: pcProcessBlock{}, wantState: ¶ms{items: []pcBlock{}, verBL: []int64{1}}, wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P2"}, }, }, }, { name: "blocks H+1 and H+2 present from same peer - H+1 applyBlock fails ", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, appBL: []int64{1}}, event: pcProcessBlock{}, wantState: ¶ms{items: []pcBlock{}, appBL: []int64{1}}, wantPanic: true, }, }, }, { name: "blocks H+1 and H+2 present from same peers - H+1 verification fails ", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P1", 2}, {"P2", 3}}, verBL: []int64{1}}, event: pcProcessBlock{}, wantState: ¶ms{items: []pcBlock{{"P2", 3}}, verBL: []int64{1}}, wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}, }, }, }, { name: "blocks H+1 and H+2 present from different peers - H+1 applyBlock fails ", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P2", 3}}, appBL: []int64{1}}, event: pcProcessBlock{}, wantState: ¶ms{items: []pcBlock{{"P2", 3}}, appBL: []int64{1}}, wantPanic: true, }, }, }, } executeProcessorTests(t, tests) } func TestPcPeerError(t *testing.T) { tests := []testFields{ { name: "peer not present", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: &peerError{peerID: "P3"}, wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, wantNextEvent: noOp, }, }, }, { name: "some blocks are from errored peer", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 100}, {"P1", 99}, {"P2", 101}}}, event: &peerError{peerID: "P1"}, wantState: ¶ms{items: []pcBlock{{"P2", 101}}}, wantNextEvent: noOp, }, }, }, { name: "all blocks are from errored peer", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 100}, {"P1", 99}}}, event: &peerError{peerID: "P1"}, wantState: ¶ms{}, wantNextEvent: noOp, }, }, }, } executeProcessorTests(t, tests) } func TestStop(t *testing.T) { tests := []testFields{ { name: "no blocks", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, event: pcStop{}, wantState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, wantNextEvent: noOp, wantErr: pcFinished{height: 100, blocksSynced: 100}, }, }, }, { name: "maxHeight+1 block present", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}}, blocksSynced: 100}, event: pcStop{}, wantState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}}, blocksSynced: 100}, wantNextEvent: noOp, wantErr: pcFinished{height: 100, blocksSynced: 100}, }, }, }, { name: "more blocks present", steps: []pcFsmMakeStateValues{ { currentState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}, {"P1", 102}}, blocksSynced: 100}, event: pcStop{}, wantState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}, {"P1", 102}}, blocksSynced: 100, draining: true}, wantNextEvent: noOp, wantErr: nil, }, }, }, } executeProcessorTests(t, tests) }