From 1e297ef21084571f5740d43beec14802ec3c3b64 Mon Sep 17 00:00:00 2001 From: "mark.lin" Date: Fri, 15 Sep 2017 15:01:31 +0800 Subject: [PATCH] consensus/istanbul: add tests for Istanbul core --- consensus/istanbul/core/backlog_test.go | 362 ++++++++++++++++++++ consensus/istanbul/core/commit_test.go | 325 ++++++++++++++++++ consensus/istanbul/core/core_test.go | 78 +++++ consensus/istanbul/core/handler_test.go | 127 +++++++ consensus/istanbul/core/message_set_test.go | 106 ++++++ consensus/istanbul/core/prepare_test.go | 359 +++++++++++++++++++ consensus/istanbul/core/preprepare_test.go | 298 ++++++++++++++++ consensus/istanbul/core/request_test.go | 138 ++++++++ consensus/istanbul/core/roundchange_test.go | 92 +++++ consensus/istanbul/core/roundstate_test.go | 76 ++++ consensus/istanbul/core/testbackend_test.go | 286 ++++++++++++++++ consensus/istanbul/core/types_test.go | 179 ++++++++++ 12 files changed, 2426 insertions(+) create mode 100644 consensus/istanbul/core/backlog_test.go create mode 100644 consensus/istanbul/core/commit_test.go create mode 100644 consensus/istanbul/core/core_test.go create mode 100644 consensus/istanbul/core/handler_test.go create mode 100644 consensus/istanbul/core/message_set_test.go create mode 100644 consensus/istanbul/core/prepare_test.go create mode 100644 consensus/istanbul/core/preprepare_test.go create mode 100644 consensus/istanbul/core/request_test.go create mode 100644 consensus/istanbul/core/roundchange_test.go create mode 100644 consensus/istanbul/core/roundstate_test.go create mode 100644 consensus/istanbul/core/testbackend_test.go create mode 100644 consensus/istanbul/core/types_test.go diff --git a/consensus/istanbul/core/backlog_test.go b/consensus/istanbul/core/backlog_test.go new file mode 100644 index 0000000000..4e00552ab0 --- /dev/null +++ b/consensus/istanbul/core/backlog_test.go @@ -0,0 +1,362 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/consensus/istanbul/validator" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "gopkg.in/karalabe/cookiejar.v2/collections/prque" +) + +func TestCheckMessage(t *testing.T) { + c := &core{ + state: StateAcceptRequest, + current: newRoundState(&istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(0), + }, newTestValidatorSet(4), common.Hash{}, nil, nil, nil), + } + + // invalid view format + err := c.checkMessage(msgPreprepare, nil) + if err != errInvalidMessage { + t.Errorf("error mismatch: have %v, want %v", err, errInvalidMessage) + } + + testStates := []State{StateAcceptRequest, StatePreprepared, StatePrepared, StateCommitted} + testCode := []uint64{msgPreprepare, msgPrepare, msgCommit, msgRoundChange} + + // future sequence + v := &istanbul.View{ + Sequence: big.NewInt(2), + Round: big.NewInt(0), + } + for i := 0; i < len(testStates); i++ { + c.state = testStates[i] + for j := 0; j < len(testCode); j++ { + err := c.checkMessage(testCode[j], v) + if err != errFutureMessage { + t.Errorf("error mismatch: have %v, want %v", err, errFutureMessage) + } + } + } + + // future round + v = &istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(1), + } + for i := 0; i < len(testStates); i++ { + c.state = testStates[i] + for j := 0; j < len(testCode); j++ { + err := c.checkMessage(testCode[j], v) + if testCode[j] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if err != errFutureMessage { + t.Errorf("error mismatch: have %v, want %v", err, errFutureMessage) + } + } + } + + // current view but waiting for round change + v = &istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(0), + } + c.waitingForRoundChange = true + for i := 0; i < len(testStates); i++ { + c.state = testStates[i] + for j := 0; j < len(testCode); j++ { + err := c.checkMessage(testCode[j], v) + if testCode[j] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if err != errFutureMessage { + t.Errorf("error mismatch: have %v, want %v", err, errFutureMessage) + } + } + } + c.waitingForRoundChange = false + + v = c.currentView() + // current view, state = StateAcceptRequest + c.state = StateAcceptRequest + for i := 0; i < len(testCode); i++ { + err = c.checkMessage(testCode[i], v) + if testCode[i] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if testCode[i] == msgPreprepare { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else { + if err != errFutureMessage { + t.Errorf("error mismatch: have %v, want %v", err, errFutureMessage) + } + } + } + + // current view, state = StatePreprepared + c.state = StatePreprepared + for i := 0; i < len(testCode); i++ { + err = c.checkMessage(testCode[i], v) + if testCode[i] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } + + // current view, state = StatePrepared + c.state = StatePrepared + for i := 0; i < len(testCode); i++ { + err = c.checkMessage(testCode[i], v) + if testCode[i] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } + + // current view, state = StateCommitted + c.state = StateCommitted + for i := 0; i < len(testCode); i++ { + err = c.checkMessage(testCode[i], v) + if testCode[i] == msgRoundChange { + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } else if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + } + +} + +func TestStoreBacklog(t *testing.T) { + c := &core{ + logger: log.New("backend", "test", "id", 0), + backlogs: make(map[istanbul.Validator]*prque.Prque), + backlogsMu: new(sync.Mutex), + } + v := &istanbul.View{ + Round: big.NewInt(10), + Sequence: big.NewInt(10), + } + p := validator.New(common.StringToAddress("12345667890")) + // push preprepare msg + preprepare := &istanbul.Preprepare{ + View: v, + Proposal: makeBlock(1), + } + prepreparePayload, _ := Encode(preprepare) + m := &message{ + Code: msgPreprepare, + Msg: prepreparePayload, + } + c.storeBacklog(m, p) + msg := c.backlogs[p].PopItem() + if !reflect.DeepEqual(msg, m) { + t.Errorf("message mismatch: have %v, want %v", msg, m) + } + + // push prepare msg + subject := &istanbul.Subject{ + View: v, + Digest: common.StringToHash("1234567890"), + } + subjectPayload, _ := Encode(subject) + + m = &message{ + Code: msgPrepare, + Msg: subjectPayload, + } + c.storeBacklog(m, p) + msg = c.backlogs[p].PopItem() + if !reflect.DeepEqual(msg, m) { + t.Errorf("message mismatch: have %v, want %v", msg, m) + } + + // push commit msg + m = &message{ + Code: msgCommit, + Msg: subjectPayload, + } + c.storeBacklog(m, p) + msg = c.backlogs[p].PopItem() + if !reflect.DeepEqual(msg, m) { + t.Errorf("message mismatch: have %v, want %v", msg, m) + } + + // push roundChange msg + m = &message{ + Code: msgRoundChange, + Msg: subjectPayload, + } + c.storeBacklog(m, p) + msg = c.backlogs[p].PopItem() + if !reflect.DeepEqual(msg, m) { + t.Errorf("message mismatch: have %v, want %v", msg, m) + } +} + +func TestProcessFutureBacklog(t *testing.T) { + backend := &testSystemBackend{ + events: new(event.TypeMux), + } + c := &core{ + logger: log.New("backend", "test", "id", 0), + backlogs: make(map[istanbul.Validator]*prque.Prque), + backlogsMu: new(sync.Mutex), + backend: backend, + current: newRoundState(&istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(0), + }, newTestValidatorSet(4), common.Hash{}, nil, nil, nil), + state: StateAcceptRequest, + } + c.subscribeEvents() + defer c.unsubscribeEvents() + + v := &istanbul.View{ + Round: big.NewInt(10), + Sequence: big.NewInt(10), + } + p := validator.New(common.StringToAddress("12345667890")) + // push a future msg + subject := &istanbul.Subject{ + View: v, + Digest: common.StringToHash("1234567890"), + } + subjectPayload, _ := Encode(subject) + m := &message{ + Code: msgCommit, + Msg: subjectPayload, + } + c.storeBacklog(m, p) + c.processBacklog() + + const timeoutDura = 2 * time.Second + timeout := time.NewTimer(timeoutDura) + select { + case e, ok := <-c.events.Chan(): + if !ok { + return + } + t.Errorf("unexpected events comes: %v", e) + case <-timeout.C: + // success + } +} + +func TestProcessBacklog(t *testing.T) { + v := &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(1), + } + preprepare := &istanbul.Preprepare{ + View: v, + Proposal: makeBlock(1), + } + prepreparePayload, _ := Encode(preprepare) + + subject := &istanbul.Subject{ + View: v, + Digest: common.StringToHash("1234567890"), + } + subjectPayload, _ := Encode(subject) + + msgs := []*message{ + { + Code: msgPreprepare, + Msg: prepreparePayload, + }, + { + Code: msgPrepare, + Msg: subjectPayload, + }, + { + Code: msgCommit, + Msg: subjectPayload, + }, + { + Code: msgRoundChange, + Msg: subjectPayload, + }, + } + for i := 0; i < len(msgs); i++ { + testProcessBacklog(t, msgs[i]) + } +} + +func testProcessBacklog(t *testing.T, msg *message) { + vset := newTestValidatorSet(1) + backend := &testSystemBackend{ + events: new(event.TypeMux), + peers: vset, + } + c := &core{ + logger: log.New("backend", "test", "id", 0), + backlogs: make(map[istanbul.Validator]*prque.Prque), + backlogsMu: new(sync.Mutex), + backend: backend, + state: State(msg.Code), + current: newRoundState(&istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(0), + }, newTestValidatorSet(4), common.Hash{}, nil, nil, nil), + } + c.subscribeEvents() + defer c.unsubscribeEvents() + + c.storeBacklog(msg, vset.GetByIndex(0)) + c.processBacklog() + + const timeoutDura = 2 * time.Second + timeout := time.NewTimer(timeoutDura) + select { + case ev := <-c.events.Chan(): + e, ok := ev.Data.(backlogEvent) + if !ok { + t.Errorf("unexpected event comes: %v", reflect.TypeOf(ev.Data)) + } + if e.msg.Code != msg.Code { + t.Errorf("message code mismatch: have %v, want %v", e.msg.Code, msg.Code) + } + // success + case <-timeout.C: + t.Error("unexpected timeout occurs") + } +} diff --git a/consensus/istanbul/core/commit_test.go b/consensus/istanbul/core/commit_test.go new file mode 100644 index 0000000000..707b94d1f4 --- /dev/null +++ b/consensus/istanbul/core/commit_test.go @@ -0,0 +1,325 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/consensus/istanbul/validator" + "github.com/ethereum/go-ethereum/crypto" +) + +func TestHandleCommit(t *testing.T) { + N := uint64(4) + F := uint64(1) + + proposal := newTestProposal() + expectedSubject := &istanbul.Subject{ + View: &istanbul.View{ + Round: big.NewInt(0), + Sequence: proposal.Number(), + }, + Digest: proposal.Hash(), + } + + testCases := []struct { + system *testSystem + expectedErr error + }{ + { + // normal case + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(1), + }, + c.valSet, + ) + + if i == 0 { + // replica 0 is the proposer + c.state = StatePrepared + } + } + return sys + }(), + nil, + }, + { + // future message + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i == 0 { + // replica 0 is the proposer + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + c.state = StatePreprepared + } else { + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(2), + Sequence: big.NewInt(3), + }, + c.valSet, + ) + } + } + return sys + }(), + errFutureMessage, + }, + { + // subject not match + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i == 0 { + // replica 0 is the proposer + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + c.state = StatePreprepared + } else { + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(0), + }, + c.valSet, + ) + } + } + return sys + }(), + errOldMessage, + }, + { + // jump state + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: proposal.Number(), + }, + c.valSet, + ) + + // only replica0 stays at StatePreprepared + // other replicas are at StatePrepared + if i != 0 { + c.state = StatePrepared + } else { + c.state = StatePreprepared + } + } + return sys + }(), + nil, + }, + // TODO: double send message + } + +OUTER: + for _, test := range testCases { + test.system.Run(false) + + v0 := test.system.backends[0] + r0 := v0.engine.(*core) + + for i, v := range test.system.backends { + validator := r0.valSet.GetByIndex(uint64(i)) + m, _ := Encode(v.engine.(*core).current.Subject()) + if err := r0.handleCommit(&message{ + Code: msgCommit, + Msg: m, + Address: validator.Address(), + Signature: []byte{}, + CommittedSeal: validator.Address().Bytes(), // small hack + }, validator); err != nil { + if err != test.expectedErr { + t.Errorf("error mismatch: have %v, want %v", err, test.expectedErr) + } + if r0.current.IsHashLocked() { + t.Errorf("block should not be locked") + } + continue OUTER + } + } + + // prepared is normal case + if r0.state != StateCommitted { + // There are not enough commit messages in core + if r0.state != StatePrepared { + t.Errorf("state mismatch: have %v, want %v", r0.state, StatePrepared) + } + if r0.current.Commits.Size() > 2*r0.valSet.F() { + t.Errorf("the size of commit messages should be less than %v", 2*r0.valSet.F()+1) + } + if r0.current.IsHashLocked() { + t.Errorf("block should not be locked") + } + continue + } + + // core should have 2F+1 prepare messages + if r0.current.Commits.Size() <= 2*r0.valSet.F() { + t.Errorf("the size of commit messages should be larger than 2F+1: size %v", r0.current.Commits.Size()) + } + + // check signatures large than 2F+1 + signedCount := 0 + committedSeals := v0.committedMsgs[0].committedSeals + for _, validator := range r0.valSet.List() { + for _, seal := range committedSeals { + if bytes.Equal(validator.Address().Bytes(), seal[:common.AddressLength]) { + signedCount++ + break + } + } + } + if signedCount <= 2*r0.valSet.F() { + t.Errorf("the expected signed count should be larger than %v, but got %v", 2*r0.valSet.F(), signedCount) + } + if !r0.current.IsHashLocked() { + t.Errorf("block should be locked") + } + } +} + +// round is not checked for now +func TestVerifyCommit(t *testing.T) { + // for log purpose + privateKey, _ := crypto.GenerateKey() + peer := validator.New(getPublicKeyAddress(privateKey)) + valSet := validator.NewSet([]common.Address{peer.Address()}, istanbul.RoundRobin) + + sys := NewTestSystemWithBackend(uint64(1), uint64(0)) + + testCases := []struct { + expected error + commit *istanbul.Subject + roundState *roundState + }{ + { + // normal case + expected: nil, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + { + // old message + expected: errInconsistentSubject, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // different digest + expected: errInconsistentSubject, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: common.StringToHash("1234567890"), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // malicious package(lack of sequence) + expected: errInconsistentSubject, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: nil}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // wrong prepare message with same sequence but different round + expected: errInconsistentSubject, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + { + // wrong prepare message with same round but different sequence + expected: errInconsistentSubject, + commit: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(1)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + } + for i, test := range testCases { + c := sys.backends[0].engine.(*core) + c.current = test.roundState + + if err := c.verifyCommit(test.commit, peer); err != nil { + if err != test.expected { + t.Errorf("result %d: error mismatch: have %v, want %v", i, err, test.expected) + } + } + } +} diff --git a/consensus/istanbul/core/core_test.go b/consensus/istanbul/core/core_test.go new file mode 100644 index 0000000000..b6aa697f85 --- /dev/null +++ b/consensus/istanbul/core/core_test.go @@ -0,0 +1,78 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/core/types" + elog "github.com/ethereum/go-ethereum/log" +) + +func makeBlock(number int64) *types.Block { + header := &types.Header{ + Difficulty: big.NewInt(0), + Number: big.NewInt(number), + GasLimit: 0, + GasUsed: 0, + Time: big.NewInt(0), + } + block := &types.Block{} + return block.WithSeal(header) +} + +func newTestProposal() istanbul.Proposal { + return makeBlock(1) +} + +func TestNewRequest(t *testing.T) { + testLogger.SetHandler(elog.StdoutHandler) + + N := uint64(4) + F := uint64(1) + + sys := NewTestSystemWithBackend(N, F) + + close := sys.Run(true) + defer close() + + request1 := makeBlock(1) + sys.backends[0].NewRequest(request1) + + <-time.After(1 * time.Second) + + request2 := makeBlock(2) + sys.backends[0].NewRequest(request2) + + <-time.After(1 * time.Second) + + for _, backend := range sys.backends { + if len(backend.committedMsgs) != 2 { + t.Errorf("the number of executed requests mismatch: have %v, want 2", len(backend.committedMsgs)) + } + if !reflect.DeepEqual(request1.Number(), backend.committedMsgs[0].commitProposal.Number()) { + t.Errorf("the number of requests mismatch: have %v, want %v", request1.Number(), backend.committedMsgs[0].commitProposal.Number()) + } + if !reflect.DeepEqual(request2.Number(), backend.committedMsgs[1].commitProposal.Number()) { + t.Errorf("the number of requests mismatch: have %v, want %v", request2.Number(), backend.committedMsgs[1].commitProposal.Number()) + } + } +} diff --git a/consensus/istanbul/core/handler_test.go b/consensus/istanbul/core/handler_test.go new file mode 100644 index 0000000000..a54b1f9ccc --- /dev/null +++ b/consensus/istanbul/core/handler_test.go @@ -0,0 +1,127 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +// notice: the normal case have been tested in integration tests. +func TestHandleMsg(t *testing.T) { + N := uint64(4) + F := uint64(1) + sys := NewTestSystemWithBackend(N, F) + + closer := sys.Run(true) + defer closer() + + v0 := sys.backends[0] + r0 := v0.engine.(*core) + + m, _ := Encode(&istanbul.Subject{ + View: &istanbul.View{ + Sequence: big.NewInt(0), + Round: big.NewInt(0), + }, + Digest: common.StringToHash("1234567890"), + }) + // with a matched payload. msgPreprepare should match with *istanbul.Preprepare in normal case. + msg := &message{ + Code: msgPreprepare, + Msg: m, + Address: v0.Address(), + Signature: []byte{}, + CommittedSeal: []byte{}, + } + + _, val := v0.Validators(nil).GetByAddress(v0.Address()) + if err := r0.handleCheckedMsg(msg, val); err != errFailedDecodePreprepare { + t.Errorf("error mismatch: have %v, want %v", err, errFailedDecodePreprepare) + } + + m, _ = Encode(&istanbul.Preprepare{ + View: &istanbul.View{ + Sequence: big.NewInt(0), + Round: big.NewInt(0), + }, + Proposal: makeBlock(1), + }) + // with a unmatched payload. msgPrepare should match with *istanbul.Subject in normal case. + msg = &message{ + Code: msgPrepare, + Msg: m, + Address: v0.Address(), + Signature: []byte{}, + CommittedSeal: []byte{}, + } + + _, val = v0.Validators(nil).GetByAddress(v0.Address()) + if err := r0.handleCheckedMsg(msg, val); err != errFailedDecodePrepare { + t.Errorf("error mismatch: have %v, want %v", err, errFailedDecodePreprepare) + } + + m, _ = Encode(&istanbul.Preprepare{ + View: &istanbul.View{ + Sequence: big.NewInt(0), + Round: big.NewInt(0), + }, + Proposal: makeBlock(2), + }) + // with a unmatched payload. istanbul.MsgCommit should match with *istanbul.Subject in normal case. + msg = &message{ + Code: msgCommit, + Msg: m, + Address: v0.Address(), + Signature: []byte{}, + CommittedSeal: []byte{}, + } + + _, val = v0.Validators(nil).GetByAddress(v0.Address()) + if err := r0.handleCheckedMsg(msg, val); err != errFailedDecodeCommit { + t.Errorf("error mismatch: have %v, want %v", err, errFailedDecodeCommit) + } + + m, _ = Encode(&istanbul.Preprepare{ + View: &istanbul.View{ + Sequence: big.NewInt(0), + Round: big.NewInt(0), + }, + Proposal: makeBlock(3), + }) + // invalid message code. message code is not exists in list + msg = &message{ + Code: uint64(99), + Msg: m, + Address: v0.Address(), + Signature: []byte{}, + CommittedSeal: []byte{}, + } + + _, val = v0.Validators(nil).GetByAddress(v0.Address()) + if err := r0.handleCheckedMsg(msg, val); err == nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + // with malicious payload + if err := r0.handleMsg([]byte{1}); err == nil { + t.Errorf("error mismatch: have %v, want nil", err) + } +} diff --git a/consensus/istanbul/core/message_set_test.go b/consensus/istanbul/core/message_set_test.go new file mode 100644 index 0000000000..bd76b5b10c --- /dev/null +++ b/consensus/istanbul/core/message_set_test.go @@ -0,0 +1,106 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/rlp" +) + +func TestMessageSetWithPreprepare(t *testing.T) { + valSet := newTestValidatorSet(4) + + ms := newMessageSet(valSet) + + view := &istanbul.View{ + Round: new(big.Int), + Sequence: new(big.Int), + } + pp := &istanbul.Preprepare{ + View: view, + Proposal: makeBlock(1), + } + + rawPP, err := rlp.EncodeToBytes(pp) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + msg := &message{ + Code: msgPreprepare, + Msg: rawPP, + Address: valSet.GetProposer().Address(), + } + + err = ms.Add(msg) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + err = ms.Add(msg) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + if ms.Size() != 1 { + t.Errorf("the size of message set mismatch: have %v, want 1", ms.Size()) + } +} + +func TestMessageSetWithSubject(t *testing.T) { + valSet := newTestValidatorSet(4) + + ms := newMessageSet(valSet) + + view := &istanbul.View{ + Round: new(big.Int), + Sequence: new(big.Int), + } + + sub := &istanbul.Subject{ + View: view, + Digest: common.StringToHash("1234567890"), + } + + rawSub, err := rlp.EncodeToBytes(sub) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + msg := &message{ + Code: msgPrepare, + Msg: rawSub, + Address: valSet.GetProposer().Address(), + } + + err = ms.Add(msg) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + err = ms.Add(msg) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + if ms.Size() != 1 { + t.Errorf("the size of message set mismatch: have %v, want 1", ms.Size()) + } +} diff --git a/consensus/istanbul/core/prepare_test.go b/consensus/istanbul/core/prepare_test.go new file mode 100644 index 0000000000..a2b495d85f --- /dev/null +++ b/consensus/istanbul/core/prepare_test.go @@ -0,0 +1,359 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/consensus/istanbul/validator" + "github.com/ethereum/go-ethereum/crypto" +) + +func TestHandlePrepare(t *testing.T) { + N := uint64(4) + F := uint64(1) + + proposal := newTestProposal() + expectedSubject := &istanbul.Subject{ + View: &istanbul.View{ + Round: big.NewInt(0), + Sequence: proposal.Number(), + }, + Digest: proposal.Hash(), + } + + testCases := []struct { + system *testSystem + expectedErr error + }{ + { + // normal case + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(1), + }, + c.valSet, + ) + + if i == 0 { + // replica 0 is the proposer + c.state = StatePreprepared + } + } + return sys + }(), + nil, + }, + { + // future message + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i == 0 { + // replica 0 is the proposer + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + c.state = StatePreprepared + } else { + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(2), + Sequence: big.NewInt(3), + }, + c.valSet, + ) + } + } + return sys + }(), + errFutureMessage, + }, + { + // subject not match + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i == 0 { + // replica 0 is the proposer + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + c.state = StatePreprepared + } else { + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(0), + }, + c.valSet, + ) + } + } + return sys + }(), + errOldMessage, + }, + { + // subject not match + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i == 0 { + // replica 0 is the proposer + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + c.state = StatePreprepared + } else { + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(1)}, + c.valSet, + ) + } + } + return sys + }(), + errInconsistentSubject, + }, + { + // less than 2F+1 + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + // save less than 2*F+1 replica + sys.backends = sys.backends[2*int(F)+1:] + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + c.current = newTestRoundState( + expectedSubject.View, + c.valSet, + ) + + if i == 0 { + // replica 0 is the proposer + c.state = StatePreprepared + } + } + return sys + }(), + nil, + }, + // TODO: double send message + } + +OUTER: + for _, test := range testCases { + test.system.Run(false) + + v0 := test.system.backends[0] + r0 := v0.engine.(*core) + + for i, v := range test.system.backends { + validator := r0.valSet.GetByIndex(uint64(i)) + m, _ := Encode(v.engine.(*core).current.Subject()) + if err := r0.handlePrepare(&message{ + Code: msgPrepare, + Msg: m, + Address: validator.Address(), + }, validator); err != nil { + if err != test.expectedErr { + t.Errorf("error mismatch: have %v, want %v", err, test.expectedErr) + } + if r0.current.IsHashLocked() { + t.Errorf("block should not be locked") + } + continue OUTER + } + } + + // prepared is normal case + if r0.state != StatePrepared { + // There are not enough PREPARE messages in core + if r0.state != StatePreprepared { + t.Errorf("state mismatch: have %v, want %v", r0.state, StatePreprepared) + } + if r0.current.Prepares.Size() > 2*r0.valSet.F() { + t.Errorf("the size of PREPARE messages should be less than %v", 2*r0.valSet.F()+1) + } + if r0.current.IsHashLocked() { + t.Errorf("block should not be locked") + } + + continue + } + + // core should have 2F+1 PREPARE messages + if r0.current.Prepares.Size() <= 2*r0.valSet.F() { + t.Errorf("the size of PREPARE messages should be larger than 2F+1: size %v", r0.current.Commits.Size()) + } + + // a message will be delivered to backend if 2F+1 + if int64(len(v0.sentMsgs)) != 1 { + t.Errorf("the Send() should be called once: times %v", len(test.system.backends[0].sentMsgs)) + } + + // verify COMMIT messages + decodedMsg := new(message) + err := decodedMsg.FromPayload(v0.sentMsgs[0], nil) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + if decodedMsg.Code != msgCommit { + t.Errorf("message code mismatch: have %v, want %v", decodedMsg.Code, msgCommit) + } + var m *istanbul.Subject + err = decodedMsg.Decode(&m) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + if !reflect.DeepEqual(m, expectedSubject) { + t.Errorf("subject mismatch: have %v, want %v", m, expectedSubject) + } + if !r0.current.IsHashLocked() { + t.Errorf("block should be locked") + } + } +} + +// round is not checked for now +func TestVerifyPrepare(t *testing.T) { + // for log purpose + privateKey, _ := crypto.GenerateKey() + peer := validator.New(getPublicKeyAddress(privateKey)) + valSet := validator.NewSet([]common.Address{peer.Address()}, istanbul.RoundRobin) + + sys := NewTestSystemWithBackend(uint64(1), uint64(0)) + + testCases := []struct { + expected error + + prepare *istanbul.Subject + roundState *roundState + }{ + { + // normal case + expected: nil, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + { + // old message + expected: errInconsistentSubject, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // different digest + expected: errInconsistentSubject, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + Digest: common.StringToHash("1234567890"), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // malicious package(lack of sequence) + expected: errInconsistentSubject, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: nil}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(1)}, + valSet, + ), + }, + { + // wrong PREPARE message with same sequence but different round + expected: errInconsistentSubject, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(1), Sequence: big.NewInt(0)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + { + // wrong PREPARE message with same round but different sequence + expected: errInconsistentSubject, + prepare: &istanbul.Subject{ + View: &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(1)}, + Digest: newTestProposal().Hash(), + }, + roundState: newTestRoundState( + &istanbul.View{Round: big.NewInt(0), Sequence: big.NewInt(0)}, + valSet, + ), + }, + } + for i, test := range testCases { + c := sys.backends[0].engine.(*core) + c.current = test.roundState + + if err := c.verifyPrepare(test.prepare, peer); err != nil { + if err != test.expected { + t.Errorf("result %d: error mismatch: have %v, want %v", i, err, test.expected) + } + } + } +} diff --git a/consensus/istanbul/core/preprepare_test.go b/consensus/istanbul/core/preprepare_test.go new file mode 100644 index 0000000000..1097cfeba9 --- /dev/null +++ b/consensus/istanbul/core/preprepare_test.go @@ -0,0 +1,298 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +func newTestPreprepare(v *istanbul.View) *istanbul.Preprepare { + return &istanbul.Preprepare{ + View: v, + Proposal: newTestProposal(), + } +} + +func TestHandlePreprepare(t *testing.T) { + N := uint64(4) // replica 0 is the proposer, it will send messages to others + F := uint64(1) // F does not affect tests + + testCases := []struct { + system *testSystem + expectedRequest istanbul.Proposal + expectedErr error + existingBlock bool + }{ + { + // normal case + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i != 0 { + c.state = StateAcceptRequest + } + } + return sys + }(), + newTestProposal(), + nil, + false, + }, + { + // future message + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i != 0 { + c.state = StateAcceptRequest + // hack: force set subject that future message can be simulated + c.current = newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(0), + }, + c.valSet, + ) + + } else { + c.current.SetSequence(big.NewInt(10)) + } + } + return sys + }(), + makeBlock(1), + errFutureMessage, + false, + }, + { + // non-proposer + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + // force remove replica 0, let replica 1 be the proposer + sys.backends = sys.backends[1:] + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i != 0 { + // replica 0 is the proposer + c.state = StatePreprepared + } + } + return sys + }(), + makeBlock(1), + errNotFromProposer, + false, + }, + { + // errOldMessage + func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i != 0 { + c.state = StatePreprepared + c.current.SetSequence(big.NewInt(10)) + c.current.SetRound(big.NewInt(10)) + } + } + return sys + }(), + makeBlock(1), + errOldMessage, + false, + }, + } + +OUTER: + for _, test := range testCases { + test.system.Run(false) + + v0 := test.system.backends[0] + r0 := v0.engine.(*core) + + curView := r0.currentView() + + preprepare := &istanbul.Preprepare{ + View: curView, + Proposal: test.expectedRequest, + } + + for i, v := range test.system.backends { + // i == 0 is primary backend, it is responsible for send PRE-PREPARE messages to others. + if i == 0 { + continue + } + + c := v.engine.(*core) + + m, _ := Encode(preprepare) + _, val := r0.valSet.GetByAddress(v0.Address()) + // run each backends and verify handlePreprepare function. + if err := c.handlePreprepare(&message{ + Code: msgPreprepare, + Msg: m, + Address: v0.Address(), + }, val); err != nil { + if err != test.expectedErr { + t.Errorf("error mismatch: have %v, want %v", err, test.expectedErr) + } + continue OUTER + } + + if c.state != StatePreprepared { + t.Errorf("state mismatch: have %v, want %v", c.state, StatePreprepared) + } + + if !test.existingBlock && !reflect.DeepEqual(c.current.Subject().View, curView) { + t.Errorf("view mismatch: have %v, want %v", c.current.Subject().View, curView) + } + + // verify prepare messages + decodedMsg := new(message) + err := decodedMsg.FromPayload(v.sentMsgs[0], nil) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + expectedCode := msgPrepare + if test.existingBlock { + expectedCode = msgCommit + } + if decodedMsg.Code != expectedCode { + t.Errorf("message code mismatch: have %v, want %v", decodedMsg.Code, expectedCode) + } + + var subject *istanbul.Subject + err = decodedMsg.Decode(&subject) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + if !test.existingBlock && !reflect.DeepEqual(subject, c.current.Subject()) { + t.Errorf("subject mismatch: have %v, want %v", subject, c.current.Subject()) + } + + } + } +} + +func TestHandlePreprepareWithLock(t *testing.T) { + N := uint64(4) // replica 0 is the proposer, it will send messages to others + F := uint64(1) // F does not affect tests + proposal := newTestProposal() + mismatchProposal := makeBlock(10) + newSystem := func() *testSystem { + sys := NewTestSystemWithBackend(N, F) + + for i, backend := range sys.backends { + c := backend.engine.(*core) + c.valSet = backend.peers + if i != 0 { + c.state = StateAcceptRequest + } + c.roundChangeSet = newRoundChangeSet(c.valSet) + } + return sys + } + + testCases := []struct { + system *testSystem + proposal istanbul.Proposal + lockProposal istanbul.Proposal + }{ + { + newSystem(), + proposal, + proposal, + }, + { + newSystem(), + proposal, + mismatchProposal, + }, + } + + for _, test := range testCases { + test.system.Run(false) + v0 := test.system.backends[0] + r0 := v0.engine.(*core) + curView := r0.currentView() + preprepare := &istanbul.Preprepare{ + View: curView, + Proposal: test.proposal, + } + lockPreprepare := &istanbul.Preprepare{ + View: curView, + Proposal: test.lockProposal, + } + + for i, v := range test.system.backends { + // i == 0 is primary backend, it is responsible for send PRE-PREPARE messages to others. + if i == 0 { + continue + } + + c := v.engine.(*core) + c.current.SetPreprepare(lockPreprepare) + c.current.LockHash() + m, _ := Encode(preprepare) + _, val := r0.valSet.GetByAddress(v0.Address()) + if err := c.handlePreprepare(&message{ + Code: msgPreprepare, + Msg: m, + Address: v0.Address(), + }, val); err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + if test.proposal == test.lockProposal { + if c.state != StatePrepared { + t.Errorf("state mismatch: have %v, want %v", c.state, StatePreprepared) + } + if !reflect.DeepEqual(curView, c.currentView()) { + t.Errorf("view mismatch: have %v, want %v", c.currentView(), curView) + } + } else { + // Should stay at StateAcceptRequest + if c.state != StateAcceptRequest { + t.Errorf("state mismatch: have %v, want %v", c.state, StateAcceptRequest) + } + // Should have triggered a round change + expectedView := &istanbul.View{ + Sequence: curView.Sequence, + Round: big.NewInt(1), + } + if !reflect.DeepEqual(expectedView, c.currentView()) { + t.Errorf("view mismatch: have %v, want %v", c.currentView(), expectedView) + } + } + } + } +} diff --git a/consensus/istanbul/core/request_test.go b/consensus/istanbul/core/request_test.go new file mode 100644 index 0000000000..5ee179c1c8 --- /dev/null +++ b/consensus/istanbul/core/request_test.go @@ -0,0 +1,138 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "gopkg.in/karalabe/cookiejar.v2/collections/prque" +) + +func TestCheckRequestMsg(t *testing.T) { + c := &core{ + state: StateAcceptRequest, + current: newRoundState(&istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(0), + }, newTestValidatorSet(4), common.Hash{}, nil, nil, nil), + } + + // invalid request + err := c.checkRequestMsg(nil) + if err != errInvalidMessage { + t.Errorf("error mismatch: have %v, want %v", err, errInvalidMessage) + } + r := &istanbul.Request{ + Proposal: nil, + } + err = c.checkRequestMsg(r) + if err != errInvalidMessage { + t.Errorf("error mismatch: have %v, want %v", err, errInvalidMessage) + } + + // old request + r = &istanbul.Request{ + Proposal: makeBlock(0), + } + err = c.checkRequestMsg(r) + if err != errOldMessage { + t.Errorf("error mismatch: have %v, want %v", err, errOldMessage) + } + + // future request + r = &istanbul.Request{ + Proposal: makeBlock(2), + } + err = c.checkRequestMsg(r) + if err != errFutureMessage { + t.Errorf("error mismatch: have %v, want %v", err, errFutureMessage) + } + + // current request + r = &istanbul.Request{ + Proposal: makeBlock(1), + } + err = c.checkRequestMsg(r) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } +} + +func TestStoreRequestMsg(t *testing.T) { + backend := &testSystemBackend{ + events: new(event.TypeMux), + } + c := &core{ + logger: log.New("backend", "test", "id", 0), + backend: backend, + state: StateAcceptRequest, + current: newRoundState(&istanbul.View{ + Sequence: big.NewInt(0), + Round: big.NewInt(0), + }, newTestValidatorSet(4), common.Hash{}, nil, nil, nil), + pendingRequests: prque.New(), + pendingRequestsMu: new(sync.Mutex), + } + requests := []istanbul.Request{ + { + Proposal: makeBlock(1), + }, + { + Proposal: makeBlock(2), + }, + { + Proposal: makeBlock(3), + }, + } + + c.storeRequestMsg(&requests[1]) + c.storeRequestMsg(&requests[0]) + c.storeRequestMsg(&requests[2]) + if c.pendingRequests.Size() != len(requests) { + t.Errorf("the size of pending requests mismatch: have %v, want %v", c.pendingRequests.Size(), len(requests)) + } + + c.current.sequence = big.NewInt(3) + + c.subscribeEvents() + defer c.unsubscribeEvents() + + c.processPendingRequests() + + const timeoutDura = 2 * time.Second + timeout := time.NewTimer(timeoutDura) + select { + case ev := <-c.events.Chan(): + e, ok := ev.Data.(istanbul.RequestEvent) + if !ok { + t.Errorf("unexpected event comes: %v", reflect.TypeOf(ev.Data)) + } + if e.Proposal.Number().Cmp(requests[2].Proposal.Number()) != 0 { + t.Errorf("the number of proposal mismatch: have %v, want %v", e.Proposal.Number(), requests[2].Proposal.Number()) + } + case <-timeout.C: + t.Error("unexpected timeout occurs") + } +} diff --git a/consensus/istanbul/core/roundchange_test.go b/consensus/istanbul/core/roundchange_test.go new file mode 100644 index 0000000000..835219ae81 --- /dev/null +++ b/consensus/istanbul/core/roundchange_test.go @@ -0,0 +1,92 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/consensus/istanbul/validator" +) + +func TestRoundChangeSet(t *testing.T) { + vset := validator.NewSet(generateValidators(4), istanbul.RoundRobin) + rc := newRoundChangeSet(vset) + + view := &istanbul.View{ + Sequence: big.NewInt(1), + Round: big.NewInt(1), + } + r := &istanbul.Subject{ + View: view, + Digest: common.Hash{}, + } + m, _ := Encode(r) + + // Test Add() + // Add message from all validators + for i, v := range vset.List() { + msg := &message{ + Code: msgRoundChange, + Msg: m, + Address: v.Address(), + } + rc.Add(view.Round, msg) + if rc.roundChanges[view.Round.Uint64()].Size() != i+1 { + t.Errorf("the size of round change messages mismatch: have %v, want %v", rc.roundChanges[view.Round.Uint64()].Size(), i+1) + } + } + + // Add message again from all validators, but the size should be the same + for _, v := range vset.List() { + msg := &message{ + Code: msgRoundChange, + Msg: m, + Address: v.Address(), + } + rc.Add(view.Round, msg) + if rc.roundChanges[view.Round.Uint64()].Size() != vset.Size() { + t.Errorf("the size of round change messages mismatch: have %v, want %v", rc.roundChanges[view.Round.Uint64()].Size(), vset.Size()) + } + } + + // Test MaxRound() + for i := 0; i < 10; i++ { + maxRound := rc.MaxRound(i) + if i <= vset.Size() { + if maxRound == nil || maxRound.Cmp(view.Round) != 0 { + t.Errorf("max round mismatch: have %v, want %v", maxRound, view.Round) + } + } else if maxRound != nil { + t.Errorf("max round mismatch: have %v, want nil", maxRound) + } + } + + // Test Clear() + for i := int64(0); i < 2; i++ { + rc.Clear(big.NewInt(i)) + if rc.roundChanges[view.Round.Uint64()].Size() != vset.Size() { + t.Errorf("the size of round change messages mismatch: have %v, want %v", rc.roundChanges[view.Round.Uint64()].Size(), vset.Size()) + } + } + rc.Clear(big.NewInt(2)) + if rc.roundChanges[view.Round.Uint64()] != nil { + t.Errorf("the change messages mismatch: have %v, want nil", rc.roundChanges[view.Round.Uint64()]) + } +} diff --git a/consensus/istanbul/core/roundstate_test.go b/consensus/istanbul/core/roundstate_test.go new file mode 100644 index 0000000000..7cf1979c76 --- /dev/null +++ b/consensus/istanbul/core/roundstate_test.go @@ -0,0 +1,76 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +func newTestRoundState(view *istanbul.View, validatorSet istanbul.ValidatorSet) *roundState { + return &roundState{ + round: view.Round, + sequence: view.Sequence, + Preprepare: newTestPreprepare(view), + Prepares: newMessageSet(validatorSet), + Commits: newMessageSet(validatorSet), + mu: new(sync.RWMutex), + hasBadProposal: func(hash common.Hash) bool { + return false + }, + } +} + +func TestLockHash(t *testing.T) { + sys := NewTestSystemWithBackend(1, 0) + rs := newTestRoundState( + &istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(0), + }, + sys.backends[0].peers, + ) + if !common.EmptyHash(rs.GetLockedHash()) { + t.Errorf("error mismatch: have %v, want empty", rs.GetLockedHash()) + } + if rs.IsHashLocked() { + t.Error("IsHashLocked should return false") + } + + // Lock + expected := rs.Proposal().Hash() + rs.LockHash() + if expected != rs.GetLockedHash() { + t.Errorf("error mismatch: have %v, want %v", rs.GetLockedHash(), expected) + } + if !rs.IsHashLocked() { + t.Error("IsHashLocked should return true") + } + + // Unlock + rs.UnlockHash() + if !common.EmptyHash(rs.GetLockedHash()) { + t.Errorf("error mismatch: have %v, want empty", rs.GetLockedHash()) + } + if rs.IsHashLocked() { + t.Error("IsHashLocked should return false") + } +} diff --git a/consensus/istanbul/core/testbackend_test.go b/consensus/istanbul/core/testbackend_test.go new file mode 100644 index 0000000000..0d6ad35740 --- /dev/null +++ b/consensus/istanbul/core/testbackend_test.go @@ -0,0 +1,286 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "crypto/ecdsa" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/consensus/istanbul/validator" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" + elog "github.com/ethereum/go-ethereum/log" +) + +var testLogger = elog.New() + +type testSystemBackend struct { + id uint64 + sys *testSystem + + engine Engine + peers istanbul.ValidatorSet + events *event.TypeMux + + committedMsgs []testCommittedMsgs + sentMsgs [][]byte // store the message when Send is called by core + + address common.Address + db ethdb.Database +} + +type testCommittedMsgs struct { + commitProposal istanbul.Proposal + committedSeals [][]byte +} + +// ============================================== +// +// define the functions that needs to be provided for Istanbul. + +func (self *testSystemBackend) Address() common.Address { + return self.address +} + +// Peers returns all connected peers +func (self *testSystemBackend) Validators(proposal istanbul.Proposal) istanbul.ValidatorSet { + return self.peers +} + +func (self *testSystemBackend) EventMux() *event.TypeMux { + return self.events +} + +func (self *testSystemBackend) Send(message []byte, target common.Address) error { + testLogger.Info("enqueuing a message...", "address", self.Address()) + self.sentMsgs = append(self.sentMsgs, message) + self.sys.queuedMessage <- istanbul.MessageEvent{ + Payload: message, + } + return nil +} + +func (self *testSystemBackend) Broadcast(valSet istanbul.ValidatorSet, message []byte) error { + testLogger.Info("enqueuing a message...", "address", self.Address()) + self.sentMsgs = append(self.sentMsgs, message) + self.sys.queuedMessage <- istanbul.MessageEvent{ + Payload: message, + } + return nil +} + +func (self *testSystemBackend) Gossip(valSet istanbul.ValidatorSet, message []byte) error { + testLogger.Warn("not sign any data") + return nil +} + +func (self *testSystemBackend) Commit(proposal istanbul.Proposal, seals [][]byte) error { + testLogger.Info("commit message", "address", self.Address()) + self.committedMsgs = append(self.committedMsgs, testCommittedMsgs{ + commitProposal: proposal, + committedSeals: seals, + }) + + // fake new head events + go self.events.Post(istanbul.FinalCommittedEvent{}) + return nil +} + +func (self *testSystemBackend) Verify(proposal istanbul.Proposal) (time.Duration, error) { + return 0, nil +} + +func (self *testSystemBackend) Sign(data []byte) ([]byte, error) { + testLogger.Warn("not sign any data") + return data, nil +} + +func (self *testSystemBackend) CheckSignature([]byte, common.Address, []byte) error { + return nil +} + +func (self *testSystemBackend) CheckValidatorSignature(data []byte, sig []byte) (common.Address, error) { + return common.Address{}, nil +} + +func (self *testSystemBackend) Hash(b interface{}) common.Hash { + return common.StringToHash("Test") +} + +func (self *testSystemBackend) NewRequest(request istanbul.Proposal) { + go self.events.Post(istanbul.RequestEvent{ + Proposal: request, + }) +} + +func (self *testSystemBackend) HasBadProposal(hash common.Hash) bool { + return false +} + +func (self *testSystemBackend) LastProposal() (istanbul.Proposal, common.Address) { + l := len(self.committedMsgs) + if l > 0 { + return self.committedMsgs[l-1].commitProposal, common.Address{} + } + return makeBlock(0), common.Address{} +} + +// Only block height 5 will return true +func (self *testSystemBackend) HasPropsal(hash common.Hash, number *big.Int) bool { + return number.Cmp(big.NewInt(5)) == 0 +} + +func (self *testSystemBackend) GetProposer(number uint64) common.Address { + return common.Address{} +} + +func (self *testSystemBackend) ParentValidators(proposal istanbul.Proposal) istanbul.ValidatorSet { + return self.peers +} + +// ============================================== +// +// define the struct that need to be provided for integration tests. + +type testSystem struct { + backends []*testSystemBackend + + queuedMessage chan istanbul.MessageEvent + quit chan struct{} +} + +func newTestSystem(n uint64) *testSystem { + testLogger.SetHandler(elog.StdoutHandler) + return &testSystem{ + backends: make([]*testSystemBackend, n), + + queuedMessage: make(chan istanbul.MessageEvent), + quit: make(chan struct{}), + } +} + +func generateValidators(n int) []common.Address { + vals := make([]common.Address, 0) + for i := 0; i < n; i++ { + privateKey, _ := crypto.GenerateKey() + vals = append(vals, crypto.PubkeyToAddress(privateKey.PublicKey)) + } + return vals +} + +func newTestValidatorSet(n int) istanbul.ValidatorSet { + return validator.NewSet(generateValidators(n), istanbul.RoundRobin) +} + +// FIXME: int64 is needed for N and F +func NewTestSystemWithBackend(n, f uint64) *testSystem { + testLogger.SetHandler(elog.StdoutHandler) + + addrs := generateValidators(int(n)) + sys := newTestSystem(n) + config := istanbul.DefaultConfig + + for i := uint64(0); i < n; i++ { + vset := validator.NewSet(addrs, istanbul.RoundRobin) + backend := sys.NewBackend(i) + backend.peers = vset + backend.address = vset.GetByIndex(i).Address() + + core := New(backend, config).(*core) + core.state = StateAcceptRequest + core.current = newRoundState(&istanbul.View{ + Round: big.NewInt(0), + Sequence: big.NewInt(1), + }, vset, common.Hash{}, nil, nil, func(hash common.Hash) bool { + return false + }) + core.valSet = vset + core.logger = testLogger + core.validateFn = backend.CheckValidatorSignature + + backend.engine = core + } + + return sys +} + +// listen will consume messages from queue and deliver a message to core +func (t *testSystem) listen() { + for { + select { + case <-t.quit: + return + case queuedMessage := <-t.queuedMessage: + testLogger.Info("consuming a queue message...") + for _, backend := range t.backends { + go backend.EventMux().Post(queuedMessage) + } + } + } +} + +// Run will start system components based on given flag, and returns a closer +// function that caller can control lifecycle +// +// Given a true for core if you want to initialize core engine. +func (t *testSystem) Run(core bool) func() { + for _, b := range t.backends { + if core { + b.engine.Start() // start Istanbul core + } + } + + go t.listen() + closer := func() { t.stop(core) } + return closer +} + +func (t *testSystem) stop(core bool) { + close(t.quit) + + for _, b := range t.backends { + if core { + b.engine.Stop() + } + } +} + +func (t *testSystem) NewBackend(id uint64) *testSystemBackend { + // assume always success + ethDB, _ := ethdb.NewMemDatabase() + backend := &testSystemBackend{ + id: id, + sys: t, + events: new(event.TypeMux), + db: ethDB, + } + + t.backends[id] = backend + return backend +} + +// ============================================== +// +// helper functions. + +func getPublicKeyAddress(privateKey *ecdsa.PrivateKey) common.Address { + return crypto.PubkeyToAddress(privateKey.PublicKey) +} diff --git a/consensus/istanbul/core/types_test.go b/consensus/istanbul/core/types_test.go new file mode 100644 index 0000000000..a280fba54b --- /dev/null +++ b/consensus/istanbul/core/types_test.go @@ -0,0 +1,179 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +func testPreprepare(t *testing.T) { + pp := &istanbul.Preprepare{ + View: &istanbul.View{ + Round: big.NewInt(1), + Sequence: big.NewInt(2), + }, + Proposal: makeBlock(1), + } + prepreparePayload, _ := Encode(pp) + + m := &message{ + Code: msgPreprepare, + Msg: prepreparePayload, + Address: common.HexToAddress("0x1234567890"), + } + + msgPayload, err := m.Payload() + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + decodedMsg := new(message) + err = decodedMsg.FromPayload(msgPayload, nil) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + var decodedPP *istanbul.Preprepare + err = decodedMsg.Decode(&decodedPP) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + // if block is encoded/decoded by rlp, we cannot to compare interface data type using reflect.DeepEqual. (like istanbul.Proposal) + // so individual comparison here. + if !reflect.DeepEqual(pp.Proposal.Hash(), decodedPP.Proposal.Hash()) { + t.Errorf("proposal hash mismatch: have %v, want %v", decodedPP.Proposal.Hash(), pp.Proposal.Hash()) + } + + if !reflect.DeepEqual(pp.View, decodedPP.View) { + t.Errorf("view mismatch: have %v, want %v", decodedPP.View, pp.View) + } + + if !reflect.DeepEqual(pp.Proposal.Number(), decodedPP.Proposal.Number()) { + t.Errorf("proposal number mismatch: have %v, want %v", decodedPP.Proposal.Number(), pp.Proposal.Number()) + } +} + +func testSubject(t *testing.T) { + s := &istanbul.Subject{ + View: &istanbul.View{ + Round: big.NewInt(1), + Sequence: big.NewInt(2), + }, + Digest: common.StringToHash("1234567890"), + } + + subjectPayload, _ := Encode(s) + + m := &message{ + Code: msgPreprepare, + Msg: subjectPayload, + Address: common.HexToAddress("0x1234567890"), + } + + msgPayload, err := m.Payload() + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + decodedMsg := new(message) + err = decodedMsg.FromPayload(msgPayload, nil) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + var decodedSub *istanbul.Subject + err = decodedMsg.Decode(&decodedSub) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + if !reflect.DeepEqual(s, decodedSub) { + t.Errorf("subject mismatch: have %v, want %v", decodedSub, s) + } +} + +func testSubjectWithSignature(t *testing.T) { + s := &istanbul.Subject{ + View: &istanbul.View{ + Round: big.NewInt(1), + Sequence: big.NewInt(2), + }, + Digest: common.StringToHash("1234567890"), + } + expectedSig := []byte{0x01} + + subjectPayload, _ := Encode(s) + // 1. Encode test + m := &message{ + Code: msgPreprepare, + Msg: subjectPayload, + Address: common.HexToAddress("0x1234567890"), + Signature: expectedSig, + CommittedSeal: []byte{}, + } + + msgPayload, err := m.Payload() + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + // 2. Decode test + // 2.1 Test normal validate func + decodedMsg := new(message) + err = decodedMsg.FromPayload(msgPayload, func(data []byte, sig []byte) (common.Address, error) { + return common.Address{}, nil + }) + if err != nil { + t.Errorf("error mismatch: have %v, want nil", err) + } + + if !reflect.DeepEqual(decodedMsg, m) { + t.Errorf("error mismatch: have %v, want nil", err) + } + + // 2.2 Test nil validate func + decodedMsg = new(message) + err = decodedMsg.FromPayload(msgPayload, nil) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(decodedMsg, m) { + t.Errorf("message mismatch: have %v, want %v", decodedMsg, m) + } + + // 2.3 Test failed validate func + decodedMsg = new(message) + err = decodedMsg.FromPayload(msgPayload, func(data []byte, sig []byte) (common.Address, error) { + return common.Address{}, istanbul.ErrUnauthorizedAddress + }) + if err != istanbul.ErrUnauthorizedAddress { + t.Errorf("error mismatch: have %v, want %v", err, istanbul.ErrUnauthorizedAddress) + } +} + +func TestMessageEncodeDecode(t *testing.T) { + testPreprepare(t) + testSubject(t) + testSubjectWithSignature(t) +}