From ae3f197413fa021d836b3869bc5f4a8d577c2a94 Mon Sep 17 00:00:00 2001 From: Loong Date: Wed, 13 May 2020 12:48:09 +1000 Subject: [PATCH] simplify interf defs in pref of more complex impl --- block/block.go | 461 ------ block/block_test.go | 486 ------ block/marshal.go | 232 --- block/marshal_test.go | 1 - go.mod | 7 +- go.sum | 8 + hyperdrive.go | 181 --- hyperdrive_test.go | 599 ------- mq/mq.go | 149 ++ .../proc_suite_test.go => mq/mq_suite_test.go | 6 +- mq/mq_test.go | 1 + mq/opt.go | 12 + mq/opt_test.go | 1 + proc/message.go | 202 --- proc/message_test.go | 1 - proc/proc.go | 688 -------- proc/proc_test.go | 1 - proc/state.go | 110 -- proc/state_test.go | 1 - process/catch.go | 28 - process/catch_test.go | 1 - process/marshal.go | 460 ------ process/marshal_test.go | 64 - process/message.go | 790 ++++------ process/message_test.go | 684 +------- process/process.go | 1396 +++++++++-------- process/process_suite_test.go | 2 +- process/process_test.go | 944 ----------- process/state.go | 259 ++- process/state_test.go | 70 - replica/broadcast.go | 76 - replica/broadcast_test.go | 88 -- replica/marshal.go | 94 -- replica/marshal_test.go | 29 - replica/mq.go | 166 -- replica/mq_test.go | 134 -- replica/opt.go | 49 + replica/opt_test.go | 1 + replica/rebase.go | 279 ---- replica/rebase_test.go | 281 ---- replica/replica.go | 428 ++--- replica/replica_suite_test.go | 103 -- replica/replica_test.go | 200 --- replica/time.go | 44 - replica/time_test.go | 43 - schedule/schedule.go | 104 -- schedule/schedule_test.go | 194 --- scheduler/scheduler.go | 51 + .../scheduler_suite_test.go | 4 +- scheduler/scheduler_test.go | 1 + testutil/block.go | 132 -- testutil/id.go | 77 - testutil/process.go | 449 ------ testutil/replica/replica.go | 249 --- testutil/replica/storage.go | 105 -- timer/opt.go | 58 + timer/opt_test.go | 1 + timer/timer.go | 53 + .../timer_suite_test.go | 6 +- timer/timer_test.go | 1 + 60 files changed, 1686 insertions(+), 9659 deletions(-) delete mode 100644 block/block.go delete mode 100644 block/block_test.go delete mode 100644 block/marshal.go delete mode 100644 block/marshal_test.go create mode 100644 mq/mq.go rename proc/proc_suite_test.go => mq/mq_suite_test.go (59%) create mode 100644 mq/mq_test.go create mode 100644 mq/opt.go create mode 100644 mq/opt_test.go delete mode 100644 proc/message.go delete mode 100644 proc/message_test.go delete mode 100644 proc/proc.go delete mode 100644 proc/proc_test.go delete mode 100644 proc/state.go delete mode 100644 proc/state_test.go delete mode 100644 process/catch.go delete mode 100644 process/catch_test.go delete mode 100644 process/marshal.go delete mode 100644 process/marshal_test.go delete mode 100644 replica/broadcast.go delete mode 100644 replica/broadcast_test.go delete mode 100644 replica/marshal.go delete mode 100644 replica/marshal_test.go delete mode 100644 replica/mq.go delete mode 100644 replica/mq_test.go create mode 100644 replica/opt.go create mode 100644 replica/opt_test.go delete mode 100644 replica/rebase.go delete mode 100644 replica/rebase_test.go delete mode 100644 replica/time.go delete mode 100644 replica/time_test.go delete mode 100644 schedule/schedule.go delete mode 100644 schedule/schedule_test.go create mode 100644 scheduler/scheduler.go rename schedule/schedule_suite_test.go => scheduler/scheduler_suite_test.go (72%) create mode 100644 scheduler/scheduler_test.go delete mode 100644 testutil/block.go delete mode 100644 testutil/id.go delete mode 100644 testutil/process.go delete mode 100644 testutil/replica/replica.go delete mode 100644 testutil/replica/storage.go create mode 100644 timer/opt.go create mode 100644 timer/opt_test.go create mode 100644 timer/timer.go rename block/block_suite_test.go => timer/timer_suite_test.go (58%) create mode 100644 timer/timer_test.go diff --git a/block/block.go b/block/block.go deleted file mode 100644 index d593a5c6..00000000 --- a/block/block.go +++ /dev/null @@ -1,461 +0,0 @@ -// Package block defines the `Block` type, and all of the related types. This -// package does not implement any kind of consensus logic; it is concerned with -// defining data types, and serialization/deserialization of those data types. -package block - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "fmt" - "io" - "time" - - "github.com/renproject/id" - "github.com/renproject/surge" -) - -// Kind defines the different kinds of blocks that exist. This is used for -// differentiating between blocks that are for consensus on application-specific -// data, blocks that suggest changing the signatories of a shard, and blocks -// that change the signatories of a shard. -type Kind uint8 - -const ( - // Invalid defines an invalid kind that must not be used. - Invalid Kind = iota - // Standard blocks are used when reaching consensus on the ordering of - // application-specific data. Standard blocks must have nil header - // signatories. This is the most common block kind. - Standard - // Rebase blocks are used when reaching consensus about a change to the - // header signatories that oversee the consensus algorithm. Rebase blocks - // must include non-empty header signatories. - Rebase - // Base blocks are used to finalise rebase blocks. Base blocks must come - // immediately after a rebase block, must have no content, and must have the - // same header signatories as their parent. - Base -) - -// SizeHint of how many bytes will be needed to represent kindedness in binary. -func (kind Kind) SizeHint() int { - return surge.SizeHint(uint8(kind)) -} - -// Marshal to binary. -func (kind Kind) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, uint8(kind), m) -} - -// Unmarshal from binary. -func (kind *Kind) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*uint8)(kind), m) -} - -// String implements the `fmt.Stringer` interface for the `Kind` type. -func (kind Kind) String() string { - switch kind { - case Standard: - return "standard" - case Rebase: - return "rebase" - case Base: - return "base" - default: - panic(fmt.Errorf("invariant violation: unexpected kind=%d", uint8(kind))) - } -} - -// A Header defines properties of a block that are not application-specific. -// These properties are required by, or produced by, the consensus algorithm and -// are not subject modification by the user. -type Header struct { - kind Kind // Kind of block - parentHash id.Hash // Hash of the block parent - baseHash id.Hash // Hash of the block base - txsRef id.Hash // Reference to the block txs - planRef id.Hash // Reference to the block plan - prevStateRef id.Hash // Reference to the block previous state - height Height // Height at which the block was proposed (and committed) - round Round // Round at which the block was proposed - timestamp Timestamp // Seconds since Unix epoch - - // Signatories oversee the consensus algorithm (must be nil unless the block - // is a rebase/base block) - signatories id.Signatories -} - -// NewHeader with all pre-conditions and invariants checked. It will panic if a -// pre-condition or invariant is violated. -func NewHeader(kind Kind, parentHash, baseHash, txsRef, planRef, prevStateRef id.Hash, height Height, round Round, timestamp Timestamp, signatories id.Signatories) Header { - switch kind { - case Standard: - if signatories != nil && len(signatories) != 0 { - panic("pre-condition violation: standard blocks must not declare signatories") - } - case Rebase, Base: - if signatories == nil || len(signatories) == 0 { - panic(fmt.Sprintf("pre-condition violation: %v blocks must declare signatories", kind)) - } - default: - panic(fmt.Errorf("pre-condition violation: unexpected block kind=%v", kind)) - } - if parentHash.Equal(InvalidHash) { - panic(fmt.Errorf("pre-condition violation: invalid parent hash=%v", parentHash)) - } - if baseHash.Equal(InvalidHash) { - panic(fmt.Errorf("pre-condition violation: invalid base hash=%v", baseHash)) - } - if height <= InvalidHeight { - panic(fmt.Errorf("pre-condition violation: invalid height=%v", height)) - } - if round <= InvalidRound { - panic(fmt.Errorf("pre-condition violation: invalid round=%v", round)) - } - if Timestamp(time.Now().Unix()) < timestamp { - panic("pre-condition violation: timestamp has not passed") - } - return Header{ - kind: kind, - parentHash: parentHash, - baseHash: baseHash, - txsRef: txsRef, - planRef: planRef, - prevStateRef: prevStateRef, - height: height, - round: round, - timestamp: timestamp, - signatories: signatories, - } -} - -// Kind of the block. -func (header Header) Kind() Kind { - return header.kind -} - -// ParentHash of the block. -func (header Header) ParentHash() id.Hash { - return header.parentHash -} - -// BaseHash of the block. -func (header Header) BaseHash() id.Hash { - return header.baseHash -} - -// TxsRef of the block. -func (header Header) TxsRef() id.Hash { - return header.txsRef -} - -// PlanRef of the block. -func (header Header) PlanRef() id.Hash { - return header.planRef -} - -// PrevStateRef of the block. -func (header Header) PrevStateRef() id.Hash { - return header.prevStateRef -} - -// Height of the block. -func (header Header) Height() Height { - return header.height -} - -// Round of the block. -func (header Header) Round() Round { - return header.round -} - -// Timestamp of the block in seconds since Unix epoch. -func (header Header) Timestamp() Timestamp { - return header.timestamp -} - -// Signatories of the block. -func (header Header) Signatories() id.Signatories { - return header.signatories -} - -// String implements the `fmt.Stringer` interface for the `Header` type. Two -// headers must not have the same string representation, unless the headers are -// equal. -func (header Header) String() string { - return fmt.Sprintf( - "Header(Kind=%v,ParentHash=%v,BaseHash=%v,TxsRef=%v,PlanRef=%v,PrevStateRef=%v,Height=%v,Round=%v,Timestamp=%v,Signatories=%v)", - header.kind, - header.parentHash, - header.baseHash, - header.txsRef, - header.planRef, - header.prevStateRef, - header.height, - header.round, - header.timestamp, - header.signatories, - ) -} - -// Txs stores application-specific transaction data used in blocks. -type Txs []byte - -// Hash the transactions using SHA256. -func (txs Txs) Hash() id.Hash { - return sha256.Sum256(txs) -} - -// String implements the `fmt.Stringer` interface for the `Txs` type. -func (txs Txs) String() string { - return base64.RawStdEncoding.EncodeToString(txs) -} - -// SizeHint of how many bytes will be needed to represent transactions in -// binary. -func (txs Txs) SizeHint() int { - return surge.SizeHint([]byte(txs)) -} - -// Marshal to binary. -func (txs Txs) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, []byte(txs), m) -} - -// Unmarshal from binary. -func (txs *Txs) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*[]byte)(txs), m) -} - -// Plan stores application-specific data used that is required for the execution -// of transactions in the block. This plan is usually pre-computed data that is -// needed by players in the secure multi-party computation. It is separated from -// transactions for clearer semantic representation (sometimes plans can be -// needed without transactions, and some transactions can be executed without -// plans). -type Plan []byte - -// Hash the plan using SHA256. -func (plan Plan) Hash() id.Hash { - return sha256.Sum256(plan) -} - -// String implements the `fmt.Stringer` interface for the `Plan` type. -func (plan Plan) String() string { - return base64.RawStdEncoding.EncodeToString(plan) -} - -// SizeHint of how many bytes will be needed to represent a plan in binary. -func (plan Plan) SizeHint() int { - return surge.SizeHint([]byte(plan)) -} - -// Marshal to binary. -func (plan Plan) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, []byte(plan), m) -} - -// Unmarshal from binary. -func (plan *Plan) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*[]byte)(plan), m) -} - -// State stores application-specific state after the execution of a block. The -// block at height H+1 will store the state after the execution of block H. This -// is required because of the nature of execution when using an interactive -// secure multi-party computations. -type State []byte - -// Hash the state using SHA256. -func (state State) Hash() id.Hash { - return sha256.Sum256(state) -} - -// String implements the `fmt.Stringer` interface for the `State` type. -func (state State) String() string { - return base64.RawStdEncoding.EncodeToString(state) -} - -// SizeHint of how many bytes will be needed to represent state in binary. -func (state State) SizeHint() int { - return surge.SizeHint([]byte(state)) -} - -// Marshal to binary. -func (state State) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, []byte(state), m) -} - -// Unmarshal from binary. -func (state *State) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*[]byte)(state), m) -} - -// Blocks is a wrapper around the `[]Block` type. -type Blocks []Block - -// A Block is the atomic unit upon which consensus is reached. Consensus -// guarantees a consistent ordering of blocks that is agreed upon by all members -// in a distributed network, even when some of the members are malicious. -type Block struct { - hash id.Hash - - header Header - txs Txs - plan Plan - prevState State -} - -// New Block with the Header, Txs, Plan, and State of the Block parent. The -// Block Hash will automatically be computed and set. -func New(header Header, txs Txs, plan Plan, prevState State) Block { - return Block{ - hash: NewBlockHash(header, txs, plan, prevState), - header: header, - txs: txs, - plan: plan, - prevState: prevState, - } -} - -// Hash returns the 256-bit SHA2 Hash of the Header and Data. -func (block Block) Hash() id.Hash { - return block.hash -} - -// Header of the Block. -func (block Block) Header() Header { - return block.header -} - -// Txs embedded in the Block for application-specific purposes. -func (block Block) Txs() Txs { - return block.txs -} - -// Plan embedded in the Block for application-specific purposes. -func (block Block) Plan() Plan { - return block.plan -} - -// PreviousState embedded in the Block for application-specific state after the -// execution of the Block parent. -func (block Block) PreviousState() State { - return block.prevState -} - -// String implements the `fmt.Stringer` interface for the `Block` type. Two -// blocks must not have the same string representation, unless the blocks are -// equal. -func (block Block) String() string { - return fmt.Sprintf("Block(Hash=%v,Header=%v,Txs=%v,Plan=%v,PreviousState=%v)", block.hash, block.header, block.txs, block.plan, block.prevState) -} - -// Equal compares one block with another. -func (block Block) Equal(other Block) bool { - return block.String() == other.String() -} - -// Timestamp represents seconds since Unix epoch. -type Timestamp uint64 - -// SizeHint of how many bytes will be needed to represent time in binary. -func (t Timestamp) SizeHint() int { - return surge.SizeHint(uint64(t)) -} - -// Marshal to binary. -func (t Timestamp) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, uint64(t), m) -} - -// Unmarshal from binary. -func (t *Timestamp) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*uint64)(t), m) -} - -// Height of a block. -type Height int64 - -// SizeHint of how many bytes will be needed to represent height in binary. -func (h Height) SizeHint() int { - return surge.SizeHint(int64(h)) -} - -// Marshal to binary. -func (h Height) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, int64(h), m) -} - -// Unmarshal from binary. -func (h *Height) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*int64)(h), m) -} - -// Round in which a block was proposed. -type Round int64 - -// SizeHint of how many bytes will be needed to represent time in binary. -func (round Round) SizeHint() int { - return surge.SizeHint(int64(round)) -} - -// Marshal to binary. -func (round Round) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, int64(round), m) -} - -// Unmarshal from binary. -func (round *Round) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*int64)(round), m) -} - -// Define some default invalid values. -var ( - InvalidHash = id.Hash{} - InvalidSignature = id.Signature{} - InvalidSignatory = id.Signatory{} - InvalidHeader = Header{ - kind: Invalid, - parentHash: id.Hash{}, - baseHash: id.Hash{}, - txsRef: id.Hash{}, - planRef: id.Hash{}, - prevStateRef: id.Hash{}, - height: InvalidHeight, - round: InvalidRound, - timestamp: Timestamp(0), - signatories: id.Signatories{}, - } - InvalidBlock = Block{ - hash: id.Hash{}, - header: InvalidHeader, - txs: []byte{}, - plan: []byte{}, - prevState: []byte{}, - } - InvalidRound = Round(-1) - InvalidHeight = Height(-1) -) - -// NewBlockHash of a block based on its header, transactions, execution plan and -// state before execution (i.e. state after execution of its parent). This -// function returns a hash that can be used when creating a block. -func NewBlockHash(header Header, txs Txs, plan Plan, prevState State) id.Hash { - buf := new(bytes.Buffer) - buf.Grow(header.SizeHint() + surge.SizeHint([]byte(txs)) + surge.SizeHint([]byte(plan)) + surge.SizeHint([]byte(prevState))) - if _, err := surge.Marshal(buf, header, surge.MaxBytes); err != nil { - return id.Hash{} // Return the empty hash on an error. - } - if _, err := surge.Marshal(buf, []byte(txs), surge.MaxBytes); err != nil { - return id.Hash{} // Return the empty hash on an error. - } - if _, err := surge.Marshal(buf, []byte(plan), surge.MaxBytes); err != nil { - return id.Hash{} // Return the empty hash on an error. - } - if _, err := surge.Marshal(buf, []byte(prevState), surge.MaxBytes); err != nil { - return id.Hash{} // Return the empty hash on an error. - } - return sha256.Sum256(buf.Bytes()) -} diff --git a/block/block_test.go b/block/block_test.go deleted file mode 100644 index 1cd52c3c..00000000 --- a/block/block_test.go +++ /dev/null @@ -1,486 +0,0 @@ -package block_test - -import ( - "bytes" - "encoding/json" - "fmt" - "math" - "math/rand" - "reflect" - "testing/quick" - "time" - - "github.com/renproject/id" - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/block" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Block", func() { - - Context("Block Kind", func() { - Context("when stringifying", func() { - It("should return correct string for each Kind", func() { - Expect(func() { - _ = Invalid.String() - }).Should(Panic()) - - Expect(fmt.Sprintf("%v", Standard)).Should(Equal("standard")) - Expect(fmt.Sprintf("%v", Rebase)).Should(Equal("rebase")) - Expect(fmt.Sprintf("%v", Base)).Should(Equal("base")) - - randKind := func() bool { - kind := rand.Intn(math.MaxUint8 - 4) - invalidKind := Kind(kind + 4) // skip the valid kinds - Expect(func() { - _ = invalidKind.String() - }).Should(Panic()) - return true - } - - Expect(quick.Check(randKind, nil)).Should(Succeed()) - }) - }) - }) - - Context("Block header", func() { - Context("when stringifying random block headers", func() { - Context("when block headers are equal", func() { - It("should return equal strings", func() { - test := func() bool { - header := RandomBlockHeader(RandomBlockKind()) - newHeader := header - return header.String() == newHeader.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when block headers are unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - header1 := RandomBlockHeader(RandomBlockKind()) - header2 := RandomBlockHeader(RandomBlockKind()) - return header1.String() != header2.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling a random block header", func() { - It("should equal itself after json marshaling and then unmarshaling", func() { - test := func() bool { - header := RandomBlockHeader(RandomBlockKind()) - data, err := json.Marshal(header) - Expect(err).NotTo(HaveOccurred()) - - var newHeader Header - Expect(json.Unmarshal(data, &newHeader)).Should(Succeed()) - Expect(header.String()).Should(Equal(newHeader.String())) - return reflect.DeepEqual(header, newHeader) - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - header := RandomBlockHeader(RandomBlockKind()) - data, err := surge.ToBinary(header) - Expect(err).NotTo(HaveOccurred()) - - var newHeader Header - Expect(surge.FromBinary(data, &newHeader)).Should(Succeed()) - Expect(header.String()).Should(Equal(newHeader.String())) - - return reflect.DeepEqual(header, newHeader) - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when initializing a new block header", func() { - Context("when the block header is well-formed", func() { - It("should return a block header with fields equal to those passed during creation", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - header := headerInit.ToBlockHeader() - - Expect(header.Kind()).Should(Equal(headerInit.Kind)) - Expect(header.ParentHash()).Should(Equal(headerInit.ParentHash)) - Expect(header.BaseHash()).Should(Equal(headerInit.BaseHash)) - Expect(header.TxsRef()).Should(Equal(headerInit.TxsRef)) - Expect(header.PlanRef()).Should(Equal(headerInit.PlanRef)) - Expect(header.PrevStateRef()).Should(Equal(headerInit.PrevStateRef)) - Expect(header.Height()).Should(Equal(headerInit.Height)) - Expect(header.Round()).Should(Equal(headerInit.Round)) - Expect(header.Timestamp()).Should(Equal(headerInit.Timestamp)) - Expect(header.Signatories()).Should(Equal(headerInit.Signatories)) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the block kind is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.Kind = Invalid - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - - // Creat a invalid kind between [4, 255] - headerInit.Kind = Kind(rand.Intn(math.MaxUint8-4) + 4) - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the parent header is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.ParentHash = InvalidHash - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the base header is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.BaseHash = InvalidHash - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the height is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.Height = InvalidHeight - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the timestamp is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.Timestamp = Timestamp(time.Now().Unix() + int64(rand.Intn(1e6))) - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the round is invalid", func() { - It("should panic", func() { - test := func() bool { - kind := RandomBlockKind() - headerInit := RandomBlockHeaderJSON(kind) - headerInit.Round = InvalidRound - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when creating standard block headers", func() { - Context("when signatories are non-nil", func() { - It("should panic", func() { - test := func() bool { - headerInit := RandomBlockHeaderJSON(Standard) - for len(headerInit.Signatories) == 0 { - headerInit.Signatories = RandomSignatories() - } - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when creating rebase block headers", func() { - Context("when signatories are nil or empty", func() { - It("should panic", func() { - test := func() bool { - headerInit := RandomBlockHeaderJSON(Rebase) - headerInit.Signatories = nil - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - - headerInit.Signatories = id.Signatories{} - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when creating base block headers", func() { - Context("when signatories are nil or empty", func() { - It("should panic", func() { - test := func() bool { - headerInit := RandomBlockHeaderJSON(Base) - headerInit.Signatories = nil - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - - headerInit.Signatories = id.Signatories{} - Expect(func() { - _ = headerInit.ToBlockHeader() - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - }) - }) - - Context("Block txs", func() { - Context("when stringifying random block txs", func() { - Context("when block txs is equal", func() { - It("should return equal strings", func() { - test := func(txs Txs) bool { - txsCopy := make(Txs, len(txs)) - copy(txsCopy, txs) - - return txs.String() == txsCopy.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when block txs is unequal", func() { - It("should return unequal strings", func() { - test := func(txs1, txs2 Txs) bool { - if bytes.Equal(txs1, txs2) { - return true - } - return txs1.String() != txs2.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - }) - - Context("Block plan", func() { - Context("when stringifying random block plan", func() { - Context("when block plan is equal", func() { - It("should return equal strings", func() { - test := func(plan Plan) bool { - planCopy := make(Plan, len(plan)) - copy(planCopy, plan) - - return plan.String() == planCopy.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when block plan is unequal", func() { - It("should return unequal strings", func() { - test := func(plan1, plan2 Plan) bool { - if bytes.Equal(plan1, plan2) { - return true - } - return plan1.String() != plan2.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - }) - - Context("Block state", func() { - Context("when stringifying random block state", func() { - Context("when block state is equal", func() { - It("should return equal strings", func() { - test := func(state State) bool { - stateCopy := make(State, len(state)) - copy(stateCopy, state) - - return state.String() == stateCopy.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when block state is unequal", func() { - It("should return unequal strings", func() { - test := func(state1, state2 State) bool { - if bytes.Equal(state1, state2) { - return true - } - return state1.String() != state2.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - }) - - Context("Block", func() { - Context("when stringifying random blocks", func() { - Context("when blocks are equal", func() { - It("should return equal strings", func() { - test := func() bool { - block := RandomBlock(RandomBlockKind()) - newBlock := block - return block.String() == newBlock.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when blocks are unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - block1 := RandomBlock(RandomBlockKind()) - block2 := RandomBlock(RandomBlockKind()) - return block1.String() != block2.String() - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling", func() { - Context("when marshaling a random block", func() { - It("should equal itself after json marshaling and then unmarshaling", func() { - test := func() bool { - block := RandomBlock(RandomBlockKind()) - data, err := json.Marshal(block) - Expect(err).NotTo(HaveOccurred()) - - var newBlock Block - Expect(json.Unmarshal(data, &newBlock)).Should(Succeed()) - Expect(block.String()).Should(Equal(newBlock.String())) - return block.Equal(newBlock) - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - block := RandomBlock(RandomBlockKind()) - data, err := surge.ToBinary(block) - Expect(err).NotTo(HaveOccurred()) - - var newBlock Block - Expect(surge.FromBinary(data, &newBlock)).Should(Succeed()) - Expect(block.String()).Should(Equal(newBlock.String())) - return block.Equal(newBlock) - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when creating blocks", func() { - It("should return a block with fields equal to those passed during creation", func() { - test := func() bool { - header := RandomBlockHeader(RandomBlockKind()) - txs, plan, state := RandomBytesSlice(), RandomBytesSlice(), RandomBytesSlice() - - // Expect the block has a valid hash - block := New(header, txs, plan, state) - Expect(block.Hash()).ShouldNot(Equal(InvalidHash)) - - Expect(block.Header().String()).Should(Equal(header.String())) - Expect(block.Txs()).Should(Equal(Txs(txs))) - Expect(block.Plan()).Should(Equal(Plan(plan))) - Expect(block.PreviousState()).Should(Equal(State(state))) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - Context("when the header, data, and previous state are equal", func() { - It("should return a block with computed hashes that are equal", func() { - test := func() bool { - header := RandomBlockHeader(RandomBlockKind()) - txs, plan, state := RandomBytesSlice(), RandomBytesSlice(), RandomBytesSlice() - - block1 := New(header, txs, plan, state) - block2 := New(header, txs, plan, state) - - Expect(block1.Hash()).Should(Equal(block2.Hash())) - Expect(block1.Equal(block2)).Should(BeTrue()) - Expect(block2.Equal(block1)).Should(BeTrue()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when the header, data, and previous state are unequal", func() { - It("should return a block with computed hashes that are unequal", func() { - test := func() bool { - kind := RandomBlockKind() - block1 := RandomBlock(kind) - block2 := RandomBlock(kind) - - Expect(block1.Hash()).ShouldNot(Equal(block2.Hash())) - Expect(block1.Equal(block2)).Should(BeFalse()) - Expect(block2.Equal(block1)).Should(BeFalse()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - }) -}) diff --git a/block/marshal.go b/block/marshal.go deleted file mode 100644 index 03913014..00000000 --- a/block/marshal.go +++ /dev/null @@ -1,232 +0,0 @@ -package block - -import ( - "encoding/json" - "io" - - "github.com/renproject/id" - "github.com/renproject/surge" -) - -// MarshalJSON is implemented because it is not uncommon that blocks and block -// headers need to be made available through external APIs. -func (header Header) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Kind Kind `json:"kind"` - ParentHash id.Hash `json:"parentHash"` - BaseHash id.Hash `json:"baseHash"` - TxsRef id.Hash `json:"txsRef"` - PlanRef id.Hash `json:"planRef"` - PrevStateRef id.Hash `json:"prevStateRef"` - Height Height `json:"height"` - Round Round `json:"round"` - Timestamp Timestamp `json:"timestamp"` - Signatories id.Signatories `json:"signatories"` - }{ - header.kind, - header.parentHash, - header.baseHash, - header.txsRef, - header.planRef, - header.prevStateRef, - header.height, - header.round, - header.timestamp, - header.signatories, - }) -} - -// UnmarshalJSON is implemented because it is not uncommon that blocks and block -// headers need to be made available through external APIs. -func (header *Header) UnmarshalJSON(data []byte) error { - tmp := struct { - Kind Kind `json:"kind"` - ParentHash id.Hash `json:"parentHash"` - BaseHash id.Hash `json:"baseHash"` - TxsRef id.Hash `json:"txsRef"` - PlanRef id.Hash `json:"planRef"` - PrevStateRef id.Hash `json:"prevStateRef"` - Height Height `json:"height"` - Round Round `json:"round"` - Timestamp Timestamp `json:"timestamp"` - Signatories id.Signatories `json:"signatories"` - }{} - if err := json.Unmarshal(data, &tmp); err != nil { - return err - } - header.kind = tmp.Kind - header.parentHash = tmp.ParentHash - header.baseHash = tmp.BaseHash - header.txsRef = tmp.TxsRef - header.planRef = tmp.PlanRef - header.prevStateRef = tmp.PrevStateRef - header.height = tmp.Height - header.round = tmp.Round - header.timestamp = tmp.Timestamp - header.signatories = tmp.Signatories - return nil -} - -// SizeHint of how many bytes will be needed to represent a header in binary. -func (header Header) SizeHint() int { - return surge.SizeHint(header.kind) + - surge.SizeHint(header.parentHash) + - surge.SizeHint(header.baseHash) + - surge.SizeHint(header.txsRef) + - surge.SizeHint(header.planRef) + - surge.SizeHint(header.prevStateRef) + - surge.SizeHint(header.height) + - surge.SizeHint(header.round) + - surge.SizeHint(header.timestamp) + - surge.SizeHint(header.signatories) -} - -// Marshal this header into binary. -func (header Header) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, header.kind, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.parentHash, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.baseHash, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.txsRef, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.planRef, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.prevStateRef, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.height, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.round, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, header.timestamp, m); err != nil { - return m, err - } - return surge.Marshal(w, header.signatories, m) -} - -// Unmarshal into this header from binary. -func (header *Header) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &header.kind, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.parentHash, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.baseHash, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.txsRef, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.planRef, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.prevStateRef, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.height, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.round, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &header.timestamp, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &header.signatories, m) -} - -// MarshalJSON is implemented because it is not uncommon that blocks and block -// headers need to be made available through external APIs. -func (block Block) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Hash id.Hash `json:"hash"` - Header Header `json:"header"` - Txs Txs `json:"txs"` - Plan Plan `json:"plan"` - PrevState State `json:"prevState"` - }{ - block.hash, - block.header, - block.txs, - block.plan, - block.prevState, - }) -} - -// UnmarshalJSON is implemented because it is not uncommon that blocks and block -// headers need to be made available through external APIs. -func (block *Block) UnmarshalJSON(data []byte) error { - tmp := struct { - Hash id.Hash `json:"hash"` - Header Header `json:"header"` - Txs Txs `json:"txs"` - Plan Plan `json:"plan"` - PrevState State `json:"prevState"` - }{} - if err := json.Unmarshal(data, &tmp); err != nil { - return err - } - block.hash = tmp.Hash - block.header = tmp.Header - block.txs = tmp.Txs - block.plan = tmp.Plan - block.prevState = tmp.PrevState - return nil -} - -// SizeHint of how many bytes will be needed to represent a header in binary. -func (block Block) SizeHint() int { - return surge.SizeHint(block.hash) + - surge.SizeHint(block.header) + - surge.SizeHint(block.txs) + - surge.SizeHint(block.plan) + - surge.SizeHint(block.prevState) -} - -// Marshal this block into binary. -func (block Block) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, block.hash, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, block.header, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, block.txs, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, block.plan, m); err != nil { - return m, err - } - return surge.Marshal(w, block.prevState, m) -} - -// Unmarshal into this block from binary. -func (block *Block) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &block.hash, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &block.header, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &block.txs, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &block.plan, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &block.prevState, m) -} diff --git a/block/marshal_test.go b/block/marshal_test.go deleted file mode 100644 index 082596c9..00000000 --- a/block/marshal_test.go +++ /dev/null @@ -1 +0,0 @@ -package block_test diff --git a/go.mod b/go.mod index 70e38727..bd4fdb3e 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,8 @@ require ( github.com/ethereum/go-ethereum v1.9.5 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 - github.com/renproject/abi v0.4.1 - github.com/renproject/id v0.2.2 - github.com/renproject/phi v0.1.0 - github.com/renproject/surge v1.1.2 + github.com/renproject/abi v0.4.1 // indirect + github.com/renproject/id v0.3.3 + github.com/renproject/surge v1.1.3 github.com/sirupsen/logrus v1.4.2 ) diff --git a/go.sum b/go.sum index f9240eb0..04f95e4b 100644 --- a/go.sum +++ b/go.sum @@ -121,12 +121,18 @@ github.com/renproject/id v0.2.1 h1:bNQStliAf/QUS8LH4bSN+GFoSDuguFCD5nzE9jlAqqM= github.com/renproject/id v0.2.1/go.mod h1:a3IoJpN44tsb2PD4QEFiu6wZy+UuanOkMtGcnxdLikk= github.com/renproject/id v0.2.2 h1:hQH74EK5GjzG588EVJ7m5nnXfJd36mmq/ipkYcL8vAc= github.com/renproject/id v0.2.2/go.mod h1:xEoepH7Jze4l+gJxzSh9yRt634XyiM1j9si+WS2Emsc= +github.com/renproject/id v0.3.1 h1:92CbN8sQTlMIXid69fOJXEkES67uJDORkobRvqoVvJs= +github.com/renproject/id v0.3.1/go.mod h1:xEoepH7Jze4l+gJxzSh9yRt634XyiM1j9si+WS2Emsc= +github.com/renproject/id v0.3.3 h1:IiJR1mJ8PvAds+zRz1gxukbWKJJrYQSUnNdihOsaGAY= +github.com/renproject/id v0.3.3/go.mod h1:BmNHJVfkLsDcvQFHAAPxhhv2KUvWhT4xXFo1Phmp8Kw= github.com/renproject/phi v0.1.0 h1:ZOn7QeDribk/uV46OhQWcTLxyuLg7P+xR1Hfl5cOQuI= github.com/renproject/phi v0.1.0/go.mod h1:Hrxx2ONVpfByficRjyRd1trecalYr0lo7Z0akx8UXqg= github.com/renproject/surge v1.1.1 h1:dpgMRBR1raPWw8TUmICjAVsyl88EmPnUq+o4CZqAm9g= github.com/renproject/surge v1.1.1/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= github.com/renproject/surge v1.1.2 h1:Yy3pTlRyaMJGLfn64JHgCnWs3cWbRJjE+aFxZXRGfWU= github.com/renproject/surge v1.1.2/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= +github.com/renproject/surge v1.1.3 h1:nCN3yWUbIbSDWyMaU6aCIidCE15yEcZb8Bcuziog/wU= +github.com/renproject/surge v1.1.3/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= @@ -156,6 +162,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/hyperdrive.go b/hyperdrive.go index ca5f64f5..1ae218c7 100644 --- a/hyperdrive.go +++ b/hyperdrive.go @@ -1,182 +1 @@ -// Package hyperdrive a high-level package for running multiple instances of the -// Hyperdrive consensus algorithm for over multiple shards. The Hyperdrive -// interface is the main entry point for users. package hyperdrive - -import ( - "crypto/ecdsa" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/replica" - "github.com/renproject/hyperdrive/schedule" - "github.com/renproject/id" - "github.com/renproject/phi" -) - -type ( - // Hashes is a wrapper around the `[]Hash` type. - Hashes = id.Hashes - // A Hash is the `[32]byte` output of a hashing function. Hyperdrive uses - // SHA256 for hashing. - Hash = id.Hash - // Signatures is a wrapper around the `[]Signature` type. - Signatures = id.Signatures - // A Signature is the `[65]byte` output of an ECDSA signing algorithm. - // Hyperdrive uses the secp256k1 curve for ECDSA signing. - Signature = id.Signature - // Signatories is a wrapper around the `[]Signatory` type. - Signatories = id.Signatories - // A Signatory is the `[32]byte` resulting from hashing an ECDSA public key. - // It represents the public identity of a content author and can be used to - // authenticate content that has been signed. - Signatory = id.Signatory -) - -type ( - // Blocks is a wrapper type around the `[]Block` type. - Blocks = block.Blocks - // A Block is an atomic unit of data upon which consensus is reached. - // Everything upon which consensus is needed should be put into a block, and - // consensus can only be reached on a block by block basis (there is no - // finer-grained way to express consensus). - Block = block.Block - // The Height in a blockchain at which a block was proposed/committed. - Height = block.Height - // The Round in a consensus algorithm at which a block was - // proposed/committed. - Round = block.Round - // Timestamp is a wrapper around the `uint64` type. - Timestamp = block.Timestamp - // BlockTxs represent the application-specific transactions that are being - // proposed as part of a block. An application that wishes to achieve - // consensus on activity within the application should represent this - // activity as transactions, serialise them into bytes, and put them into a - // block. No assumptions are made about the format of these transactions. - BlockTxs = block.Txs - // A BlockPlan represents application-specific data that is needed to - // execute the transactions in a block. No assumptions are made about the - // format of this plan. - BlockPlan = block.Plan - // The BlockState represents application-specific state. - BlockState = block.State -) - -type ( - Blockchain = process.Blockchain - Process = process.Process - ProcessState = process.State -) - -type ( - Messages = replica.Messages - Message = replica.Message - Shards = replica.Shards - Shard = replica.Shard - Options = replica.Options - Replicas = replica.Replicas - Replica = replica.Replica - ProcessStorage = replica.ProcessStorage - BlockStorage = replica.BlockStorage - BlockIterator = replica.BlockIterator - Validator = replica.Validator - Observer = replica.Observer - Broadcaster = replica.Broadcaster -) - -var ( - // NewSignatory returns a Signatory from an ECDSA public key by serializing - // the ECDSA public key into bytes and hashing it with SHA256. - NewSignatory = id.NewSignatory -) - -var ( - StandardBlockKind = block.Standard - RebaseBlockKind = block.Rebase - BaseBlockKind = block.Base - NewBlock = block.New - NewBlockHeader = block.NewHeader -) - -// Hyperdrive manages multiple `Replicas` from different -// `Shards`. -type Hyperdrive interface { - Start() - Rebase(sigs Signatories) - HandleMessage(message Message) -} - -type hyperdrive struct { - replicas map[Shard]Replica -} - -// New returns a new `Hyperdrive` instance that wraps multiple replica -// instances. One replica instance will be created per Shard, but all replica -// instances will use the same interfaces and private key. Replicas will not be -// created for shards for which the replica is not a signatory. This means that -// rebasing can shuffle Signatories, but it cannot introduce new ones or remove -// existing ones (this will be supported in future updates). -// -// hyper := hyperdrive.New( -// hyperdrive.Options{}, -// pStorage, -// bStorage, -// bIter, -// validator, -// observer, -// broadcaster, -// shards, -// privKey, -// ) -// hyper.Start() -// for { -// select { -// case <-ctx.Done(): -// break -// case message, ok := <-messagesFromNetwork: -// if !ok { -// break -// } -// hyper.HandleMessage(message) -// } -// } -func New(options Options, pStorage ProcessStorage, blockStorage BlockStorage, blockIterator BlockIterator, validator Validator, observer Observer, broadcaster Broadcaster, catcher process.Catcher, shards Shards, privKey ecdsa.PrivateKey) Hyperdrive { - replicas := make(map[Shard]Replica, len(shards)) - for _, shard := range shards { - if observer.IsSignatory(shard) { - rr := schedule.RoundRobin(blockStorage.LatestBaseBlock(shard).Header().Signatories()) - replicas[shard] = replica.New(options, pStorage, blockStorage, blockIterator, validator, observer, broadcaster, rr, catcher, shard, privKey) - } - } - return &hyperdrive{ - replicas: replicas, - } -} - -// Start all replicas in the `Hyperdrive` instance. All replicas will be started -// in parallel. This must be done before shards can be rebased, and before -// messages can be handled. -func (hyper *hyperdrive) Start() { - phi.ParForAll(hyper.replicas, func(shard Shard) { - replica := hyper.replicas[shard] - replica.Start() - }) -} - -func (hyper *hyperdrive) Rebase(sigs Signatories) { - for shard, replica := range hyper.replicas { - replica.Rebase(sigs) - hyper.replicas[shard] = replica - } -} - -func (hyper *hyperdrive) HandleMessage(message Message) { - replica, ok := hyper.replicas[message.Shard] - if !ok { - return - } - defer func() { - hyper.replicas[message.Shard] = replica - }() - replica.HandleMessage(message) -} diff --git a/hyperdrive_test.go b/hyperdrive_test.go index 5e8c4639..be7e95fe 100644 --- a/hyperdrive_test.go +++ b/hyperdrive_test.go @@ -1,600 +1 @@ package hyperdrive_test - -import ( - "context" - "crypto/ecdsa" - "crypto/rand" - "fmt" - "log" - mrand "math/rand" - "sync" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/replica" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/id" - "github.com/renproject/phi" - "github.com/renproject/surge" - "github.com/sirupsen/logrus" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive" - . "github.com/renproject/hyperdrive/testutil/replica" -) - -func init() { - seed := time.Now().Unix() - log.Printf("seed = %v", seed) - mrand.Seed(seed) -} - -var _ = Describe("Hyperdrive", func() { - - table := []struct { - shard int - f int - r int - }{ - {1, 2, 0}, - {1, 2, 2}, - } - - for _, entry := range table { - shards := make([]Shard, entry.shard) - for i := range shards { - shards[i] = RandomShard() - } - f := entry.f - r := entry.r - - Context(fmt.Sprintf("when the network have %v signatory nodes (f = %v), %v non-signatory nodes and %v shards", 3*f+1, f, r, len(shards)), func() { - Context("when all nodes have 100% live time", func() { - Context("when all nodes start at same time", func() { - It("should keep producing new blocks", func() { - options := DefaultOption - options.maxBootDelay = 0 - network := NewNetwork(f, r, shards, options) - - network.Start() - defer network.Stop() - - Eventually(func() bool { - return network.HealthCheck(nil) - }, time.Minute).Should(BeTrue()) - }) - }) - - Context("when each node has a random delay when starting", func() { - It("should keep producing new blocks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - // Expect the network should be handle nodes starting with different delays and - Eventually(func() bool { - return network.HealthCheck(nil) - }, time.Minute).Should(BeTrue()) - }) - }) - }) - - Context("when f nodes are offline at the beginning", func() { - It("should keep producing new blocks", func() { - option := DefaultOption - shuffledIndices := mrand.Perm(3*f + 1) - option.disabledNodes = shuffledIndices[:f] - - network := NewNetwork(f, r, shards, option) - network.Start() - defer network.Stop() - - // Only check the nodes which are online are progressing after certain amount time - Eventually(func() bool { - return network.HealthCheck(shuffledIndices[f:]) - }, time.Duration(f*30)*time.Second).Should(BeTrue()) - }) - }) - - Context("when some nodes are having network connection issue", func() { - Context("when they go back online after certain amount of time", func() { - It("should keep producing new blocks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - numNodesOffline := mrand.Intn(f) + 1 // Making sure at least one node is offline - shuffledIndices := mrand.Perm(3*f + 1) - - // Wait for all nodes reach consensus - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // Simulate connection issue for less than 1/3 nodes - phi.ParForAll(numNodesOffline, func(i int) { - index := shuffledIndices[i] - network.BlockNodeConnection(index) - SleepRandomSeconds(5, 10) - network.UnblockNodeConnection(index) - }) - - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - }) - }) - - Context("when they fail to reconnect to the network", func() { - It("should keep producing blocks with the rest of the networks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - numNodesOffline := mrand.Intn(f) + 1 // Making sure at least one node is offline - shuffledIndices := mrand.Perm(3*f + 1) - - // Wait for all nodes reach consensus - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // Simulate connection issue for less than 1/3 nodes - phi.ParForAll(numNodesOffline, func(i int) { - index := shuffledIndices[i] - network.BlockNodeConnection(index) - }) - - Eventually(func() bool { - return network.HealthCheck(shuffledIndices[numNodesOffline:]) - }, 30*time.Second).Should(BeTrue()) - }) - }) - }) - - Context("when nodes are completely offline", func() { - Context("when no more than f nodes crashed", func() { - Context("when they go back online after some time", func() { - It("should keep producing new blocks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - numNodesOffline := mrand.Intn(f) + 1 // Making sure at least one node is offline - shuffledIndices := mrand.Perm(3*f + 1) - - // Wait for all nodes reach consensus - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // Simulate connection issue for less than 1/3 nodes - phi.ParForAll(numNodesOffline, func(i int) { - index := shuffledIndices[i] - network.StopNode(index) - SleepRandomSeconds(5, 10) - network.StartNode(index) - }) - - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - }) - }) - - Context("when they fail to reconnect to the network", func() { - It("should keep producing new blocks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - numNodesOffline := mrand.Intn(f) + 1 // Making sure at least one node is offline - shuffledIndices := mrand.Perm(3*f + 1) - - // Wait for all nodes reach consensus - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // Simulate connection issue for less than 1/3 nodes - phi.ParForAll(numNodesOffline, func(i int) { - index := shuffledIndices[i] - network.StopNode(index) - SleepRandomSeconds(5, 10) - network.StartNode(index) - }) - - Eventually(func() bool { - return network.HealthCheck(shuffledIndices[numNodesOffline:]) - }, 30*time.Second).Should(BeTrue()) - }) - }) - }) - - Context("when more than f nodes crash,", func() { - Context("when they fail to reconnect to the network", func() { - It("should stop producing new blocks", func() { - network := NewNetwork(f, r, shards, DefaultOption) - network.Start() - defer network.Stop() - - shuffledIndices := mrand.Perm(3*f + 1) - crashedNodes := shuffledIndices[:f+1] - - // Wait for all nodes reach consensus - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // simulate the nodes crashed. - phi.ParForAll(crashedNodes, func(i int) { - index := crashedNodes[i] - network.StopNode(index) - }) - - // expect the network not progressing - time.Sleep(30 * time.Second) - Expect(network.HealthCheck(shuffledIndices[f:])).Should(BeFalse()) - }) - }) - - Context("when they successfully reconnect to the network", func() { - It("should start producing blocks again", func() { - options := DefaultOption - options.debugLogger = []int{0} - options.timeoutProposers = []int{mrand.Intn(3*f + 1)} // Pick a random node to timeout when proposing blocks. - network := NewNetwork(f, r, shards, options) - network.Start() - defer network.Stop() - - // Wait for all nodes to reach consensus. - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - - // Crash f+1 random nodes and expect no blocks to be - // produced. - shuffledIndices := mrand.Perm(3*f + 1) - crashedNodes := shuffledIndices[:f+1] - phi.ParForAll(crashedNodes, func(i int) { - SleepRandomSeconds(0, 2) - index := crashedNodes[i] - network.StopNode(index) - }) - Expect(network.HealthCheck(nil)).Should(BeFalse()) - - // Restart the nodes at different times. - phi.ParForAll(crashedNodes, func(i int) { - SleepRandomSeconds(5, 10) - index := crashedNodes[i] - network.StartNode(index) - }) - - Eventually(func() bool { - return network.HealthCheck(nil) - }, 120*time.Second).Should(BeTrue()) - }) - }) - }) - }) - - Context("when more than f nodes fail to boot", func() { - Context("when the failed node never come back", func() { - It("should not process any blocks", func() { - // Start the network with more than f nodes offline - options := DefaultOption - shuffledIndices := mrand.Perm(3*f + 1) - options.disabledNodes = shuffledIndices[:f+1] - - network := NewNetwork(f, r, shards, options) - network.Start() - defer network.Stop() - - // expect all nodes only have the genesis block - time.Sleep(20 * time.Second) - Expect(network.HealthCheck(shuffledIndices[f:])).Should(BeFalse()) - }) - }) - - Context("when the failed node come back online", func() { - It("should start produce blocks", func() { - // Start the network with more than f nodes offline - options := DefaultOption - shuffledIndices := mrand.Perm(3*f + 1) - options.disabledNodes = shuffledIndices[:f+1] - - network := NewNetwork(f, r, shards, options) - network.Start() - defer network.Stop() - - phi.ParForAll(shuffledIndices[:f+1], func(i int) { - index := shuffledIndices[i] - network.StartNode(index) - }) - - Eventually(func() bool { - return network.HealthCheck(nil) - }, 30*time.Second).Should(BeTrue()) - }) - }) - }) - }) - } -}) - -type networkOptions struct { - minNetworkDelay int // minimum network latency when sending messages in milliseconds - maxNetworkDelay int // maximum network latency when sending messages in milliseconds - minBootDelay int // minimum delay when booting the node in seconds - maxBootDelay int // maximum delay when booting the node in seconds - debugLogger []int // indices of nodes that use a debug logger, nil to disable all - disabledNodes []int // indices of nodes that are disabled when the network starts, nil to enable all - timeoutProposers []int // indices of nodes that timeout prior to proposing blocks, nil for no invalid proposers -} - -var DefaultOption = networkOptions{ - minNetworkDelay: 100, - maxNetworkDelay: 500, - minBootDelay: 0, - maxBootDelay: 3, - debugLogger: nil, - disabledNodes: nil, - timeoutProposers: nil, -} - -type Network struct { - f int - r int - shards replica.Shards - options networkOptions - - nodesMu *sync.RWMutex - nodes []*Node - - context context.Context - cancel context.CancelFunc - nodesCancels []context.CancelFunc - keys []*ecdsa.PrivateKey - Broadcaster *MockBroadcaster -} - -func NewNetwork(f, r int, shards replica.Shards, options networkOptions) Network { - if f <= 0 { - panic("f must be positive") - } - total := (3*f + 1) + r - - // Generate keys for all the nodes - keys := make([]*ecdsa.PrivateKey, total) - sigs := make([]id.Signatory, total) - for i := range keys { - pk, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) - if err != nil { - panic(err) - } - sig := id.NewSignatory(pk.PublicKey) - keys[i], sigs[i] = pk, sig - } - - // Initialize all nodes - genesisBlock := testutil.GenesisBlock(sigs[:3*f+1]) - broadcaster := NewMockBroadcaster(keys, options.minNetworkDelay, options.maxNetworkDelay) - nodes := make([]*Node, total) - for i := range nodes { - logger := logrus.New() - if Contain(options.debugLogger, i) { - logger.Infof("✏️ node %d has debug logs enabled", i) - logger.SetLevel(logrus.DebugLevel) - } - store := NewMockPersistentStorage(shards) - store.Init(genesisBlock) - - var timeoutProposer bool - if Contain(options.timeoutProposers, i) { - logger.Infof("✏️ node %d will time out when proposing", i) - timeoutProposer = true - } - iter := NewMockBlockIterator(store, timeoutProposer) - nodes[i] = NewNode(logger.WithField("node", i), shards, keys[i], iter, broadcaster, store, i < 3*f+1) - } - ctx, cancel := context.WithCancel(context.Background()) - - return Network{ - f: f, - r: r, - shards: shards, - options: options, - nodesMu: new(sync.RWMutex), - nodes: nodes, - context: ctx, - cancel: cancel, - keys: keys, - nodesCancels: make([]context.CancelFunc, len(nodes)), - Broadcaster: broadcaster, - } -} - -func (network Network) Start() { - phi.ParForAll(network.nodes, func(i int) { - if Contain(network.options.disabledNodes, i) { - return - } - SleepRandomSeconds(network.options.minBootDelay, network.options.maxBootDelay) - - network.nodesMu.RLock() - defer network.nodesMu.RUnlock() - network.startNode(i) - }) -} - -func (network Network) Stop() { - for _, cancel := range network.nodesCancels { - if cancel != nil { - cancel() - } - } - network.cancel() - time.Sleep(time.Second) -} - -func (network Network) Signatories() id.Signatories { - sigs := make(id.Signatories, len(network.nodes)) - for i := range sigs { - sigs[i] = id.NewSignatory(network.keys[i].PublicKey) - } - return sigs -} - -func (network *Network) StartNode(i int) { - logger := logrus.New() - if Contain(network.options.debugLogger, i) { - logger.SetLevel(logrus.DebugLevel) - } - store := network.nodes[i].storage - var timeoutProposer bool - if Contain(network.options.timeoutProposers, i) { - timeoutProposer = true - } - iter := NewMockBlockIterator(store, timeoutProposer) - - network.nodesMu.Lock() - defer network.nodesMu.Unlock() - - network.nodes[i] = NewNode(logger.WithField("node", i), network.shards, network.nodes[i].key, iter, network.Broadcaster, store, i >= network.r) - network.startNode(i) -} - -func (network *Network) startNode(i int) { - // Creating cancel for this node and enable its network connection - node := network.nodes[i] - innerCtx, cancel := context.WithCancel(network.context) - network.nodesCancels[i] = cancel - network.Broadcaster.EnablePeer(node.Signatory()) - - // Start the node - node.logger.Infof("💡starting hyperdrive...") - node.hyperdrive.Start() - - // Start reading messages from the broadcaster - messages := network.Broadcaster.Messages(node.Signatory()) - hyperdrive, logger := node.hyperdrive, node.logger - - go func() { - defer logger.Info("❌ shutting down hyperdrive...") - - for { - select { - case messageBytes := <-messages: - var message replica.Message - if err := surge.FromBinary(messageBytes, &message); err != nil { - panic(err) - } - hyperdrive.HandleMessage(message) - select { - case <-innerCtx.Done(): - return - default: - } - case <-innerCtx.Done(): - return - } - } - }() -} - -func (network *Network) StopNode(i int) { - if network.nodesCancels[i] == nil { - return - } - network.nodesCancels[i]() - network.Broadcaster.DisablePeer(network.Signatories()[i]) -} - -func (network *Network) UnblockNodeConnection(i int) { - network.nodes[i].logger.Infof("💡 enable network connection... ") - network.Broadcaster.EnablePeer(network.Signatories()[i]) -} - -func (network *Network) BlockNodeConnection(i int) { - network.nodes[i].logger.Infof("⚠️ block network connection...") - network.Broadcaster.DisablePeer(network.Signatories()[i]) -} - -// Check the nodes of given indexes are working together producing new blocks. -func (network *Network) HealthCheck(indexes []int) bool { - network.nodesMu.RLock() - defer network.nodesMu.RUnlock() - - nodes := network.nodes - if indexes != nil { - nodes = make([]*Node, 0, len(indexes)) - for _, index := range indexes { - nodes = append(nodes, network.nodes[index]) - } - } - - // Check the block height of each nodes - currentBlockHeights := make([]block.Height, len(nodes)) - for _, shard := range network.shards { - for i, node := range nodes { - if node.observer.IsSignatory(shard) { - block := node.storage.LatestBlock(shard) - currentBlockHeights[i] = block.Header().Height() - } - } - } - - time.Sleep(5 * time.Second) - - for _, shard := range network.shards { - for i, node := range nodes { - if node.observer.IsSignatory(shard) { - block := node.storage.LatestBlock(shard) - if block.Header().Height() <= currentBlockHeights[i] { - node.logger.Infof("⚠️ node %d did not progress, old height = %d, new height = %d", i, currentBlockHeights[i], block.Header().Height()) - return false - } - } - } - } - return true -} - -type Node struct { - logger logrus.FieldLogger - key *ecdsa.PrivateKey - storage *MockPersistentStorage - iter replica.BlockIterator - validator replica.Validator - observer replica.Observer - hyperdrive Hyperdrive -} - -func (node Node) Signatory() id.Signatory { - return id.NewSignatory(node.key.PublicKey) -} - -func NewNode(logger logrus.FieldLogger, shards Shards, pk *ecdsa.PrivateKey, iter *MockBlockIterator, broadcaster *MockBroadcaster, store *MockPersistentStorage, isSignatory bool) *Node { - option := Options{ - Logger: logger, - BackOffExp: 1, - BackOffBase: 3 * time.Second, - BackOffMax: 3 * time.Second, - } - validator := NewMockValidator(store) - observer := NewMockObserver(store, isSignatory) - hd := New(option, store, store, iter, validator, observer, broadcaster, process.CatchAndIgnore(), shards, *pk) - - return &Node{ - logger: logger, - key: pk, - storage: store, - iter: iter, - validator: validator, - observer: observer, - hyperdrive: hd, - } -} diff --git a/mq/mq.go b/mq/mq.go new file mode 100644 index 00000000..371549d5 --- /dev/null +++ b/mq/mq.go @@ -0,0 +1,149 @@ +package mq + +import ( + "fmt" + "sort" + + "github.com/renproject/hyperdrive/process" +) + +// A MessageQueue is used to sort incoming messages by their height and round, +// where messages with lower heights/rounds are found at the beginning of the +// queue. Every sender, identified by their pid, has their own dedicated queue +// with its own dedicated maximum capacity. This limits how far in the future +// the MessageQueue will buffer messages, to prevent running out of memory. +// However, this also means that explicit resynchronisation is needed, because +// not all messages that are received are guaranteed to be kept. MessageQueues +// do not handle de-duplication, and are not safe for concurrent use. +type MessageQueue struct { + opts Options + queuesByPid map[process.Pid][]interface{} +} + +// New returns an empty MessageQueue. +func New(opts Options) MessageQueue { + return MessageQueue{ + queuesByPid: make(map[process.Pid][]interface{}), + } +} + +// Consume Propose, Prevote, and Precommit messages from the MessageQueue that +// have heights up to (and including) the given height. The appropriate callback +// will be called for every message that is consumed. All consumed messages will +// be dropped from the MessageQueue. +func (mq *MessageQueue) Consume(h process.Height, propose func(process.Propose), prevote func(process.Prevote), precommit func(process.Precommit)) (n int) { + for from, q := range mq.queuesByPid { + for len(q) > 0 { + if height(q[0]) > h { + break + } + switch msg := q[0].(type) { + case process.Propose: + propose(msg) + case process.Prevote: + prevote(msg) + case process.Precommit: + precommit(msg) + } + n++ + q = q[1:] + } + mq.queuesByPid[from] = q + } + return +} + +// InsertPropose message into the MessageQueue. This method assumes that the +// sender has already been authenticated and filtered. +func (mq *MessageQueue) InsertPropose(propose process.Propose) { + mq.insert(propose) +} + +// InsertPrevote message into the MessageQueue. This method assumes that the +// sender has already been authenticated and filtered. +func (mq *MessageQueue) InsertPrevote(prevote process.Prevote) { + mq.insert(prevote) +} + +// InsertPrecommit message into the MessageQueue. This method assumes that the +// sender has already been authenticated and filtered. +func (mq *MessageQueue) InsertPrecommit(precommit process.Precommit) { + mq.insert(precommit) +} + +func (mq *MessageQueue) insert(msg interface{}) { + // Initialise the queue for the sender of the message, to avoid nil-pointer + // errors. This makes the assumption that messages that have not already + // passed authentication checks will not be placed into the MessageQueue. + msgFrom := from(msg) + if _, ok := mq.queuesByPid[msgFrom]; !ok { + mq.queuesByPid[msgFrom] = make([]interface{}, mq.opts.MaxCapacity) + } + + // Load the queue from the map, and defer saving it back to the map. + q := mq.queuesByPid[msgFrom] + defer func() { mq.queuesByPid[msgFrom] = q }() + + // Find the index at which the message should be inserted to maintain + // height/round ordering. + msgHeight := height(msg) + msgRound := round(msg) + insertAt := sort.Search(len(q), func(i int) bool { + height := height(q[i]) + round := round(q[i]) + return height > msgHeight || (height == msgHeight && round > msgRound) + }) + + // Insert into the slice using the trick described at + // https://github.com/golang/go/wiki/SliceTricks (which minimises + // allocations and copying). + q = append(q, nil) + copy(q[insertAt+1:], q[insertAt:]) + q[insertAt] = msg + + // If the queue for this sender has exceeded its maximum capacity, then we + // drop excess elements. This protects against adversaries that might seek + // to cause an OOM by sending messages "from the far future". + if len(q) > mq.opts.MaxCapacity { + q = q[:mq.opts.MaxCapacity] + } +} + +func height(msg interface{}) process.Height { + switch msg := msg.(type) { + case process.Propose: + return msg.Height + case process.Prevote: + return msg.Height + case process.Precommit: + return msg.Height + default: + panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) + } +} + +func round(msg interface{}) process.Round { + switch msg := msg.(type) { + case process.Propose: + return msg.Round + case process.Prevote: + return msg.Round + case process.Precommit: + return msg.Round + default: + panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) + } +} + +func from(msg interface{}) process.Pid { + switch msg := msg.(type) { + case process.Propose: + return msg.From + case process.Prevote: + return msg.From + case process.Precommit: + return msg.From + default: + panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) + } +} diff --git a/proc/proc_suite_test.go b/mq/mq_suite_test.go similarity index 59% rename from proc/proc_suite_test.go rename to mq/mq_suite_test.go index c19a0ea3..2dab6bdf 100644 --- a/proc/proc_suite_test.go +++ b/mq/mq_suite_test.go @@ -1,4 +1,4 @@ -package proc_test +package mq_test import ( "testing" @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestProc(t *testing.T) { +func TestMq(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Proc Suite") + RunSpecs(t, "Mq Suite") } diff --git a/mq/mq_test.go b/mq/mq_test.go new file mode 100644 index 00000000..5921ebb1 --- /dev/null +++ b/mq/mq_test.go @@ -0,0 +1 @@ +package mq_test \ No newline at end of file diff --git a/mq/opt.go b/mq/opt.go new file mode 100644 index 00000000..bacf0dce --- /dev/null +++ b/mq/opt.go @@ -0,0 +1,12 @@ +package mq + +import "github.com/sirupsen/logrus" + +type Options struct { + Logger logrus.FieldLogger + MaxCapacity int +} + +func DefaultOptions() Options { + return Options{} +} diff --git a/mq/opt_test.go b/mq/opt_test.go new file mode 100644 index 00000000..f1c18f05 --- /dev/null +++ b/mq/opt_test.go @@ -0,0 +1 @@ +package mq_test diff --git a/proc/message.go b/proc/message.go deleted file mode 100644 index c3de484b..00000000 --- a/proc/message.go +++ /dev/null @@ -1,202 +0,0 @@ -package proc - -import ( - "fmt" - "io" - - "github.com/renproject/surge" -) - -type Propose struct { - Height Height `json:"height"` - Round Round `json:"round"` - ValidRound Round `json:"validRound"` - Value Value `json:"value"` - From Pid `json:"from"` -} - -func (propose *Propose) Equal(other *Propose) bool { - return propose.Height == other.Height && - propose.Round == other.Round && - propose.ValidRound == other.ValidRound && - propose.Value.Equal(&other.Value) && - propose.From.Equal(&other.From) -} - -func (propose *Propose) SizeHint() int { - return surge.SizeHint(propose.Height) + - surge.SizeHint(propose.Round) + - surge.SizeHint(propose.ValidRound) + - surge.SizeHint(propose.Value) + - surge.SizeHint(propose.From) -} - -func (propose *Propose) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, propose.Height, m) - if err != nil { - return m, fmt.Errorf("marshaling height=%v: %v", propose.Height, err) - } - m, err = surge.Marshal(w, propose.Round, m) - if err != nil { - return m, fmt.Errorf("marshaling round=%v: %v", propose.Round, err) - } - m, err = surge.Marshal(w, propose.ValidRound, m) - if err != nil { - return m, fmt.Errorf("marshaling valid round=%v: %v", propose.ValidRound, err) - } - m, err = surge.Marshal(w, propose.Value, m) - if err != nil { - return m, fmt.Errorf("marshaling value=%v: %v", propose.Value, err) - } - m, err = surge.Marshal(w, propose.From, m) - if err != nil { - return m, fmt.Errorf("marshaling from=%v: %v", propose.From, err) - } - return m, nil -} - -func (propose *Propose) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &propose.Height, m) - if err != nil { - return m, fmt.Errorf("unmarshaling height: %v", err) - } - m, err = surge.Unmarshal(r, &propose.Round, m) - if err != nil { - return m, fmt.Errorf("unmarshaling round: %v", err) - } - m, err = surge.Unmarshal(r, &propose.ValidRound, m) - if err != nil { - return m, fmt.Errorf("unmarshaling valid round: %v", err) - } - m, err = surge.Unmarshal(r, &propose.Value, m) - if err != nil { - return m, fmt.Errorf("unmarshaling value: %v", err) - } - m, err = surge.Unmarshal(r, &propose.From, m) - if err != nil { - return m, fmt.Errorf("unmarshaling from: %v", err) - } - return m, nil -} - -type Prevote struct { - Height Height `json:"height"` - Round Round `json:"round"` - Value Value `json:"value"` - From Pid `json:"from"` -} - -func (prevote *Prevote) Equal(other *Prevote) bool { - return prevote.Height == other.Height && - prevote.Round == other.Round && - prevote.Value.Equal(&other.Value) && - prevote.From.Equal(&other.From) -} - -func (prevote *Prevote) SizeHint() int { - return surge.SizeHint(prevote.Height) + - surge.SizeHint(prevote.Round) + - surge.SizeHint(prevote.Value) + - surge.SizeHint(prevote.From) -} - -func (prevote *Prevote) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, prevote.Height, m) - if err != nil { - return m, fmt.Errorf("marshaling height=%v: %v", prevote.Height, err) - } - m, err = surge.Marshal(w, prevote.Round, m) - if err != nil { - return m, fmt.Errorf("marshaling round=%v: %v", prevote.Round, err) - } - m, err = surge.Marshal(w, prevote.Value, m) - if err != nil { - return m, fmt.Errorf("marshaling value=%v: %v", prevote.Value, err) - } - m, err = surge.Marshal(w, prevote.From, m) - if err != nil { - return m, fmt.Errorf("marshaling from=%v: %v", prevote.From, err) - } - return m, nil -} - -func (prevote *Prevote) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &prevote.Height, m) - if err != nil { - return m, fmt.Errorf("unmarshaling height: %v", err) - } - m, err = surge.Unmarshal(r, &prevote.Round, m) - if err != nil { - return m, fmt.Errorf("unmarshaling round: %v", err) - } - m, err = surge.Unmarshal(r, &prevote.Value, m) - if err != nil { - return m, fmt.Errorf("unmarshaling value: %v", err) - } - m, err = surge.Unmarshal(r, &prevote.From, m) - if err != nil { - return m, fmt.Errorf("unmarshaling from: %v", err) - } - return m, nil -} - -type Precommit struct { - Height Height `json:"height"` - Round Round `json:"round"` - Value Value `json:"value"` - From Pid `json:"from"` -} - -func (precommit *Precommit) Equal(other *Precommit) bool { - return precommit.Height == other.Height && - precommit.Round == other.Round && - precommit.Value.Equal(&other.Value) && - precommit.From.Equal(&other.From) -} - -func (precommit *Precommit) SizeHint() int { - return surge.SizeHint(precommit.Height) + - surge.SizeHint(precommit.Round) + - surge.SizeHint(precommit.Value) + - surge.SizeHint(precommit.From) -} - -func (precommit *Precommit) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, precommit.Height, m) - if err != nil { - return m, fmt.Errorf("marshaling height=%v: %v", precommit.Height, err) - } - m, err = surge.Marshal(w, precommit.Round, m) - if err != nil { - return m, fmt.Errorf("marshaling round=%v: %v", precommit.Round, err) - } - m, err = surge.Marshal(w, precommit.Value, m) - if err != nil { - return m, fmt.Errorf("marshaling value=%v: %v", precommit.Value, err) - } - m, err = surge.Marshal(w, precommit.From, m) - if err != nil { - return m, fmt.Errorf("marshaling from=%v: %v", precommit.From, err) - } - return m, nil -} - -func (precommit *Precommit) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &precommit.Height, m) - if err != nil { - return m, fmt.Errorf("unmarshaling height: %v", err) - } - m, err = surge.Unmarshal(r, &precommit.Round, m) - if err != nil { - return m, fmt.Errorf("unmarshaling round: %v", err) - } - m, err = surge.Unmarshal(r, &precommit.Value, m) - if err != nil { - return m, fmt.Errorf("unmarshaling value: %v", err) - } - m, err = surge.Unmarshal(r, &precommit.From, m) - if err != nil { - return m, fmt.Errorf("unmarshaling from: %v", err) - } - return m, nil -} diff --git a/proc/message_test.go b/proc/message_test.go deleted file mode 100644 index 86f9c7ed..00000000 --- a/proc/message_test.go +++ /dev/null @@ -1 +0,0 @@ -package proc_test \ No newline at end of file diff --git a/proc/proc.go b/proc/proc.go deleted file mode 100644 index 25eaa48f..00000000 --- a/proc/proc.go +++ /dev/null @@ -1,688 +0,0 @@ -// Package proc implements the Byzantine fault tolerant consensus algorithm -// described by "The latest gossip of BFT consensus" (Buchman et al.), which can -// be found at https://arxiv.org/pdf/1807.04938.pdf. It makes extensive use of -// dependency injection, and concrete implementions must be careful to meet all -// of the requirements specified by the interface, otherwise the correctness of -// the consensus algorithm can be broken. -package proc - -// A Scheduler is used to determine which Process should be proposing a Vaue at -// the given Height and Round. A Scheduler must be derived solely from the -// Height, Round, and Values on which all correct Processes have already -// achieved consensus. -type Scheduler interface { - Schedule(height Height, round Round) Pid -} - -// A Proposer is used to propose new Values for consensus. A Proposer must only -// ever return a valid Value, and once it returns a Value, it must never return -// a different Value for the same Height and Round. -type Proposer interface { - Propose(Height, Round) Value -} - -// A Timer is used to schedule timeout events. -type Timer interface { - // TimeoutPropose is called when the Process needs its OnTimeoutPropose - // method called after a timeout. The timeout should be proportional to the - // Round. - TimeoutPropose(Height, Round) - // TimeoutPrevote is called when the Process needs its OnTimeoutPrevote - // method called after a timeout. The timeout should be proportional to the - // Round. - TimeoutPrevote(Height, Round) - // TimeoutPrecommit is called when the Process needs its OnTimeoutPrecommit - // method called after a timeout. The timeout should be proportional to the - // Round. - TimeoutPrecommit(Height, Round) -} - -// A Broadcaster is used to broadcast Propose, Prevote, and Precommit messages -// to all Processes in the consensus algorithm, including the Process that -// initiated the broadcast. It is assumed that all messages between correct -// Processes are eventually delivered, although no specific order is assumed. -// -// Once a Value has been broadcast as part of a Propose, Prevote, or Precommit -// message, different Values must not be broadcast for that same message type -// with the same Height and Round. The same restriction applies to valid Rounds -// broadcast with a Propose message. -type Broadcaster interface { - BroadcastPropose(Height, Round, Value, Round) - BroadcastPrevote(Height, Round, Value) - BroadcastPrecommit(Height, Round, Value) -} - -// A Validator is used to validate a proposed Value. Processes are not required -// to agree on the validity of a Value. -type Validator interface { - Valid(Value) bool -} - -// A Committer is used to emit Values that are committed. The commitment of a -// new Value implies that all correct Processes agree on this Value at this -// Height, and will never revert. -type Committer interface { - Commit(Height, Value) -} - -// A Catcher is used to catch bad behaviour in other Processes. For example, -// when the same Process sends two different Proposes at the same Height and -// Round. -type Catcher interface { - CatchDoublePropose(Propose, Propose) - CatchDoublePrevote(Prevote, Prevote) - CatchDoublePrecommit(Precommit, Precommit) -} - -// A Process is a deterministic finite state automaton that communicates with -// other Processes to implement a Byzantine fault tolerant consensus algorithm. -// It is intended to be used as part of a larger component that implements a -// Byzantine fault tolerant replicated state machine. -// -// All messages from previous and future Heights will be ignored. The component -// using the Process should buffer all messages from future Heights so that they -// are not lost. It is assumed that this component will also handle the -// authentication and rate-limiting of messages. -// -// Processes are not safe for concurrent use. All methods must be called by the -// same goroutine that allocates and starts the Process. -type Process struct { - - // Input interface that provide data to the Process. - scheduler Scheduler - proposer Proposer - validator Validator - - // Output interfaces that received data from the Process. - timer Timer - broadcaster Broadcaster - committer Committer - catcher Catcher - - // ProposeLogs store the Proposes for all Rounds. - ProposeLogs map[Round]Propose `json:"proposeLogs"` - // PrevoteLogs store the Prevotes for all Processes in all Rounds. - PrevoteLogs map[Round]map[Pid]Prevote `json:"prevoteLogs"` - // PrecommitLogs store the Precommits for all Processes in all Rounds. - PrecommitLogs map[Round]map[Pid]Precommit `json:"precommitLogs"` - // F is the maximum number of malicious adversaries that the Process can - // withstand while still maintaining safety and liveliness. - F int `json:"f"` - - // OnceFlags prevents events from happening more than once. - OnceFlags map[Round]OnceFlag `json:"onceFlags"` - - // Whoami represnts the Pid of this Process. It is assumed that the ECDSA - // privkey required to prove ownership of this Pid is known. - Whoami Pid `json:"whoami"` - - // State of the Process. - State `json:"state"` -} - -// Propose is used to notify the Process that a Propose message has been -// received (this includes Propose messages that the Process itself has -// broadcast). All conditions that could be opened by the receipt of a Propose -// message will be tried. -func (p *Process) Propose(propose Propose) { - if !p.insertPropose(propose) { - return - } - - p.trySkipToFutureRound(propose.Round) - p.tryCommitUponSufficientPrecommits(propose.Round) - p.tryPrecommitUponSufficientPrevotes() - p.tryPrevoteUponPropose() - p.tryPrevoteUponSufficientPrevotes() -} - -// Prevote is used to notify the Process that a Prevote message has been -// received (this includes Prevote messages that the Process itself has -// broadcast). All conditions that could be opened by the receipt of a Prevote -// message will be tried. -func (p *Process) Prevote(prevote Prevote) { - if !p.insertPrevote(prevote) { - return - } - - p.trySkipToFutureRound(prevote.Round) - p.tryPrecommitUponSufficientPrevotes() - p.tryPrecommitNilUponSufficientPrevotes() - p.tryPrevoteUponSufficientPrevotes() - p.tryTimeoutPrevoteUponSufficientPrevotes() -} - -// Precommit is used to notify the Process that a Precommit message has been -// received (this includes Precommit messages that the Process itself has -// broadcast). All conditions that could be opened by the receipt of a Precommit -// message will be tried. -func (p *Process) Precommit(precommit Precommit) { - if !p.insertPrecommit(precommit) { - return - } - - p.trySkipToFutureRound(precommit.Round) - p.tryCommitUponSufficientPrecommits(precommit.Round) - p.tryTimeoutPrecommitUponSufficientPrecommits() -} - -// Start the Process. -// -// L10: -// upon start do -// StartRound(0) -// -func (p *Process) Start() { - p.StartRound(0) -} - -// StartRound will progress the Process to a new Round. It does not asssume that -// the Height has changed. Since this changes the current Round and the current -// Step, most of the condition methods will be retried at the end (by way of -// defer). -// -// L11: -// Function StartRound(round) -// currentRound ← round -// currentStep ← propose -// if proposer(currentHeight, currentRound) = p then -// if validValue = nil then -// proposal ← validValue -// else -// proposal ← getValue() -// broadcast〈PROPOSAL, currentHeight, currentRound, proposal, validRound〉 -// else -// schedule OnTimeoutPropose(currentHeight, currentRound) to be executed after timeoutPropose(currentRound) -func (p *Process) StartRound(round Round) { - defer func() { - p.tryPrecommitUponSufficientPrevotes() - p.tryPrecommitNilUponSufficientPrevotes() - p.tryPrevoteUponPropose() - p.tryPrevoteUponSufficientPrevotes() - p.tryTimeoutPrecommitUponSufficientPrecommits() - p.tryTimeoutPrevoteUponSufficientPrevotes() - }() - - // Set the state the new round, and set the step to the first step in the - // sequence. We do not have special methods dedicated to change the current - // Roound, or changing the current Step to Proposing, because StartRound is - // the only location where this logic happens. - p.CurrentRound = round - p.CurrentStep = Proposing - - // If we are not the proposer, then we trigger the propose timeout. - proposer := p.scheduler.Schedule(p.CurrentHeight, p.CurrentRound) - if !p.Whoami.Equal(&proposer) { - p.timer.TimeoutPropose(p.CurrentHeight, p.CurrentRound) - return - } - - // If we are the proposer, then we emit a propose. - proposeValue := p.ValidValue - if proposeValue.Equal(&NilValue) { - proposeValue = p.proposer.Propose(p.CurrentHeight, p.CurrentRound) - } - p.broadcaster.BroadcastPropose( - p.CurrentHeight, - p.CurrentRound, - proposeValue, - p.ValidRound, - ) -} - -// OnTimeoutPropose is used to notify the Process that a timeout has been -// activated. It must only be called after the TimeoutPropose method in the -// Timer has been called. -// -// L57: -// Function OnTimeoutPropose(height, round) -// if height = currentHeight ∧ round = currentRound ∧ currentStep = propose then -// broadcast〈PREVOTE, currentHeight, currentRound, nil -// currentStep ← prevote -func (p *Process) OnTimeoutPropose(height Height, round Round) { - if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Proposing { - p.broadcaster.BroadcastPrevote(p.CurrentHeight, p.CurrentRound, NilValue) - p.stepToPrevoting() - } -} - -// OnTimeoutPrevote is used to notify the Process that a timeout has been -// activated. It must only be called after the TimeoutPrevote method in the -// Timer has been called. -// -// L61: -// Function OnTimeoutPrevote(height, round) -// if height = currentHeight ∧ round = currentRound ∧ currentStep = prevote then -// broadcast〈PREVOTE, currentHeight, currentRound, nil -// currentStep ← prevote -func (p *Process) OnTimeoutPrevote(height Height, round Round) { - if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Prevoting { - p.broadcaster.BroadcastPrecommit(p.CurrentHeight, p.CurrentRound, NilValue) - p.stepToPrecommitting() - } -} - -// OnTimeoutPrecommit is used to notify the Process that a timeout has been -// activated. It must only be called after the TimeoutPrecommit method in the -// Timer has been called. -// -// L65: -// Function OnTimeoutPrecommit(height, round) -// if height = currentHeight ∧ round = currentRound then -// StartRound(currentRound + 1) -func (p *Process) OnTimeoutPrecommit(height Height, round Round) { - if height == p.CurrentHeight && round == p.CurrentRound { - p.StartRound(round + 1) - } -} - -// L22: -// upon〈PROPOSAL, currentHeight, currentRound, v, −1〉from proposer(currentHeight, currentRound) -// while currentStep = propose do -// if valid(v) ∧ (lockedRound = −1 ∨ lockedValue = v) then -// broadcast〈PREVOTE, currentHeight, currentRound, id(v) -// else -// broadcast〈PREVOTE, currentHeight, currentRound, nil -// currentStep ← prevote -// -// This method must be tried whenever a Propose is received at the current -// Ronud, the current Round changes, the current Step changes to Proposing, the -// LockedRound changes, or the the LockedValue changes. -func (p *Process) tryPrevoteUponPropose() { - if p.CurrentStep != Proposing { - return - } - - propose, ok := p.ProposeLogs[p.CurrentRound] - if !ok { - return - } - if propose.ValidRound != InvalidRound { - return - } - - if p.LockedRound == InvalidRound || p.LockedValue.Equal(&propose.Value) { - p.broadcaster.BroadcastPrevote(p.CurrentHeight, p.CurrentRound, propose.Value) - } else { - p.broadcaster.BroadcastPrevote(p.CurrentHeight, p.CurrentRound, NilValue) - } - p.stepToPrevoting() -} - -// L28: -// -// upon〈PROPOSAL, currentHeight, currentRound, v, vr〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, vr, id(v)〉 -// while currentStep = propose ∧ (vr ≥ 0 ∧ vr < currentRound) do -// if valid(v) ∧ (lockedRound ≤ vr ∨ lockedValue = v) then -// broadcast〈PREVOTE, currentHeight, currentRound, id(v)〉 -// else -// broadcast〈PREVOTE, currentHeight, currentRound, nil〉 -// currentStep ← prevote -// -// This method must be tried whenever a Propose is received at the current Rond, -// a Prevote is received (at any Round), the current Round changes, the -// LockedRound changes, or the the LockedValue changes. -func (p *Process) tryPrevoteUponSufficientPrevotes() { - if p.CurrentStep != Proposing { - return - } - - propose, ok := p.ProposeLogs[p.CurrentRound] - if !ok { - return - } - if propose.ValidRound == InvalidRound || propose.ValidRound >= p.CurrentRound { - return - } - - prevotesInValidRound := 0 - for _, prevote := range p.PrevoteLogs[propose.ValidRound] { - if prevote.Value.Equal(&propose.Value) { - prevotesInValidRound++ - } - } - if prevotesInValidRound < 2*p.F+1 { - return - } - - if p.LockedRound <= propose.ValidRound || p.LockedValue.Equal(&propose.Value) { - p.broadcaster.BroadcastPrevote(p.CurrentHeight, p.CurrentRound, propose.Value) - } else { - p.broadcaster.BroadcastPrevote(p.CurrentHeight, p.CurrentRound, NilValue) - } - p.stepToPrevoting() -} - -// L34: -// -// upon 2f+ 1〈PREVOTE, currentHeight, currentRound, ∗〉 -// while currentStep = prevote for the first time do -// scheduleOnTimeoutPrevote(currentHeight, currentRound) to be executed after timeoutPrevote(currentRound) -// -// This method must be tried whenever a Prevote is received at the current -// Round, the current Round changes, or the current Step changes to Prevoting. -// It assumes that the Timer will eventually call the OnTimeoutPrevote method. -// This method must only succeed once in any current Round. -func (p *Process) tryTimeoutPrevoteUponSufficientPrevotes() { - if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) { - return - } - if p.CurrentStep != Prevoting { - return - } - if len(p.PrevoteLogs[p.CurrentRound]) == 2*p.F+1 { - p.timer.TimeoutPrevote(p.CurrentHeight, p.CurrentRound) - } - p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) -} - -// L36: -// -// upon〈PROPOSAL, currentHeight, currentRound, v, ∗〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, currentRound, id(v)〉 -// while valid(v) ∧ currentStep ≥ prevote for the first time do -// if currentStep = prevote then -// lockedValue ← v -// lockedRound ← currentRound -// broadcast〈PRECOMMIT, currentHeight, currentRound, id(v))〉 -// currentStep ← precommit -// validValue ← v -// validRound ← currentRound -// -// This method must be tried whenever a Propose is received at the current -// Round, a Prevote is received at the current Round, the current Round changes, -// or the current Step changes to Prevoting or Precommitting. This method must -// only succeed once in any current Round. -func (p *Process) tryPrecommitUponSufficientPrevotes() { - if p.checkOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) { - return - } - if p.CurrentStep < Prevoting { - return - } - - propose, ok := p.ProposeLogs[p.CurrentRound] - if !ok { - return - } - prevotesForValue := 0 - for _, prevote := range p.PrevoteLogs[p.CurrentRound] { - if prevote.Value.Equal(&propose.Value) { - prevotesForValue++ - } - } - if prevotesForValue < 2*p.F+1 { - return - } - - if p.CurrentStep == Prevoting { - p.LockedValue = propose.Value - p.LockedRound = p.CurrentRound - p.broadcaster.BroadcastPrecommit(p.CurrentHeight, p.CurrentRound, propose.Value) - p.stepToPrecommitting() - - // Beacuse the LockedValue and LockedRound have changed, we need to try - // this condition again. - defer func() { - p.tryPrevoteUponPropose() - p.tryPrevoteUponSufficientPrevotes() - }() - } - p.ValidValue = propose.Value - p.ValidRound = p.CurrentRound - p.setOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) -} - -// L44: -// -// upon 2f+ 1〈PREVOTE, currentHeight, currentRound, nil〉 -// while currentStep = prevote do -// broadcast〈PRECOMMIT, currentHeight, currentRound, nil〉 -// currentStep ← precommit -// -// This method must be tried whenever a Prevote is received at the current -// Round, the current Round changes, or the Step changes to Prevoting. -func (p *Process) tryPrecommitNilUponSufficientPrevotes() { - if p.CurrentStep != Prevoting { - return - } - prevotesForNil := 0 - for _, prevote := range p.PrevoteLogs[p.CurrentRound] { - if prevote.Value.Equal(&NilValue) { - prevotesForNil++ - } - } - if prevotesForNil == 2*p.F+1 { - p.broadcaster.BroadcastPrecommit(p.CurrentHeight, p.CurrentRound, NilValue) - p.stepToPrecommitting() - } -} - -// L47: -// -// upon 2f+ 1〈PRECOMMIT, currentHeight, currentRound, ∗〉for the first time do -// scheduleOnTimeoutPrecommit(currentHeight, currentRound) to be executed after timeoutPrecommit(currentRound) -// -// This method must be tried whenever a Precommit is received at the current -// Round, or the current Round changes. It assumes that the Timer will -// eventually call the OnTimeoutPrecommit method. This method must only succeed -// once in any current Round. -func (p *Process) tryTimeoutPrecommitUponSufficientPrecommits() { - if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) { - return - } - if len(p.PrecommitLogs[p.CurrentRound]) == 2*p.F+1 { - p.timer.TimeoutPrecommit(p.CurrentHeight, p.CurrentRound) - p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) - } -} - -// L49: -// -// upon〈PROPOSAL, currentHeight, r, v, ∗〉from proposer(currentHeight, r) AND 2f+ 1〈PRECOMMIT, currentHeight, r, id(v)〉 -// while decision[currentHeight] = nil do -// if valid(v) then -// decision[currentHeight] = v -// currentHeight ← currentHeight + 1 -// reset -// StartRound(0) -// -// This method must be tried whenever a Propose is received, or a Precommit is -// received. Because this method checks whichever Round is relevant (i.e. the -// Round of the Propose/Precommit), it does not need to be tried whenever the -// current Round changes. -// -// We can avoid explicitly checking for validity of the Propose value, because -// no Propose value is stored in the message logs unless it is valid. We can -// also avoid checking for a nil-decision at the current Height, because the -// only condition under which this would not be true is when the Process has -// progressed passed the Height in question (put another way, the fact that this -// method causes the Height to be incremented prevents it from being triggered -// multiple times). -func (p *Process) tryCommitUponSufficientPrecommits(round Round) { - propose, ok := p.ProposeLogs[round] - if !ok { - return - } - precommitsForValue := 0 - for _, precommit := range p.PrecommitLogs[round] { - if precommit.Value.Equal(&propose.Value) { - precommitsForValue++ - } - } - if precommitsForValue == 2*p.F+1 { - p.committer.Commit(p.CurrentHeight, propose.Value) - p.CurrentHeight++ - - // Empty message logs in preparation for the new Height. - p.ProposeLogs = map[Round]Propose{} - p.PrevoteLogs = map[Round]map[Pid]Prevote{} - p.PrecommitLogs = map[Round]map[Pid]Precommit{} - p.OnceFlags = map[Round]OnceFlag{} - - // Reset the State and start from the first Round in the new Height. - p.Reset() - p.StartRound(0) - } -} - -// L55: -// -// upon f+ 1〈∗, currentHeight, r, ∗, ∗〉with r > currentRound do -// StartRound(r) -// -// This method must be tried whenever a Propose is received, a Prevote is -// received, or a Precommit is received. Because this method checks whichever -// Round is relevant (i.e. the Round of the Propose/Prevote/Precommit), and an -// increase in the current Round can only cause this condition to be closed, it -// does not need to be tried whenever the current Round changes. -func (p *Process) trySkipToFutureRound(round Round) { - if round <= p.CurrentRound { - return - } - - msgsInRound := 0 - if _, ok := p.ProposeLogs[round]; ok { - msgsInRound = 1 - } - msgsInRound += len(p.PrevoteLogs[round]) - msgsInRound += len(p.PrecommitLogs[round]) - - if msgsInRound == p.F+1 { - p.StartRound(round) - } -} - -// insertPropose after validating it and checking for duplicates. If the Propose -// was accepted and inserted, then it return true, otherwise it returns false. -func (p *Process) insertPropose(propose Propose) bool { - if propose.Height != p.CurrentHeight { - return false - } - - existingPropose, ok := p.ProposeLogs[propose.Round] - if ok { - // We have caught a Process attempting to broadcast two different - // Proposes at the same Height and Round. Even though we only - // explicitly check the Round, we know that the Proposes will have the - // same Height, because we only keep message logs for message with the - // same Height as the current Height of the Process. - if !propose.Equal(&existingPropose) { - p.catcher.CatchDoublePropose(propose, existingPropose) - } - return false - } - proposer := p.scheduler.Schedule(propose.Height, propose.Round) - if !proposer.Equal(&propose.From) { - return false - } - - // By never inserting a Propose that is not valid, we can avoid the validity - // checks elsewhere in the Process. - if !p.validator.Valid(propose.Value) { - return false - } - - p.ProposeLogs[propose.Round] = propose - return true -} - -// insertPrevote after validating it and checking for duplicates. If the Prevote -// was accepted and inserted, then it return true, otherwise it returns false. -func (p *Process) insertPrevote(prevote Prevote) bool { - if prevote.Height != p.CurrentHeight { - return false - } - if _, ok := p.PrevoteLogs[prevote.Round]; !ok { - p.PrevoteLogs[prevote.Round] = map[Pid]Prevote{} - } - - existingPrevote, ok := p.PrevoteLogs[prevote.Round][prevote.From] - if ok { - // We have caught a Process attempting to broadcast two different - // Prevotes at the same Height and Round. Even though we only explicitly - // check the Round, we know that the Prevotes will have the same Height, - // because we only keep message logs for message with the same Height as - // the current Height of the Process. - if !prevote.Equal(&existingPrevote) { - p.catcher.CatchDoublePrevote(prevote, existingPrevote) - } - return false - } - - p.PrevoteLogs[prevote.Round][prevote.From] = prevote - return true -} - -// insertPrecommit after validating it and checking for duplicates. If the -// Precommit was accepted and inserted, then it return true, otherwise it -// returns false. -func (p *Process) insertPrecommit(precommit Precommit) bool { - if precommit.Height != p.CurrentHeight { - return false - } - if _, ok := p.PrecommitLogs[precommit.Round]; !ok { - p.PrecommitLogs[precommit.Round] = map[Pid]Precommit{} - } - - existingPrecommit, ok := p.PrecommitLogs[precommit.Round][precommit.From] - if ok { - // We have caught a Process attempting to broadcast two different - // Precommits at the same Height and Round. Even though we only - // explicitly check the Round, we know that the Precommits will have the - // same Height, because we only keep message logs for message with the - // same Height as the current Height of the Process. - if !precommit.Equal(&existingPrecommit) { - p.catcher.CatchDoublePrecommit(precommit, existingPrecommit) - } - return false - } - - p.PrecommitLogs[precommit.Round][precommit.From] = precommit - return true -} - -// stepToPrevoting puts the Process into the Prevoting Step. This will also try -// other methods that might now have passing conditions. -func (p *Process) stepToPrevoting() { - p.CurrentStep = Prevoting - - // Because the current Step of the Process has changed, new conditions might - // be open, so we try the relevant ones. Once flags protect us against - // double-tries where necessary. - p.tryPrecommitUponSufficientPrevotes() - p.tryPrecommitNilUponSufficientPrevotes() - p.tryTimeoutPrevoteUponSufficientPrevotes() -} - -// stepToPrecommitting puts the Process into the Precommitting Step. This will -// also try other methods that might now have passing conditions. -func (p *Process) stepToPrecommitting() { - p.CurrentStep = Precommitting - - // Because the current Step of the Process has changed, new conditions might - // be open, so we try the relevant ones. Once flags protect us against - // double-tries where necessary. - p.tryPrecommitUponSufficientPrevotes() -} - -// checkOnceFlag returns true if the OnceFlag has already been set for the given -// Round. Otherwise, it returns false. -func (p *Process) checkOnceFlag(round Round, flag OnceFlag) bool { - return p.OnceFlags[round]&flag == flag -} - -// setOnceFlag set the OnceFlag for the given Round. -func (p *Process) setOnceFlag(round Round, flag OnceFlag) { - p.OnceFlags[round] |= flag -} - -// A OnceFlag is used to guarantee that events only happen once in any given -// Round. -type OnceFlag uint16 - -// Enumerate all OnceFlag values. -const ( - OnceFlagTimeoutPrecommitUponSufficientPrecommits = OnceFlag(1) - OnceFlagTimeoutPrevoteUponSufficientPrevotes = OnceFlag(2) - OnceFlagPrecommitUponSufficientPrevotes = OnceFlag(4) -) diff --git a/proc/proc_test.go b/proc/proc_test.go deleted file mode 100644 index 86f9c7ed..00000000 --- a/proc/proc_test.go +++ /dev/null @@ -1 +0,0 @@ -package proc_test \ No newline at end of file diff --git a/proc/state.go b/proc/state.go deleted file mode 100644 index ede2f93b..00000000 --- a/proc/state.go +++ /dev/null @@ -1,110 +0,0 @@ -package proc - -import ( - "bytes" - - "github.com/renproject/id" -) - -// The State of a Process. It is isolated from the Process so that it can be -// easily marshaled to/from JSON. -type State struct { - CurrentHeight Height `json:"currentHeight"` - CurrentRound Round `json:"currentRound"` - CurrentStep Step `json:"currentStep"` - LockedValue Value `json:"lockedValue"` // The most recent value for which a precommit message has been sent. - LockedRound Round `json:"lockedRound"` // The last round in which the process sent a precommit message that is not nil. - ValidValue Value `json:"validValue"` // The most recent possible decision value. - ValidRound Round `json:"validRound"` // The last round in which valid value is updated. -} - -// DefaultState returns a State with all values set to their default. See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func DefaultState() State { - return State{ - CurrentHeight: 1, // Skip genesis. - CurrentRound: 0, - CurrentStep: Proposing, - LockedValue: Value{}, - LockedRound: InvalidRound, - ValidValue: Value{}, - ValidRound: InvalidRound, - } -} - -// Reset the State (not all values are reset). See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func (state *State) Reset() { - state.LockedValue = Value{} - state.LockedRound = InvalidRound - - state.ValidValue = Value{} - state.ValidRound = InvalidRound -} - -// Equal compares one State with another. If they are equal, then it returns -// true, otherwise it returns false. -func (state *State) Equal(other *State) bool { - return state.CurrentHeight == other.CurrentHeight && - state.CurrentRound == other.CurrentRound && - state.CurrentStep == other.CurrentStep && - state.LockedValue.Equal(&other.LockedValue) && - state.LockedRound == other.LockedRound && - state.ValidValue.Equal(&other.ValidValue) && - state.ValidRound == other.ValidRound -} - -// Step defines a typedef for uint8 values that represent the step of the state -// of a Process partaking in the consensus algorithm. -type Step uint8 - -// Enumerate step values. -const ( - Proposing = Step(0) - Prevoting = Step(1) - Precommitting = Step(2) -) - -// Height defines a typedef for int64 values that represent the height of a -// Value at which the consensus algorithm is attempting to reach consensus. -type Height int64 - -// Round defines a typedef for int64 values that represent the round of a Value -// at which the consensus algorithm is attempting to reach consensus. -type Round int64 - -const ( - // InvalidRound is a reserved int64 that represents an invalid Round. It is - // used when a Process is trying to represent that it does have have a - // LockedRound or ValidRound. - InvalidRound = Round(-1) -) - -// Value defines a typedef for hashes that represent the hashes of proposed -// values in the consensus algorithm. In the context of a blockchain, a Value -// would be a block. -type Value id.Hash - -// Equal compares two Values. If they are equal, then it returns true, otherwise -// it returns false. -func (v *Value) Equal(other *Value) bool { - return bytes.Equal(v[:], other[:]) -} - -var ( - // NilValue is a reserved hash that represents when a Process is - // prevoting/precommitting to nothing (i.e. the Process wants to progress to - // the next Round). - NilValue = Value(id.Hash{}) -) - -// Pid defines a typedef for hsahes that represent the unique identity of a -// Process in the consensus algorithm. No distrinct Processes should ever have -// the same Pid, and a Process must maintain the same Pid for its entire life. -type Pid id.Hash - -// Equal compares two Pids. If they are equal, the it returns true, otherwise it -// returns false. -func (pid *Pid) Equal(other *Pid) bool { - return bytes.Equal(pid[:], other[:]) -} diff --git a/proc/state_test.go b/proc/state_test.go deleted file mode 100644 index 0189c8fb..00000000 --- a/proc/state_test.go +++ /dev/null @@ -1 +0,0 @@ -package proc_test diff --git a/process/catch.go b/process/catch.go deleted file mode 100644 index 971280e1..00000000 --- a/process/catch.go +++ /dev/null @@ -1,28 +0,0 @@ -package process - -// A Catcher is used to publish events about potential malicious behaviour by -// other Processes. -type Catcher interface { - // DidReceiveMessageConflict is called when a new Message is received that - // conflicts with an existing Message. Messages of the same type are defined - // to be in conflict when they are from the same Signatory, Height, and - // Round, but have different contents. - // - // For example, when proposing a Block in any given Height and Round, an - // honest Process should only ever propose one Block. A malicious Process - // might try to break consensus by proposing two different Blocks, resulting - // in two conflicting Proposes in the same Height and Round. - DidReceiveMessageConflict(conflicting, message Message) -} - -type catchAndIgnore struct{} - -// CatchAndIgnore returns a Catcher that ignores all potentiall malicious -// behaviour. It should only be used during testing, or when Processes are known -// to be honest. -func CatchAndIgnore() Catcher { - return catchAndIgnore{} -} - -// DidReceiveMessageConflict does nothing. -func (catchAndIgnore) DidReceiveMessageConflict(conflicting, message Message) {} diff --git a/process/catch_test.go b/process/catch_test.go deleted file mode 100644 index 05f732af..00000000 --- a/process/catch_test.go +++ /dev/null @@ -1 +0,0 @@ -package process_test diff --git a/process/marshal.go b/process/marshal.go deleted file mode 100644 index becf1145..00000000 --- a/process/marshal.go +++ /dev/null @@ -1,460 +0,0 @@ -package process - -import ( - "fmt" - "io" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/id" - "github.com/renproject/surge" -) - -// SizeHint of how many bytes will be needed to represent this propose in -// binary. -func (propose Propose) SizeHint() int { - return surge.SizeHint(propose.signatory) + - surge.SizeHint(propose.sig) + - surge.SizeHint(propose.height) + - surge.SizeHint(propose.round) + - surge.SizeHint(propose.block) + - surge.SizeHint(propose.validRound) + - surge.SizeHint(propose.latestCommit.Block) + - surge.SizeHint(propose.latestCommit.Precommits) -} - -// Marshal this propose into binary. -func (propose Propose) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, propose.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.sig, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.height, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.round, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.block, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.validRound, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, propose.latestCommit.Block, m); err != nil { - return m, err - } - return surge.Marshal(w, propose.latestCommit.Precommits, m) -} - -// Unmarshal into this propose from binary. -func (propose *Propose) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &propose.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.sig, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.height, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.round, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.block, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.validRound, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &propose.latestCommit.Block, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &propose.latestCommit.Precommits, m) -} - -// SizeHint of how many bytes will be needed to represent this prevote in -// binary. -func (prevote Prevote) SizeHint() int { - return surge.SizeHint(prevote.signatory) + - surge.SizeHint(prevote.sig) + - surge.SizeHint(prevote.height) + - surge.SizeHint(prevote.round) + - surge.SizeHint(prevote.blockHash) + - surge.SizeHint(prevote.nilReasons) -} - -// Marshal this prevote into binary. -func (prevote Prevote) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, prevote.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, prevote.sig, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, prevote.height, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, prevote.round, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, prevote.blockHash, m); err != nil { - return m, err - } - return surge.Marshal(w, prevote.nilReasons, m) -} - -// Unmarshal into this prevote from binary. -func (prevote *Prevote) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &prevote.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &prevote.sig, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &prevote.height, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &prevote.round, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &prevote.blockHash, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &prevote.nilReasons, m) -} - -// SizeHint of how many bytes will be needed to represent this precommit in -// binary. -func (precommit Precommit) SizeHint() int { - return surge.SizeHint(precommit.signatory) + - surge.SizeHint(precommit.sig) + - surge.SizeHint(precommit.height) + - surge.SizeHint(precommit.round) + - surge.SizeHint(precommit.blockHash) -} - -// Marshal this precommit into binary. -func (precommit Precommit) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, precommit.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, precommit.sig, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, precommit.height, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, precommit.round, m); err != nil { - return m, err - } - return surge.Marshal(w, precommit.blockHash, m) -} - -// Unmarshal into this precommit from binary. -func (precommit *Precommit) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &precommit.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &precommit.sig, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &precommit.height, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &precommit.round, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &precommit.blockHash, m) -} - -// SizeHint of how many bytes will be needed to represent this resync in -// binary. -func (resync Resync) SizeHint() int { - return surge.SizeHint(resync.signatory) + - surge.SizeHint(resync.sig) + - surge.SizeHint(resync.height) + - surge.SizeHint(resync.round) + - surge.SizeHint(resync.timestamp) -} - -// Marshal this resync into binary. -func (resync Resync) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, resync.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, resync.sig, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, resync.height, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, resync.round, m); err != nil { - return m, err - } - return surge.Marshal(w, resync.timestamp, m) -} - -// Unmarshal into this resync from binary. -func (resync *Resync) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &resync.signatory, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &resync.sig, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &resync.height, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &resync.round, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &resync.timestamp, m) -} - -// SizeHint of how many bytes will be needed to represent this inbox in binary. -func (inbox Inbox) SizeHint() int { - return surge.SizeHint(uint32(inbox.f)) + surge.SizeHint(inbox.messages) -} - -// Marshal this inbox into binary. -func (inbox Inbox) Marshal(w io.Writer, m int) (int, error) { - // Write f. - m, err := surge.Marshal(w, int64(inbox.f), m) - if err != nil { - return m, err - } - // Write message type. - if m, err = surge.Marshal(w, inbox.messageType, m); err != nil { - return m, err - } - // Write the number of heights. - if m, err = surge.Marshal(w, uint32(len(inbox.messages)), m); err != nil { - return m, err - } - // Write rounds for each height. - for height, rounds := range inbox.messages { - // Write the height. - if m, err = surge.Marshal(w, height, m); err != nil { - return m, err - } - // Write the number of rounds at this height. - if m, err = surge.Marshal(w, uint32(len(rounds)), m); err != nil { - return m, err - } - // Write the rounds at this height. - for round, signatories := range rounds { - // Write the round. - if m, err := surge.Marshal(w, round, m); err != nil { - return m, err - } - // Write number of signatories. - if m, err = surge.Marshal(w, uint32(len(signatories)), m); err != nil { - return m, err - } - // Write signatory/message pairs. - for signatory, message := range signatories { - if m, err = surge.Marshal(w, signatory, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, message, m); err != nil { - return m, err - } - } - } - } - return m, nil -} - -// Unmarshal into this inbox from binary. -func (inbox *Inbox) Unmarshal(r io.Reader, m int) (int, error) { - // Read f. - var f int64 - m, err := surge.Unmarshal(r, &f, m) - if err != nil { - return m, fmt.Errorf("cannot unmarshal f: %v", err) - } - inbox.f = int(f) - // Read message type. - if m, err = surge.Unmarshal(r, &inbox.messageType, m); err != nil { - return m, err - } - // Read the number of heights. - numHeights := uint32(0) - if m, err = surge.Unmarshal(r, &numHeights, m); err != nil { - return m, fmt.Errorf("cannot unmarshal number of heights: %v", err) - } - if int(numHeights) < 0 { - return m, fmt.Errorf("cannot unmarshal number of heights: unexpected negative length") - } - m -= int(numHeights) - if m <= 0 { - return m, surge.ErrMaxBytesExceeded - } - // Read rounds for each height. - inbox.messages = map[block.Height]map[block.Round]map[id.Signatory]Message{} - for i := uint32(0); i < numHeights; i++ { - // Read the height. - height := block.Height(0) - if m, err = surge.Unmarshal(r, &height, m); err != nil { - return m, fmt.Errorf("cannot unmarshal height: %v", err) - } - // Read the number of rounds at this height. - numRounds := uint32(0) - if m, err = surge.Unmarshal(r, &numRounds, m); err != nil { - return m, fmt.Errorf("cannot unmarshal number of rounds: %v", err) - } - if int(numRounds) < 0 { - return m, fmt.Errorf("cannot unmarshal number of rounds: unexpected negative length") - } - m -= int(numRounds) - if m <= 0 { - return m, surge.ErrMaxBytesExceeded - } - - inbox.messages[height] = map[block.Round]map[id.Signatory]Message{} - for j := uint32(0); j < numRounds; j++ { - // Read the round. - round := block.Round(0) - if m, err = surge.Unmarshal(r, &round, m); err != nil { - return m, fmt.Errorf("cannot unmarshal middle key: %v", err) - } - // Read the number of signatories in this round. - numSignatories := uint32(0) - if m, err = surge.Unmarshal(r, &numSignatories, m); err != nil { - return m, fmt.Errorf("cannot unmarshal middle length: %v", err) - } - if int(numSignatories) < 0 { - return m, fmt.Errorf("expected negative length") - } - m -= int(numSignatories) - if m <= 0 { - return m, surge.ErrMaxBytesExceeded - } - inbox.messages[height][round] = map[id.Signatory]Message{} - for k := uint32(0); k < numSignatories; k++ { - // Read the signatory. - signatory := id.Signatory{} - if m, err = surge.Unmarshal(r, &signatory, m); err != nil { - return m, fmt.Errorf("cannot unmarshal inner key: %v", err) - } - // Read the message from this signatory. - switch inbox.messageType { - case ProposeMessageType: - message := new(Propose) - if m, err = message.Unmarshal(r, m); err != nil { - return m, fmt.Errorf("cannot unmarshal propose: %v", err) - } - inbox.messages[height][round][signatory] = message - case PrevoteMessageType: - message := new(Prevote) - if m, err = message.Unmarshal(r, m); err != nil { - return m, fmt.Errorf("cannot unmarshal prevote: %v", err) - } - inbox.messages[height][round][signatory] = message - case PrecommitMessageType: - message := new(Precommit) - if m, err = message.Unmarshal(r, m); err != nil { - return m, fmt.Errorf("cannot unmarshal precommit: %v", err) - } - inbox.messages[height][round][signatory] = message - default: - return m, fmt.Errorf("unsupported MessageType=%v", inbox.messageType) - } - } - } - } - return m, nil -} - -// SizeHint of how many bytes will be needed to represent state in binary. -func (state State) SizeHint() int { - return surge.SizeHint(state.CurrentHeight) + - surge.SizeHint(state.CurrentRound) + - surge.SizeHint(state.CurrentStep) + - surge.SizeHint(state.LockedBlock) + - surge.SizeHint(state.LockedRound) + - surge.SizeHint(state.ValidBlock) + - surge.SizeHint(state.ValidRound) + - surge.SizeHint(state.Proposals) + - surge.SizeHint(state.Prevotes) + - surge.SizeHint(state.Precommits) -} - -// Marshal this state into binary. -func (state State) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, state.CurrentHeight, m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.CurrentRound, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.CurrentStep, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.LockedBlock, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.LockedRound, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.ValidBlock, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.ValidRound, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.Proposals, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, state.Prevotes, m); err != nil { - return m, err - } - return surge.Marshal(w, state.Precommits, m) -} - -// Unmarshal into this state from binary. -func (state *State) Unmarshal(r io.Reader, m int) (int, error) { - m, err := surge.Unmarshal(r, &state.CurrentHeight, m) - if err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.CurrentRound, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.CurrentStep, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.LockedBlock, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.LockedRound, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.ValidBlock, m); err != nil { - return m, err - } - if m, err = surge.Unmarshal(r, &state.ValidRound, m); err != nil { - return m, err - } - state.Proposals = new(Inbox) - if m, err = surge.Unmarshal(r, state.Proposals, m); err != nil { - return m, err - } - state.Prevotes = new(Inbox) - if m, err = surge.Unmarshal(r, state.Prevotes, m); err != nil { - return m, err - } - state.Precommits = new(Inbox) - return surge.Unmarshal(r, state.Precommits, m) -} diff --git a/process/marshal_test.go b/process/marshal_test.go deleted file mode 100644 index a7a7d645..00000000 --- a/process/marshal_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package process_test - -import ( - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/process" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Marshaling", func() { - Context("when marshaling the same propose multiple times", func() { - It("should return the same bytes", func() { - propose := RandomMessage(ProposeMessageType) - proposeBytes, err := surge.ToBinary(propose) - Expect(err).ToNot(HaveOccurred()) - for i := 0; i < 100; i++ { - tmpProposeBytes, err := surge.ToBinary(propose) - Expect(err).ToNot(HaveOccurred()) - Expect(tmpProposeBytes).Should(Equal(proposeBytes)) - } - }) - }) - - Context("when marshaling the same prevote multiple times", func() { - It("should return the same bytes", func() { - prevote := RandomMessage(PrevoteMessageType) - prevoteBytes, err := surge.ToBinary(prevote) - Expect(err).ToNot(HaveOccurred()) - for i := 0; i < 100; i++ { - tmpPrevoteBytes, err := surge.ToBinary(prevote) - Expect(err).ToNot(HaveOccurred()) - Expect(tmpPrevoteBytes).Should(Equal(prevoteBytes)) - } - }) - }) - - Context("when marshaling the same prevote multiple times", func() { - It("should return the same bytes", func() { - precommit := RandomMessage(PrecommitMessageType) - precommitBytes, err := surge.ToBinary(precommit) - Expect(err).ToNot(HaveOccurred()) - for i := 0; i < 100; i++ { - tmpPrecommitBytes, err := surge.ToBinary(precommit) - Expect(err).ToNot(HaveOccurred()) - Expect(tmpPrecommitBytes).Should(Equal(precommitBytes)) - } - }) - }) - - Context("when marshaling the same resync multiple times", func() { - It("should return the same bytes", func() { - resync := RandomMessage(ResyncMessageType) - resyncBytes, err := surge.ToBinary(resync) - Expect(err).ToNot(HaveOccurred()) - for i := 0; i < 100; i++ { - tmpResyncBytes, err := surge.ToBinary(resync) - Expect(err).ToNot(HaveOccurred()) - Expect(tmpResyncBytes).Should(Equal(resyncBytes)) - } - }) - }) -}) diff --git a/process/message.go b/process/message.go index c484cfd7..69844a34 100644 --- a/process/message.go +++ b/process/message.go @@ -1,600 +1,338 @@ package process import ( - "crypto/ecdsa" - "crypto/sha256" - "errors" + "bytes" "fmt" "io" - "time" - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" "github.com/renproject/id" "github.com/renproject/surge" ) -// MessageType distinguished between the three valid (and one invalid) messages -// types that are supported during consensus rounds. -type MessageType uint64 - -// SizeHint of how many bytes will be needed to represent message types in -// binary. -func (mt MessageType) SizeHint() int { - return surge.SizeHint(uint64(mt)) -} - -// Marshal this message type into binary. -func (mt MessageType) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, uint64(mt), m) +// A Propose message is sent by the proposer Process at most once per Round. The +// Scheduler interfaces determines which Process is the proposer at any given +// Height and Round. +type Propose struct { + Height Height `json:"height"` + Round Round `json:"round"` + ValidRound Round `json:"validRound"` + Value Value `json:"value"` + From id.Signatory `json:"from"` + Signature id.Signature `json:"signature"` } -// Unmarshal into this message type from binary. -func (mt *MessageType) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*uint64)(mt), m) +func NewProposeHash(height Height, round Round, validRound Round, value Value) (id.Hash, error) { + buf := new(bytes.Buffer) + buf.Grow(surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(validRound) + surge.SizeHint(value)) + return NewProposeHashWithBuffer(height, round, validRound, value, buf) } -const ( - // NilMessageType is invalid and must not be used. - NilMessageType = 0 - // ProposeMessageType is used by messages that propose blocks for consensus. - ProposeMessageType = 1 - // PrevoteMessageType is used by messages that are prevoting for block - // hashes (or nil prevoting). - PrevoteMessageType = 2 - // PrecommitMessageType is used by messages that are precommitting for block - // hashes (or nil precommitting). - PrecommitMessageType = 3 - // ResyncMessageType is used by messages that query others for previous - // messages. - ResyncMessageType = 4 -) - -// Messages is a wrapper around the `[]Message` type. -type Messages []Message - -// The Message interface defines the common behaviour of all messages that are -// broadcast throughout the network during consensus rounds. -type Message interface { - // Stringer allows Messages to format themselves as strings. This is mostly - // used for generating sighashes. - fmt.Stringer - // Surger allows Messages to un/marshal themselves from/to binary. - surge.Surger - - // The Signatory that sent the message. - Signatory() id.Signatory - // The SigHash that is expected to by signed by the signatory. This is used - // to authenticate the claimed signatory. - SigHash() id.Hash - // The Signature produced by the signatory signing the sighash. This is used - // to authenticate the claimed signatory. - Sig() id.Signature - - // The Height of the blockchain in which this message was broadcast. - Height() block.Height - // The Round of consensus in which this message was broadcast. - Round() block.Round - // The BlockHash of the block to this message concerns. Proposals will be - // proposing the block identified by this hash, prevotes will be prevoting - // for the block identified by this hash (nil prevotes will use - // `InvalidBlockHash`), and precommits will be precommitting for block - // identified by this hash (nil precommits will also use - // `InvalidBlockHash`). - BlockHash() id.Hash - - // Type returns the message type of this message. This is useful for - // marshaling/unmarshaling when type information is elided. - Type() MessageType -} - -var ( - // ErrBlockHashNotProvided is returned when querying the block hash for a - // message type that does not implement the function. - ErrBlockHashNotProvided = errors.New("block hash not provided") -) - -// Sign a message using an ECDSA private key. The resulting signature will be -// stored inside the message. -func Sign(m Message, privKey ecdsa.PrivateKey) error { - sigHash := m.SigHash() - signatory := id.NewSignatory(privKey.PublicKey) - sig, err := crypto.Sign(sigHash[:], &privKey) +func NewProposeHashWithBuffer(height Height, round Round, validRound Round, value Value, buf *bytes.Buffer) (id.Hash, error) { + m, err := surge.Marshal(buf, height, surge.MaxBytes) if err != nil { - return fmt.Errorf("invariant violation: error signing message: %v", err) + return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) } - if len(sig) != id.SignatureLength { - return fmt.Errorf("invariant violation: invalid signed message, expected = %v, got = %v", id.SignatureLength, len(sig)) + m, err = surge.Marshal(buf, round, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) } - - switch m := m.(type) { - case *Propose: - m.signatory = signatory - copy(m.sig[:], sig) - case *Prevote: - m.signatory = signatory - copy(m.sig[:], sig) - case *Precommit: - m.signatory = signatory - copy(m.sig[:], sig) - case *Resync: - m.signatory = signatory - copy(m.sig[:], sig) - default: - panic(fmt.Errorf("invariant violation: unexpected message type=%T", m)) - } - return nil -} - -// Verify that the signature in a message is from the expected signatory. This -// is done by checking the `Message.Sig()` against the `Message.SigHash()` and -// `Message.Signatory()`. -func Verify(m Message) error { - sigHash := m.SigHash() - sig := m.Sig() - pubKey, err := crypto.SigToPub(sigHash[:], sig[:]) + m, err = surge.Marshal(buf, validRound, m) if err != nil { - return fmt.Errorf("error verifying message: %v", err) + return id.Hash{}, fmt.Errorf("marshaling valid round=%v: %v", validRound, err) } - - signatory := id.NewSignatory(*pubKey) - if !m.Signatory().Equal(signatory) { - return fmt.Errorf("bad signatory: expected signatory=%v, got signatory=%v", m.Signatory(), signatory) + m, err = surge.Marshal(buf, value, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) } - return nil + return id.NewHash(buf.Bytes()), nil } -// Proposes is a wrapper around the `[]Propose` type. -type Proposes []Propose - -// Propose a block for committment. -type Propose struct { - signatory id.Signatory - sig id.Signature - height block.Height - round block.Round - block block.Block - validRound block.Round - - latestCommit LatestCommit -} - -// The LatestCommit can be attached to a proposal. It stores the latest -// committed block, and a set of precommits that prove this block was committed. -// This is useful for allowing processes that have fallen out-of-sync to fast -// forward. See https://github.com/renproject/hyperdrive/wiki/Consensus for more -// information about fast fowarding. -type LatestCommit struct { - Block block.Block - Precommits []Precommit +// Equal compares two Proposes. If they are equal, then it return true, +// otherwise it returns false. The signatures are not checked for equality, +// because signatures include randomness. +func (propose Propose) Equal(other *Propose) bool { + return propose.Height == other.Height && + propose.Round == other.Round && + propose.ValidRound == other.ValidRound && + propose.Value.Equal(&other.Value) && + propose.From.Equal(&other.From) } -func NewPropose(height block.Height, round block.Round, b block.Block, validRound block.Round) *Propose { - return &Propose{ - height: height, - round: round, - block: b, - validRound: validRound, - latestCommit: LatestCommit{ - Block: block.InvalidBlock, - Precommits: []Precommit{}, - }, +// SizeHint returns the number of bytes required to represent this message in +// binary. +func (propose Propose) SizeHint() int { + return surge.SizeHint(propose.Height) + + surge.SizeHint(propose.Round) + + surge.SizeHint(propose.ValidRound) + + surge.SizeHint(propose.Value) + + surge.SizeHint(propose.From) + + surge.SizeHint(propose.Signature) +} + +// Marshal this message into binary. +func (propose Propose) Marshal(w io.Writer, m int) (int, error) { + m, err := surge.Marshal(w, propose.Height, m) + if err != nil { + return m, fmt.Errorf("marshaling height=%v: %v", propose.Height, err) } -} - -func (propose *Propose) Signatory() id.Signatory { - return propose.signatory -} - -func (propose *Propose) SigHash() id.Hash { - return sha256.Sum256([]byte(propose.String())) -} - -func (propose *Propose) Sig() id.Signature { - return propose.sig -} - -func (propose *Propose) Height() block.Height { - return propose.height -} - -func (propose *Propose) Round() block.Round { - return propose.round -} - -func (propose *Propose) BlockHash() id.Hash { - return propose.block.Hash() -} - -func (propose *Propose) Type() MessageType { - return ProposeMessageType -} - -func (propose *Propose) Block() block.Block { - return propose.block -} - -func (propose *Propose) ValidRound() block.Round { - return propose.validRound -} - -func (propose *Propose) String() string { - return fmt.Sprintf("Propose(Height=%v,Round=%v,BlockHash=%v,ValidRound=%v)", propose.Height(), propose.Round(), propose.BlockHash(), propose.ValidRound()) -} - -// Prevotes is a wrapper around the `[]Prevote` type. -type Prevotes []Prevote - -// Prevote for a block hash. -type Prevote struct { - signatory id.Signatory - sig id.Signature - height block.Height - round block.Round - blockHash id.Hash - nilReasons NilReasons -} - -func NewPrevote(height block.Height, round block.Round, blockHash id.Hash, nilReasons NilReasons) *Prevote { - return &Prevote{ - height: height, - round: round, - blockHash: blockHash, - nilReasons: nilReasons, + m, err = surge.Marshal(w, propose.Round, m) + if err != nil { + return m, fmt.Errorf("marshaling round=%v: %v", propose.Round, err) } -} - -func (prevote *Prevote) Signatory() id.Signatory { - return prevote.signatory -} - -func (prevote *Prevote) SigHash() id.Hash { - return sha256.Sum256([]byte(prevote.String())) -} - -func (prevote *Prevote) Sig() id.Signature { - return prevote.sig -} - -func (prevote *Prevote) Height() block.Height { - return prevote.height -} - -func (prevote *Prevote) Round() block.Round { - return prevote.round -} - -func (prevote *Prevote) BlockHash() id.Hash { - return prevote.blockHash -} - -func (prevote *Prevote) NilReasons() NilReasons { - return prevote.nilReasons -} - -func (prevote *Prevote) Type() MessageType { - return PrevoteMessageType -} - -func (prevote *Prevote) String() string { - nilReasonsBytes, err := surge.ToBinary(prevote.NilReasons()) + m, err = surge.Marshal(w, propose.ValidRound, m) if err != nil { - return fmt.Sprintf("Prevote(Height=%v,Round=%v,BlockHash=%v)", prevote.Height(), prevote.Round(), prevote.BlockHash()) + return m, fmt.Errorf("marshaling valid round=%v: %v", propose.ValidRound, err) } - nilReasonsHash := id.Hash(sha256.Sum256(nilReasonsBytes)) - return fmt.Sprintf("Prevote(Height=%v,Round=%v,BlockHash=%v,NilReasons=%v)", prevote.Height(), prevote.Round(), prevote.BlockHash(), nilReasonsHash.String()) -} - -// Precommits is a wrapper around the `[]Precommit` type. -type Precommits []Precommit - -// Precommit a block hash. -type Precommit struct { - signatory id.Signatory - sig id.Signature - height block.Height - round block.Round - blockHash id.Hash -} - -func NewPrecommit(height block.Height, round block.Round, blockHash id.Hash) *Precommit { - return &Precommit{ - height: height, - round: round, - blockHash: blockHash, + m, err = surge.Marshal(w, propose.Value, m) + if err != nil { + return m, fmt.Errorf("marshaling value=%v: %v", propose.Value, err) } -} - -func (precommit *Precommit) Signatory() id.Signatory { - return precommit.signatory -} - -func (precommit *Precommit) SigHash() id.Hash { - return sha256.Sum256([]byte(precommit.String())) -} - -func (precommit *Precommit) Sig() id.Signature { - return precommit.sig -} - -func (precommit *Precommit) Height() block.Height { - return precommit.height -} - -func (precommit *Precommit) Round() block.Round { - return precommit.round -} - -func (precommit *Precommit) BlockHash() id.Hash { - return precommit.blockHash -} - -func (precommit *Precommit) Type() MessageType { - return PrecommitMessageType -} - -func (precommit *Precommit) String() string { - return fmt.Sprintf("Precommit(Height=%v,Round=%v,BlockHash=%v)", precommit.Height(), precommit.Round(), precommit.BlockHash()) -} - -// Resyncs is a wrapper around the `[]Resync` type. -type Resyncs []Resync - -// Resync previous messages. -type Resync struct { - signatory id.Signatory - sig id.Signature - height block.Height - round block.Round - timestamp block.Timestamp -} - -func NewResync(height block.Height, round block.Round) *Resync { - return &Resync{ - height: height, - round: round, - timestamp: block.Timestamp(time.Now().Unix()), + m, err = surge.Marshal(w, propose.From, m) + if err != nil { + return m, fmt.Errorf("marshaling from=%v: %v", propose.From, err) } + m, err = surge.Marshal(w, propose.Signature, m) + if err != nil { + return m, fmt.Errorf("marshaling signature=%v: %v", propose.Signature, err) + } + return m, nil } -func (resync *Resync) Signatory() id.Signatory { - return resync.signatory -} - -func (resync *Resync) SigHash() id.Hash { - return sha256.Sum256([]byte(resync.String())) -} - -func (resync *Resync) Sig() id.Signature { - return resync.sig -} - -func (resync *Resync) Height() block.Height { - return resync.height -} - -func (resync *Resync) Round() block.Round { - return resync.round +// Unmarshal binary into this message. +func (propose *Propose) Unmarshal(r io.Reader, m int) (int, error) { + m, err := surge.Unmarshal(r, &propose.Height, m) + if err != nil { + return m, fmt.Errorf("unmarshaling height: %v", err) + } + m, err = surge.Unmarshal(r, &propose.Round, m) + if err != nil { + return m, fmt.Errorf("unmarshaling round: %v", err) + } + m, err = surge.Unmarshal(r, &propose.ValidRound, m) + if err != nil { + return m, fmt.Errorf("unmarshaling valid round: %v", err) + } + m, err = surge.Unmarshal(r, &propose.Value, m) + if err != nil { + return m, fmt.Errorf("unmarshaling value: %v", err) + } + m, err = surge.Unmarshal(r, &propose.From, m) + if err != nil { + return m, fmt.Errorf("unmarshaling from: %v", err) + } + m, err = surge.Unmarshal(r, &propose.Signature, m) + if err != nil { + return m, fmt.Errorf("unmarshaling signature: %v", err) + } + return m, nil } -func (resync *Resync) Timestamp() block.Timestamp { - return resync.timestamp +// A Prevote is sent by every correct Process at most once per Round. It is the +// first step of reaching consensus. Informally, if a correct Process receives +// 2F+1 Precommits for a Value, then it will Precommit to that Value. However, +// there are many other conditions which can cause a Process to Prevote. See the +// Process for more information. +type Prevote struct { + Height Height `json:"height"` + Round Round `json:"round"` + Value Value `json:"value"` + From id.Signatory `json:"from"` + Signature id.Signature `json:"signature"` } -func (resync *Resync) BlockHash() id.Hash { - panic(ErrBlockHashNotProvided) +func NewPrevoteHash(height Height, round Round, value Value) (id.Hash, error) { + buf := new(bytes.Buffer) + buf.Grow(surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(value)) + return NewPrevoteHashWithBuffer(height, round, value, buf) } -func (resync *Resync) Type() MessageType { - return ResyncMessageType +func NewPrevoteHashWithBuffer(height Height, round Round, value Value, buf *bytes.Buffer) (id.Hash, error) { + m, err := surge.Marshal(buf, height, surge.MaxBytes) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) + } + m, err = surge.Marshal(buf, round, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) + } + m, err = surge.Marshal(buf, value, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) + } + return id.NewHash(buf.Bytes()), nil } -func (resync *Resync) String() string { - return fmt.Sprintf("Resync(Height=%v,Round=%v,Timestamp=%v)", resync.Height(), resync.Round(), resync.Timestamp()) +// Equal compares two Prevotes. If they are equal, then it return true, +// otherwise it returns false. The signatures are not checked for equality, +// because signatures include randomness. +func (prevote Prevote) Equal(other *Prevote) bool { + return prevote.Height == other.Height && + prevote.Round == other.Round && + prevote.Value.Equal(&other.Value) && + prevote.From.Equal(&other.From) } -// An Inbox is storage container for one type message. Any type of message can -// be stored, but an attempt to store messages of different types in one inbox -// will cause a panic. Inboxes are used extensively by the consensus algorithm -// to track how many messages (of particular types) have been received, and -// under what conditions. For example, inboxes are used to track when `2F+1` -// prevote messages have been received for a specific block hash for the first -// time. -type Inbox struct { - f int - messages map[block.Height]map[block.Round]map[id.Signatory]Message - messageType MessageType +// SizeHint returns the number of bytes required to represent this message in +// binary. +func (prevote Prevote) SizeHint() int { + return surge.SizeHint(prevote.Height) + + surge.SizeHint(prevote.Round) + + surge.SizeHint(prevote.Value) + + surge.SizeHint(prevote.From) + + surge.SizeHint(prevote.Signature) } -// NewInbox returns an inbox for one type of message. It assumes at most `F` -// adversaries are present. -func NewInbox(f int, messageType MessageType) *Inbox { - if f <= 0 { - panic(fmt.Sprintf("invariant violation: f = %v needs to be a positive number", f)) +// Marshal this message into binary. +func (prevote Prevote) Marshal(w io.Writer, m int) (int, error) { + m, err := surge.Marshal(w, prevote.Height, m) + if err != nil { + return m, fmt.Errorf("marshaling height=%v: %v", prevote.Height, err) } - if messageType == NilMessageType { - panic("invariant violation: message type cannot be nil") + m, err = surge.Marshal(w, prevote.Round, m) + if err != nil { + return m, fmt.Errorf("marshaling round=%v: %v", prevote.Round, err) + } + m, err = surge.Marshal(w, prevote.Value, m) + if err != nil { + return m, fmt.Errorf("marshaling value=%v: %v", prevote.Value, err) } - return &Inbox{ - f: f, - messages: map[block.Height]map[block.Round]map[id.Signatory]Message{}, - messageType: messageType, + m, err = surge.Marshal(w, prevote.From, m) + if err != nil { + return m, fmt.Errorf("marshaling from=%v: %v", prevote.From, err) + } + m, err = surge.Marshal(w, prevote.Signature, m) + if err != nil { + return m, fmt.Errorf("marshaling signature=%v: %v", prevote.Signature, err) } + return m, nil } -// Insert a message into the inbox. It returns: -// -// - `n` the number of unique messages at the height and round of the inserted -// message (not necessarily for the same block hash), -// - `firstTime` whether, or not, this is the first time this message has been -// seen, -// - `firstTimeExceedingF` whether, or not, `n` has exceeded `F` for the first -// time as a result of this message being inserted, -// - `firstTimeExceeding2F` whether, or not, `n` has exceeded `2F` for the first -// time as a result of this message being inserted, and -// - `firstTimeExceeding2FOnBlockHash` whether, or not, this is the first -// time that more than `2F` unique messages have been seen for the same block. -// -// This method is used extensively for tracking the different conditions under -// which the state machine is allowed to transition between various states. Its -// correctness is fundamental to the correctness of the overall implementation. -// -// The last return value is any conflicting message that already existed before -// the Insert method was called. See the Catcher for more information. -func (inbox *Inbox) Insert(message Message) (n int, firstTime, firstTimeExceedingF, firstTimeExceeding2F, firstTimeExceeding2FOnBlockHash bool, conflicting Message) { - if message.Type() != inbox.messageType { - panic(fmt.Sprintf("pre-condition violation: expected type %v, got type %T", inbox.messageType, message)) +// Unmarshal binary into this message. +func (prevote *Prevote) Unmarshal(r io.Reader, m int) (int, error) { + m, err := surge.Unmarshal(r, &prevote.Height, m) + if err != nil { + return m, fmt.Errorf("unmarshaling height: %v", err) } - - height, round, signatory := message.Height(), message.Round(), message.Signatory() - if _, ok := inbox.messages[height]; !ok { - inbox.messages[height] = map[block.Round]map[id.Signatory]Message{} + m, err = surge.Unmarshal(r, &prevote.Round, m) + if err != nil { + return m, fmt.Errorf("unmarshaling round: %v", err) } - if _, ok := inbox.messages[height][round]; !ok { - inbox.messages[height][round] = map[id.Signatory]Message{} + m, err = surge.Unmarshal(r, &prevote.Value, m) + if err != nil { + return m, fmt.Errorf("unmarshaling value: %v", err) } - - previousN := len(inbox.messages[height][round]) - conflicting, ok := inbox.messages[height][round][signatory] - if ok { - // We do not override existing messages. This means that it is - // impossible for N to change, and thus for any "first time" triggers to - // become true. - if conflicting.SigHash().Equal(message.SigHash()) { - // Messages are exact duplicates of each other. - return previousN, false, false, false, false, nil - } - // Messages are in conflict. - return previousN, false, false, false, false, conflicting + m, err = surge.Unmarshal(r, &prevote.From, m) + if err != nil { + return m, fmt.Errorf("unmarshaling from: %v", err) } - - inbox.messages[height][round][signatory] = message - - n = len(inbox.messages[height][round]) - nOnBlockHash := 0 - if !ok { - nOnBlockHash = inbox.QueryByHeightRoundBlockHash(height, round, message.BlockHash()) + m, err = surge.Unmarshal(r, &prevote.Signature, m) + if err != nil { + return m, fmt.Errorf("unmarshaling signature: %v", err) } + return m, nil +} - firstTime = (previousN == 0) && (n == 1) - firstTimeExceedingF = (previousN == inbox.F()) && (n == inbox.F()+1) - firstTimeExceeding2F = (previousN == 2*inbox.F()) && (n == 2*inbox.F()+1) - firstTimeExceeding2FOnBlockHash = !ok && (nOnBlockHash == 2*inbox.F()+1) - conflicting = nil - return +// A Precommit is sent by every correct Process at most once per Round. It is +// the second step of reaching consensus. Informally, if a correct Process +// receives 2F+1 Precommits for a Value, then it will commit to that Value and +// progress to the next Height. However, there are many other conditions which +// can cause a Process to Precommit. See the Process for more information. +type Precommit struct { + Height Height `json:"height"` + Round Round `json:"round"` + Value Value `json:"value"` + From id.Signatory `json:"from"` + Signature id.Signature `json:"signature"` } -// Delete removes all messages at a given height. -func (inbox *Inbox) Delete(height block.Height) { - delete(inbox.messages, height) +func NewPrecommitHash(height Height, round Round, value Value) (id.Hash, error) { + buf := new(bytes.Buffer) + buf.Grow(surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(value)) + return NewPrecommitHashWithBuffer(height, round, value, buf) } -// QueryMessagesByHeightRound returns all unique messages that have been -// received at the specified height and round. The specific block hash of the -// messages are ignored and might be different from each other. -func (inbox *Inbox) QueryMessagesByHeightRound(height block.Height, round block.Round) []Message { - if _, ok := inbox.messages[height]; !ok { - return nil +func NewPrecommitHashWithBuffer(height Height, round Round, value Value, buf *bytes.Buffer) (id.Hash, error) { + m, err := surge.Marshal(buf, height, surge.MaxBytes) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) } - if _, ok := inbox.messages[height][round]; !ok { - return nil + m, err = surge.Marshal(buf, round, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) } - messages := make([]Message, 0, len(inbox.messages[height][round])) - for _, message := range inbox.messages[height][round] { - messages = append(messages, message) + m, err = surge.Marshal(buf, value, m) + if err != nil { + return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) } - return messages + return id.NewHash(buf.Bytes()), nil } -// QueryMessagesByHeightWithHighestRound returns all unique messages that have -// been received at the specified height and at the heighest round observed (for -// the specified height). Only rounds with >2F messages are considered. The -// specific block hash of the messages are ignored and might be different from -// each other. -func (inbox *Inbox) QueryMessagesByHeightWithHighestRound(height block.Height) []Message { - if _, ok := inbox.messages[height]; !ok { - return nil - } - highestRound := block.Round(-1) - for round := range inbox.messages[height] { - if len(inbox.messages[height][round]) > 2*inbox.f { - if round > highestRound { - highestRound = round - } - } - } - if highestRound == -1 { - return nil - } - if _, ok := inbox.messages[height][highestRound]; !ok { - return nil - } - messages := make([]Message, 0, len(inbox.messages[height][highestRound])) - for _, message := range inbox.messages[height][highestRound] { - messages = append(messages, message) - } - return messages +// Equal compares two Precommits. If they are equal, then it return true, +// otherwise it returns false. The signatures are not checked for equality, +// because signatures include randomness. +func (precommit Precommit) Equal(other *Precommit) bool { + return precommit.Height == other.Height && + precommit.Round == other.Round && + precommit.Value.Equal(&other.Value) && + precommit.From.Equal(&other.From) } -// QueryByHeightRoundBlockHash returns the number of unique messages that have -// been received at the specified height and round. Only messages that reference -// the specified block hash are considered. -func (inbox *Inbox) QueryByHeightRoundBlockHash(height block.Height, round block.Round, blockHash id.Hash) (n int) { - if _, ok := inbox.messages[height]; !ok { - return - } - if _, ok := inbox.messages[height][round]; !ok { - return - } - for _, message := range inbox.messages[height][round] { - if blockHash.Equal(message.BlockHash()) { - n++ - } - } - return +// SizeHint returns the number of bytes required to represent this message in +// binary. +func (precommit Precommit) SizeHint() int { + return surge.SizeHint(precommit.Height) + + surge.SizeHint(precommit.Round) + + surge.SizeHint(precommit.Value) + + surge.SizeHint(precommit.From) + + surge.SizeHint(precommit.Signature) } -// QueryByHeightRoundSignatory the message (or nil) sent by a specific signatory -// at a specific height and round. -func (inbox *Inbox) QueryByHeightRoundSignatory(height block.Height, round block.Round, sig id.Signatory) Message { - if _, ok := inbox.messages[height]; !ok { - return nil +// Marshal this message into binary. +func (precommit Precommit) Marshal(w io.Writer, m int) (int, error) { + m, err := surge.Marshal(w, precommit.Height, m) + if err != nil { + return m, fmt.Errorf("marshaling height=%v: %v", precommit.Height, err) } - if _, ok := inbox.messages[height][round]; !ok { - return nil + m, err = surge.Marshal(w, precommit.Round, m) + if err != nil { + return m, fmt.Errorf("marshaling round=%v: %v", precommit.Round, err) } - return inbox.messages[height][round][sig] -} - -// QueryByHeightRound returns the number of unique messages that have been -// received at the specified height and round. The specific block hash of the -// messages are ignored and might be different from each other. -func (inbox *Inbox) QueryByHeightRound(height block.Height, round block.Round) (n int) { - if _, ok := inbox.messages[height]; !ok { - return - } - if _, ok := inbox.messages[height][round]; !ok { - return - } - n = len(inbox.messages[height][round]) - return -} - -// Reset the inbox to a specific height. All messages for height lower than the -// specified height are dropped. This is necessary to ensure that, over time, -// the storage space of the inbox is bounded. -func (inbox *Inbox) Reset(height block.Height) { - for blockHeight := range inbox.messages { - if blockHeight < height { - delete(inbox.messages, blockHeight) - } + m, err = surge.Marshal(w, precommit.Value, m) + if err != nil { + return m, fmt.Errorf("marshaling value=%v: %v", precommit.Value, err) } + m, err = surge.Marshal(w, precommit.From, m) + if err != nil { + return m, fmt.Errorf("marshaling from=%v: %v", precommit.From, err) + } + m, err = surge.Marshal(w, precommit.Signature, m) + if err != nil { + return m, fmt.Errorf("marshaling signature=%v: %v", precommit.Signature, err) + } + return m, nil } -func (inbox *Inbox) F() int { - return inbox.f -} - -func (inbox *Inbox) MessageType() MessageType { - return inbox.messageType +// Unmarshal binary into this message. +func (precommit *Precommit) Unmarshal(r io.Reader, m int) (int, error) { + m, err := surge.Unmarshal(r, &precommit.Height, m) + if err != nil { + return m, fmt.Errorf("unmarshaling height: %v", err) + } + m, err = surge.Unmarshal(r, &precommit.Round, m) + if err != nil { + return m, fmt.Errorf("unmarshaling round: %v", err) + } + m, err = surge.Unmarshal(r, &precommit.Value, m) + if err != nil { + return m, fmt.Errorf("unmarshaling value: %v", err) + } + m, err = surge.Unmarshal(r, &precommit.From, m) + if err != nil { + return m, fmt.Errorf("unmarshaling from: %v", err) + } + m, err = surge.Unmarshal(r, &precommit.Signature, m) + if err != nil { + return m, fmt.Errorf("unmarshaling signature: %v", err) + } + return m, nil } diff --git a/process/message_test.go b/process/message_test.go index 1b542484..5a8536ab 100644 --- a/process/message_test.go +++ b/process/message_test.go @@ -1,683 +1 @@ -package process_test - -import ( - "crypto/ecdsa" - cRand "crypto/rand" - "math/rand" - "reflect" - "testing/quick" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/id" - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/process" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Messages", func() { - - Context("Propose", func() { - Context("when initializing", func() { - It("should return a message with fields equal to those passed during creation", func() { - test := func() bool { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - validRound := block.Round(rand.Int63()) - block := RandomBlock(RandomBlockKind()) - - propose := NewPropose(height, round, block, validRound) - - Expect(propose.Type()).Should(Equal(MessageType(ProposeMessageType))) - Expect(propose.Height()).Should(Equal(height)) - Expect(propose.Round()).Should(Equal(round)) - Expect(propose.ValidRound()).Should(Equal(validRound)) - Expect(propose.Block().Equal(block)).Should(BeTrue()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when stringifying", func() { - It("should return equal strings", func() { - test := func() bool { - msg := RandomPropose() - newMsg := msg - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - Context("when unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - msg1, msg2 := RandomPropose(), RandomPropose() - return msg1.String() != msg2.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling random", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - msg := RandomPropose() - data, err := surge.ToBinary(msg) - Expect(err).NotTo(HaveOccurred()) - - var newMsg Propose - Expect(surge.FromBinary(data, &newMsg)).Should(Succeed()) - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when signing and verifying", func() { - It("should verify if a message if has been signed properly", func() { - test := func() bool { - propose := RandomPropose() - Expect(Verify(propose)).ShouldNot(Succeed()) - - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - Expect(err).NotTo(HaveOccurred()) - Expect(Sign(propose, *privateKey)).Should(Succeed()) - Expect(Verify(propose)).Should(Succeed()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("Prevote", func() { - Context("when initializing", func() { - It("should return a message with fields equal to those passed during creation", func() { - test := func() bool { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - blockHash := RandomHash() - - prevote := NewPrevote(height, round, blockHash, nil) - - Expect(prevote.Type()).Should(Equal(MessageType(PrevoteMessageType))) - Expect(prevote.Height()).Should(Equal(height)) - Expect(prevote.Round()).Should(Equal(round)) - Expect(prevote.BlockHash().Equal(blockHash)).Should(BeTrue()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when stringifying", func() { - It("should return equal strings", func() { - test := func() bool { - msg := RandomPrevote() - newMsg := msg - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - Context("when unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - msg1, msg2 := RandomPrevote(), RandomPrevote() - return msg1.String() != msg2.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling random", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - msg := RandomPrevote() - data, err := surge.ToBinary(msg) - Expect(err).NotTo(HaveOccurred()) - - var newMsg Prevote - Expect(surge.FromBinary(data, &newMsg)).Should(Succeed()) - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when signing and verifying", func() { - It("should verify if a message if has been signed properly", func() { - test := func() bool { - prevote := RandomPrevote() - Expect(Verify(prevote)).ShouldNot(Succeed()) - - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - Expect(err).NotTo(HaveOccurred()) - Expect(Sign(prevote, *privateKey)).Should(Succeed()) - Expect(Verify(prevote)).Should(Succeed()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("Precommit", func() { - Context("when initializing", func() { - It("should return a message with fields equal to those passed during creation", func() { - test := func() bool { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - blockHash := RandomHash() - - precommit := NewPrecommit(height, round, blockHash) - - Expect(precommit.Type()).Should(Equal(MessageType(PrecommitMessageType))) - Expect(precommit.Height()).Should(Equal(height)) - Expect(precommit.Round()).Should(Equal(round)) - Expect(precommit.BlockHash().Equal(blockHash)).Should(BeTrue()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when stringifying", func() { - It("should return equal strings", func() { - test := func() bool { - msg := RandomPrecommit() - newMsg := msg - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - Context("when unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - msg1, msg2 := RandomPrecommit(), RandomPrecommit() - return msg1.String() != msg2.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling random", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - msg := RandomPrecommit() - data, err := surge.ToBinary(msg) - Expect(err).NotTo(HaveOccurred()) - - var newMsg Precommit - Expect(surge.FromBinary(data, &newMsg)).Should(Succeed()) - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when signing and verifying", func() { - It("should verify if a message if has been signed properly", func() { - test := func() bool { - precommit := RandomPrecommit() - Expect(Verify(precommit)).ShouldNot(Succeed()) - - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - Expect(err).NotTo(HaveOccurred()) - Expect(Sign(precommit, *privateKey)).Should(Succeed()) - Expect(Verify(precommit)).Should(Succeed()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("Resync", func() { - Context("when initializing", func() { - It("should return a message with fields equal to those passed during creation", func() { - test := func() bool { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - - resync := NewResync(height, round) - - Expect(resync.Type()).Should(Equal(MessageType(ResyncMessageType))) - Expect(resync.Height()).Should(Equal(height)) - Expect(resync.Round()).Should(Equal(round)) - Expect(func() { - resync.BlockHash() - }).Should(Panic()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when stringifying", func() { - It("should return equal strings", func() { - test := func() bool { - msg := RandomResync() - newMsg := msg - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - Context("when unequal", func() { - It("should return unequal strings", func() { - test := func() bool { - msg1, msg2 := RandomResync(), RandomResync() - return msg1.String() != msg2.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when marshaling random", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - msg := RandomResync() - data, err := surge.ToBinary(msg) - Expect(err).NotTo(HaveOccurred()) - - var newMsg Resync - Expect(surge.FromBinary(data, &newMsg)).Should(Succeed()) - return msg.String() == newMsg.String() - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when signing and verifying", func() { - It("should verify if a message if has been signed properly", func() { - test := func() bool { - resync := RandomResync() - Expect(Verify(resync)).ShouldNot(Succeed()) - - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - Expect(err).NotTo(HaveOccurred()) - Expect(Sign(resync, *privateKey)).Should(Succeed()) - Expect(Verify(resync)).Should(Succeed()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when initializing a new inbox", func() { - It("should have the given f and message type", func() { - test := func() bool { - messageType := RandomMessageType(false) - f := rand.Int() + 1 - inbox := RandomInbox(f, messageType) - Expect(inbox.F()).Should(Equal(f)) - Expect(messageType).Should(Equal(inbox.MessageType())) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should panic when passing a invalid f", func() { - test := func() bool { - messageType := RandomMessageType(false) - - // Should panic when passing 0 - Expect(func() { - _ = RandomInbox(0, messageType) - }).Should(Panic()) - - // Should panic when passing negative number - Expect(func() { - _ = RandomInbox(-1*rand.Int(), messageType) - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should panic when passing a nil message type", func() { - test := func() bool { - f := rand.Int() + 1 - Expect(func() { - _ = RandomInbox(f, NilMessageType) - }).Should(Panic()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when marshaling a random inbox", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - test := func() bool { - messageType := RandomMessageType(false) - f := rand.Int() + 1 - inbox := RandomInbox(f, messageType) - Expect(inbox.F()).Should(Equal(f)) - data, err := surge.ToBinary(inbox) - Expect(err).NotTo(HaveOccurred()) - - newInbox := NewInbox(1, messageType) - Expect(surge.FromBinary(data, newInbox)).Should(Succeed()) - Expect(inbox.F()).To(Equal(newInbox.F())) - Expect(inbox.MessageType()).To(Equal(newInbox.MessageType())) - - return reflect.DeepEqual(inbox, newInbox) - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when inserting messages into an inbox", func() { - Context("when we first insert message to an inbox", func() { - It("should return n=1, firstTime=true, firstTimeExceedingF=false, and firstTimeExceeding2F=false", func() { - test := func() bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - n, firstTime, firstTimeExceedingF, firstTimeExceeding2F, _, _ := inbox.Insert(RandomMessage(messageType)) - Expect(n).Should(Equal(1)) - Expect(firstTime).Should(BeTrue()) - Expect(firstTimeExceedingF).Should(BeFalse()) - Expect(firstTimeExceeding2F).Should(BeFalse()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when we insert an equal duplicate", func() { - It("should return conflicting=nil", func() { - test := func() bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - message := RandomMessage(messageType) - inbox := NewInbox(f, messageType) - _, _, _, _, _, conflicting := inbox.Insert(message) - Expect(conflicting).To(BeNil()) - _, _, _, _, _, conflicting = inbox.Insert(message) - Expect(conflicting).To(BeNil()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when we insert a non-equal duplicate", func() { - It("should return conflicting=non-nil", func() { - test := func() bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - fst := RandomMessage(messageType) - snd := RandomMessageWithHeightAndRound(fst.Height(), fst.Round(), messageType) - inbox := NewInbox(f, messageType) - _, _, _, _, _, conflicting := inbox.Insert(fst) - Expect(conflicting).To(BeNil()) - _, _, _, _, _, conflicting = inbox.Insert(snd) - Expect(conflicting).ToNot(BeNil()) - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when F + 1 messages are inserted", func() { - It("should return n=F+1, firstTime=false, firstTimeExceedingF=true, and firstTimeExceeding2F=false", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - // Expect n, false, false, false when inserting no more than F messages - for i := 1; i <= f; i++ { - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, firstTime, firstTimeExceedingF, firstTimeExceeding2F, _, _ := inbox.Insert(msg) - Expect(n).Should(Equal(i)) - if i == 1 { - Expect(firstTime).Should(BeTrue()) - } else { - Expect(firstTime).Should(BeFalse()) - } - Expect(firstTimeExceedingF).Should(BeFalse()) - Expect(firstTimeExceeding2F).Should(BeFalse()) - } - - // Expect F+1, false, true, false when inserting F+1 message - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, firstTime, firstTimeExceedingF, firstTimeExceeding2F, _, _ := inbox.Insert(msg) - - Expect(n).Should(Equal(f + 1)) - Expect(firstTime).Should(BeFalse()) - Expect(firstTimeExceedingF).Should(BeTrue()) - Expect(firstTimeExceeding2F).Should(BeFalse()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when 2F + 1 messages are inserted", func() { - It("should return n=2F+1, firstTime=false, firstTimeExceedingF=false, and firstTimeExceeding2F=true", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - // Expect n, false, false,false when inserting no more than F messages - for i := 1; i <= 2*f; i++ { - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, _, _, firstTimeExceeding2F, _, _ := inbox.Insert(msg) - Expect(n).Should(Equal(i)) - Expect(firstTimeExceeding2F).Should(BeFalse()) - } - - // Expect 2F+1, false, true, false when inserting F+1 message - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, firstTime, firstTimeExceedingF, firstTimeExceeding2F, _, _ := inbox.Insert(msg) - - Expect(n).Should(Equal(2*f + 1)) - Expect(firstTime).Should(BeFalse()) - Expect(firstTimeExceedingF).Should(BeFalse()) - Expect(firstTimeExceeding2F).Should(BeTrue()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("after 2F + 1 messages are inserted", func() { - It("should return n=i, firstTime=false, firstTimeExceedingF=false, and firstTimeExceeding2F=false", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - // Expect n, false, false,false when inserting no more than F messages - for i := 1; i <= 2*f+1; i++ { - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, _, _, _, _, _ := inbox.Insert(msg) - Expect(n).Should(Equal(i)) - } - - // Expect 3F+1, false, true, false when inserting F+1 message - for i := 1; i < rand.Intn(100); i++ { - msg := RandomSingedMessageWithHeightAndRound(height, round, messageType) - n, firstTime, firstTimeExceedingF, firstTimeExceeding2F, _, _ := inbox.Insert(msg) - - Expect(n).Should(Equal(2*f + 1 + i)) - Expect(firstTime).Should(BeFalse()) - Expect(firstTimeExceedingF).Should(BeFalse()) - Expect(firstTimeExceeding2F).Should(BeFalse()) - } - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when querying by height, round and block hash", func() { - It("should return the number of votes", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - source := map[block.Height]map[block.Round]map[id.Hash]int{} - noMessages := rand.Intn(100) - for i := 0; i < noMessages; i++ { - msg := RandomSignedMessage(messageType) - - // Inserting the same msg twice should not affect anything - _, _, _, _, _, _ = inbox.Insert(msg) - _, _, _, _, _, _ = inbox.Insert(msg) - - if _, ok := source[msg.Height()]; !ok { - source[msg.Height()] = map[block.Round]map[id.Hash]int{} - } - if _, ok := source[msg.Height()][msg.Round()]; !ok { - source[msg.Height()][msg.Round()] = map[id.Hash]int{} - } - source[msg.Height()][msg.Round()][msg.BlockHash()]++ - } - - // Expect the query function gives us the same result as the source. - for height, roundMap := range source { - for round, hashMap := range roundMap { - for hash, num := range hashMap { - Expect(inbox.QueryByHeightRoundBlockHash(height, round, hash)).Should(Equal(num)) - } - } - } - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when querying by height, round and signatory", func() { - It("should return the message if exist", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - noMessages := rand.Intn(100) - for i := 0; i < noMessages; i++ { - msg := RandomSignedMessage(messageType) - - // It should return nil before inserting into the inbox. - nilMessage := inbox.QueryByHeightRoundSignatory(msg.Height(), msg.Round(), msg.Signatory()) - Expect(nilMessage).Should(BeNil()) - - // Inserting the same msg twice should not affect anything - _, _, _, _, _, _ = inbox.Insert(msg) - _, _, _, _, _, _ = inbox.Insert(msg) - - // It return the same message we inserted - storedMsg := inbox.QueryByHeightRoundSignatory(msg.Height(), msg.Round(), msg.Signatory()) - Expect(reflect.DeepEqual(msg, storedMsg)).Should(BeTrue()) - } - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when querying by height, round ", func() { - It("should return correct number of message of that round", func() { - test := func(height block.Height, round block.Round) bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - - source := map[block.Height]map[block.Round]int{} - noMessages := rand.Intn(100) - for i := 0; i < noMessages; i++ { - msg := RandomSignedMessage(messageType) - - // Inserting the same msg twice should not affect anything - _, _, _, _, _, _ = inbox.Insert(msg) - _, _, _, _, _, _ = inbox.Insert(msg) - - if _, ok := source[msg.Height()]; !ok { - source[msg.Height()] = map[block.Round]int{} - } - source[msg.Height()][msg.Round()]++ - } - // Expect the query function gives us the same result as the source. - for height, roundMap := range source { - for round, num := range roundMap { - Expect(inbox.QueryByHeightRound(height, round)).Should(Equal(num)) - } - } - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when deleting messages from an inbox", func() { - It("should return correct number of messages", func() { - test := func() bool { - f := rand.Intn(100) + 1 - messageType := RandomMessageType(false) - inbox := NewInbox(f, messageType) - message := RandomMessage(messageType) - - inbox.Insert(message) - Expect(inbox.QueryByHeightRound(message.Height(), message.Round())).Should(Equal(1)) - - inbox.Insert(RandomMessage(messageType)) - Expect(inbox.QueryByHeightRound(message.Height(), message.Round())).Should(Equal(1)) - - inbox.Insert(RandomMessage(messageType)) - Expect(inbox.QueryByHeightRound(message.Height(), message.Round())).Should(Equal(1)) - - inbox.Delete(message.Height()) - Expect(inbox.QueryByHeightRound(message.Height(), message.Round())).Should(Equal(0)) - - inbox.Delete(message.Height()) - Expect(inbox.QueryByHeightRound(message.Height(), message.Round())).Should(Equal(0)) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) -}) +package process_test \ No newline at end of file diff --git a/process/process.go b/process/process.go index 9cb6b7d9..f184c9e7 100644 --- a/process/process.go +++ b/process/process.go @@ -1,813 +1,825 @@ +// Package process implements the Byzantine fault tolerant consensus algorithm +// described by "The latest gossip of BFT consensus" (Buchman et al.), which can +// be found at https://arxiv.org/pdf/1807.04938.pdf. It makes extensive use of +// dependency injection, and concrete implementions must be careful to meet all +// of the requirements specified by the interface, otherwise the correctness of +// the consensus algorithm can be broken. package process import ( + "fmt" "io" - "sync" - "time" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/schedule" "github.com/renproject/id" "github.com/renproject/surge" - "github.com/sirupsen/logrus" ) -// Step in the consensus algorithm. -type Step uint8 - -// SizeHint of how many bytes will be needed to represent steps in -// binary. -func (Step) SizeHint() int { - return 1 -} - -// Marshal this step into binary. -func (step Step) Marshal(w io.Writer, m int) (int, error) { - return surge.Marshal(w, uint8(step), m) +// A Timer is used to schedule timeout events. +type Timer interface { + // TimeoutPropose is called when the Process needs its OnTimeoutPropose + // method called after a timeout. The timeout should be proportional to the + // Round. + TimeoutPropose(Height, Round) + // TimeoutPrevote is called when the Process needs its OnTimeoutPrevote + // method called after a timeout. The timeout should be proportional to the + // Round. + TimeoutPrevote(Height, Round) + // TimeoutPrecommit is called when the Process needs its OnTimeoutPrecommit + // method called after a timeout. The timeout should be proportional to the + // Round. + TimeoutPrecommit(Height, Round) +} + +// A Scheduler is used to determine which Process should be proposing a Vaue at +// the given Height and Round. A Scheduler must be derived solely from the +// Height, Round, and Values on which all correct Processes have already +// achieved consensus. +type Scheduler interface { + Schedule(height Height, round Round) id.Signatory +} + +// A Proposer is used to propose new Values for consensus. A Proposer must only +// ever return a valid Value, and once it returns a Value, it must never return +// a different Value for the same Height and Round. +type Proposer interface { + Propose(Height, Round) Value } -// Unmarshal into this step from binary. -func (step *Step) Unmarshal(r io.Reader, m int) (int, error) { - return surge.Unmarshal(r, (*uint8)(step), m) +// A Broadcaster is used to broadcast Propose, Prevote, and Precommit messages +// to all Processes in the consensus algorithm, including the Process that +// initiated the broadcast. It is assumed that all messages between correct +// Processes are eventually delivered, although no specific order is assumed. +// +// Once a Value has been broadcast as part of a Propose, Prevote, or Precommit +// message, different Values must not be broadcast for that same message type +// with the same Height and Round. The same restriction applies to valid Rounds +// broadcast with a Propose message. +type Broadcaster interface { + BroadcastPropose(Propose) + BroadcastPrevote(Prevote) + BroadcastPrecommit(Precommit) } -// Define all Steps. -const ( - StepNil Step = iota - StepPropose - StepPrevote - StepPrecommit -) - -// NilReasons can be used to provide contextual information alongside an error -// upon validating blocks. -type NilReasons map[string][]byte - -// A Blockchain defines a storage interface for Blocks that is based around -// Height. -type Blockchain interface { - InsertBlockAtHeight(block.Height, block.Block) - BlockAtHeight(block.Height) (block.Block, bool) - BlockExistsAtHeight(block.Height) bool - LatestBaseBlock() block.Block +// A Validator is used to validate a proposed Value. Processes are not required +// to agree on the validity of a Value. +type Validator interface { + Valid(Value) bool } -// A SaveRestorer defines a storage interface for the State. -type SaveRestorer interface { - Save(*State) - Restore(*State) +// A Committer is used to emit Values that are committed. The commitment of a +// new Value implies that all correct Processes agree on this Value at this +// Height, and will never revert. +type Committer interface { + Commit(Height, Value) } -// A Proposer builds a `block.Block` for proposals. -type Proposer interface { - BlockProposal(block.Height, block.Round) block.Block +// A Catcher is used to catch bad behaviour in other Processes. For example, +// when the same Process sends two different Proposes at the same Height and +// Round. +type Catcher interface { + CatchDoublePropose(Propose, Propose) + CatchDoublePrevote(Prevote, Prevote) + CatchDoublePrecommit(Precommit, Precommit) } -// A Validator validates a `block.Block` that has been proposed. +// A Process is a deterministic finite state automaton that communicates with +// other Processes to implement a Byzantine fault tolerant consensus algorithm. +// It is intended to be used as part of a larger component that implements a +// Byzantine fault tolerant replicated state machine. // -// When the Validator decides that a `block.Block` is invalid, it must provide -// `NilReasons` (that is, reasons explaining why the `block.Block` is invalid). -// This is expected to be used for debugging, but it can also be used to help -// the proposer "prune" invalid transactions from future `Propose` messages -// (when `DidReceiveSufficientNilPrevotes` triggers). This can be useful in -// fast-forwarding, because a proposer might have missed a `block.Block` and be -// attempting to `Propose` transactions that are already committed in that -// `block.Block`. +// All messages from previous and future Heights will be ignored. The component +// using the Process should buffer all messages from future Heights so that they +// are not lost. It is assumed that this component will also handle the +// authentication and rate-limiting of messages. // -// The `checkHistory` argument tells the Validator whether, or not, it must also -// check the parental history of the `block.Block` (that is, are all parent -// blocks present and known to be valid too). This is important, because when -// fast-forwarding, it is (by definition) not required to check all parental -// history. An implementor can "disable" fast-forwarding by also validating as -// if `checkHistory` was true. -type Validator interface { - IsBlockValid(block block.Block, checkHistory bool) (NilReasons, error) -} - -// An Observer is notified when note-worthy events happen for the first time. -type Observer interface { - DidCommitBlock(block.Height) - DidReceiveSufficientNilPrevotes(messages Messages, f int) +// Processes are not safe for concurrent use. All methods must be called by the +// same goroutine that allocates and starts the Process. +type Process struct { + // whoami represents the identity of this Process. It is assumed that the + // ECDSA private key required to prove ownership of this identity is known. + whoami id.Signatory + // f is the maximum number of malicious adversaries that the Process can + // withstand while still maintaining safety and liveliness. + f int + + // Input interface that provide data to the Process. + timer Timer + scheduler Scheduler + proposer Proposer + validator Validator + + // Output interfaces that received data from the Process. + broadcaster Broadcaster + committer Committer + catcher Catcher + + // State of the Process. + State `json:"state"` + // ProposeLogs store the Proposes for all Rounds. + ProposeLogs map[Round]Propose `json:"proposeLogs"` + // PrevoteLogs store the Prevotes for all Processes in all Rounds. + PrevoteLogs map[Round]map[id.Signatory]Prevote `json:"prevoteLogs"` + // PrecommitLogs store the Precommits for all Processes in all Rounds. + PrecommitLogs map[Round]map[id.Signatory]Precommit `json:"precommitLogs"` + // OnceFlags prevents events from happening more than once. + OnceFlags map[Round]OnceFlag `json:"onceFlags"` +} + +// New returns a new Process that is in the default State with empty message +// logs. +func New( + whoami id.Signatory, + f int, + timer Timer, + scheduler Scheduler, + proposer Proposer, + validator Validator, + broadcaster Broadcaster, + committer Committer, + catcher Catcher, +) Process { + return Process{ + whoami: whoami, + f: f, + + timer: timer, + scheduler: scheduler, + proposer: proposer, + validator: validator, + + broadcaster: broadcaster, + committer: committer, + catcher: catcher, + + State: DefaultState(), + ProposeLogs: make(map[Round]Propose), + PrevoteLogs: make(map[Round]map[id.Signatory]Prevote), + PrecommitLogs: make(map[Round]map[id.Signatory]Precommit), + OnceFlags: make(map[Round]OnceFlag), + } +} + +// SizeHint returns the number of bytes required to represent this Process in +// binary. +func (p Process) SizeHint() int { + return surge.SizeHint(p.State) + + surge.SizeHint(p.ProposeLogs) + + surge.SizeHint(p.PrevoteLogs) + + surge.SizeHint(p.PrecommitLogs) + + surge.SizeHint(p.OnceFlags) } -// A Broadcaster sends a Message to a either specific Process or as many -// Processes in the network as possible. -// -// For the consensus algorithm to work correctly, it is assumed that all honest -// processes will eventually deliver all messages to all other honest processes. -// The specific message ordering is not important. In practice, the Prevote -// messages are the only messages that must guarantee delivery when guaranteeing -// correctness. -type Broadcaster interface { - Broadcast(Message) - Cast(id.Signatory, Message) +// Marshal this Process into binary. +func (p Process) Marshal(w io.Writer, m int) (int, error) { + m, err := surge.Marshal(w, p.State, m) + if err != nil { + return m, fmt.Errorf("marshaling state: %v", err) + } + m, err = surge.Marshal(w, p.ProposeLogs, m) + if err != nil { + return m, fmt.Errorf("marshaling propose logs: %v", err) + } + m, err = surge.Marshal(w, p.PrevoteLogs, m) + if err != nil { + return m, fmt.Errorf("marshaling prevote logs: %v", err) + } + m, err = surge.Marshal(w, p.PrecommitLogs, m) + if err != nil { + return m, fmt.Errorf("marshaling precommit logs: %v", err) + } + m, err = surge.Marshal(w, p.OnceFlags, m) + if err != nil { + return m, fmt.Errorf("marshaling once flags: %v", err) + } + return m, nil } -// A Timer determines the timeout duration at a given Step and `block.Round`. -type Timer interface { - Timeout(step Step, round block.Round) time.Duration +// Unmarshal from binary into this Process. +func (p *Process) Unmarshal(r io.Reader, m int) (int, error) { + m, err := surge.Unmarshal(r, &p.State, m) + if err != nil { + return m, fmt.Errorf("unmarshaling state: %v", err) + } + m, err = surge.Unmarshal(r, &p.ProposeLogs, m) + if err != nil { + return m, fmt.Errorf("unmarshaling propose logs: %v", err) + } + m, err = surge.Unmarshal(r, &p.PrevoteLogs, m) + if err != nil { + return m, fmt.Errorf("unmarshaling prevote logs: %v", err) + } + m, err = surge.Unmarshal(r, &p.PrecommitLogs, m) + if err != nil { + return m, fmt.Errorf("unmarshaling precommit logs: %v", err) + } + m, err = surge.Unmarshal(r, &p.OnceFlags, m) + if err != nil { + return m, fmt.Errorf("unmarshaling once flags: %v", err) + } + return m, nil } -// Processes defines a wrapper type around the []Process type. -type Processes []Process - -// A Process defines a state machine in the distributed replicated state -// machine. See https://arxiv.org/pdf/1807.04938.pdf for more information. -type Process struct { - logger logrus.FieldLogger - mu *sync.Mutex - - signatory id.Signatory - blockchain Blockchain - state State +// Propose is used to notify the Process that a Propose message has been +// received (this includes Propose messages that the Process itself has +// broadcast). All conditions that could be opened by the receipt of a Propose +// message will be tried. +func (p *Process) Propose(propose Propose) { + if !p.insertPropose(propose) { + return + } - saveRestorer SaveRestorer - proposer Proposer - validator Validator - scheduler schedule.Scheduler - broadcaster Broadcaster - timer Timer - observer Observer - catcher Catcher + p.trySkipToFutureRound(propose.Round) + p.tryCommitUponSufficientPrecommits(propose.Round) + p.tryPrecommitUponSufficientPrevotes() + p.tryPrevoteUponPropose() + p.tryPrevoteUponSufficientPrevotes() } -// New Process initialised to the default state, starting in the first round. -func New(logger logrus.FieldLogger, signatory id.Signatory, blockchain Blockchain, state State, saveRestorer SaveRestorer, proposer Proposer, validator Validator, observer Observer, broadcaster Broadcaster, scheduler schedule.Scheduler, timer Timer, catcher Catcher) *Process { - p := &Process{ - logger: logger, - mu: new(sync.Mutex), - - signatory: signatory, - blockchain: blockchain, - state: state, - - saveRestorer: saveRestorer, - proposer: proposer, - validator: validator, - scheduler: scheduler, - broadcaster: broadcaster, - timer: timer, - observer: observer, - catcher: catcher, +// Prevote is used to notify the Process that a Prevote message has been +// received (this includes Prevote messages that the Process itself has +// broadcast). All conditions that could be opened by the receipt of a Prevote +// message will be tried. +func (p *Process) Prevote(prevote Prevote) { + if !p.insertPrevote(prevote) { + return } - return p -} - -// CurrentHeight of the Process. -func (p *Process) CurrentHeight() block.Height { - p.mu.Lock() - defer p.mu.Unlock() - return p.state.CurrentHeight -} -// Save the current state of the process using the saveRestorer. -func (p *Process) Save() { - p.mu.Lock() - defer p.mu.Unlock() - p.saveRestorer.Save(&p.state) + p.trySkipToFutureRound(prevote.Round) + p.tryPrecommitUponSufficientPrevotes() + p.tryPrecommitNilUponSufficientPrevotes() + p.tryPrevoteUponSufficientPrevotes() + p.tryTimeoutPrevoteUponSufficientPrevotes() } -// Restore the current state of the process using the saveRestorer. -func (p *Process) Restore() { - p.mu.Lock() - defer p.mu.Unlock() - p.saveRestorer.Restore(&p.state) -} +// Precommit is used to notify the Process that a Precommit message has been +// received (this includes Precommit messages that the Process itself has +// broadcast). All conditions that could be opened by the receipt of a Precommit +// message will be tried. +func (p *Process) Precommit(precommit Precommit) { + if !p.insertPrecommit(precommit) { + return + } -// SizeHint returns the number of bytes required to store this process in -// binary. -func (p *Process) SizeHint() int { - p.mu.Lock() - defer p.mu.Unlock() - return p.state.SizeHint() + p.trySkipToFutureRound(precommit.Round) + p.tryCommitUponSufficientPrecommits(precommit.Round) + p.tryTimeoutPrecommitUponSufficientPrecommits() } -// Marshal the process into binary. -func (p *Process) Marshal(w io.Writer, m int) (int, error) { - p.mu.Lock() - defer p.mu.Unlock() - return p.state.Marshal(w, m) +// Start the Process. +// +// L10: +// upon start do +// StartRound(0) +// +func (p *Process) Start() { + p.StartRound(0) } -// Unmarshal into this process from binary. -func (p *Process) Unmarshal(r io.Reader, m int) (int, error) { - p.mu.Lock() - defer p.mu.Unlock() - return p.state.Unmarshal(r, m) -} +// StartRound will progress the Process to a new Round. It does not asssume that +// the Height has changed. Since this changes the current Round and the current +// Step, most of the condition methods will be retried at the end (by way of +// defer). +// +// L11: +// Function StartRound(round) +// currentRound ← round +// currentStep ← propose +// if proposer(currentHeight, currentRound) = p then +// if validValue = nil then +// proposal ← validValue +// else +// proposal ← getValue() +// broadcast〈PROPOSAL, currentHeight, currentRound, proposal, validRound〉 +// else +// schedule OnTimeoutPropose(currentHeight, currentRound) to be executed after timeoutPropose(currentRound) +func (p *Process) StartRound(round Round) { + defer func() { + p.tryPrecommitUponSufficientPrevotes() + p.tryPrecommitNilUponSufficientPrevotes() + p.tryPrevoteUponPropose() + p.tryPrevoteUponSufficientPrevotes() + p.tryTimeoutPrecommitUponSufficientPrecommits() + p.tryTimeoutPrevoteUponSufficientPrevotes() + }() -// Start the process. -func (p *Process) Start() { - p.mu.Lock() - defer p.mu.Unlock() - - // Log the starting state of process for debugging purpose. - p.logger.Debugf("🎰 starting process at height=%v, round=%v, step=%v", p.state.CurrentHeight, p.state.CurrentRound, p.state.CurrentStep) - numProposes := p.state.Proposals.QueryByHeightRound(p.state.CurrentHeight, p.state.CurrentRound) - numPrevotes := p.state.Prevotes.QueryByHeightRound(p.state.CurrentHeight, p.state.CurrentRound) - numPrecommits := p.state.Precommits.QueryByHeightRound(p.state.CurrentHeight, p.state.CurrentRound) - p.logger.Debugf("propose inbox len=%v, prevote inbox len=%v, precommit inbox len=%v", numProposes, numPrevotes, numPrecommits) - - // Resend our latest messages to others. - p.resendLatestMessages(nil) - - // Query others for previous messages. - resync := NewResync(p.state.CurrentHeight, p.state.CurrentRound) - p.broadcaster.Broadcast(resync) - - // Start the Process from previous state. - if p.state.CurrentStep == StepNil || p.state.CurrentStep == StepPropose { - p.startRound(p.state.CurrentRound) - } - if numPrevotes >= 2*p.state.Prevotes.f+1 && p.state.CurrentStep == StepPrevote { - p.scheduleTimeoutPrevote(p.state.CurrentHeight, p.state.CurrentRound, p.timer.Timeout(StepPrevote, p.state.CurrentRound)) - } - if numPrecommits >= 2*p.state.Precommits.f+1 { - p.scheduleTimeoutPrecommit(p.state.CurrentHeight, p.state.CurrentRound, p.timer.Timeout(StepPrecommit, p.state.CurrentRound)) - } -} - -// StartRound is safe for concurrent use. See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func (p *Process) StartRound(round block.Round) { - p.mu.Lock() - defer p.mu.Unlock() - p.startRound(round) -} - -// HandleMessage is safe for concurrent use. See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func (p *Process) HandleMessage(m Message) { - p.mu.Lock() - defer p.mu.Unlock() - - switch m := m.(type) { - case *Propose: - p.handlePropose(m) - case *Prevote: - p.handlePrevote(m) - case *Precommit: - p.handlePrecommit(m) - case *Resync: - p.handleResync(m) - } -} - -// resend sends any messages stored at the given height and round to the `to` -// signatory. If no signatory is provided, we broadcast the message to all known -// peers. -func (p *Process) resend(to *id.Signatory, height block.Height, round block.Round) { - proposal := p.state.Proposals.QueryByHeightRoundSignatory(height, round, p.signatory) - prevote := p.state.Prevotes.QueryByHeightRoundSignatory(height, round, p.signatory) - precommit := p.state.Precommits.QueryByHeightRoundSignatory(height, round, p.signatory) - if proposal != nil { - // Resend messages to all peers if no signatory is provided. - if to == nil { - p.broadcaster.Broadcast(proposal) - } else { - p.broadcaster.Cast(*to, proposal) - } - } - if prevote != nil { - if to == nil { - p.broadcaster.Broadcast(prevote) - } else { - p.broadcaster.Cast(*to, prevote) - } + // Set the state the new round, and set the step to the first step in the + // sequence. We do not have special methods dedicated to change the current + // Roound, or changing the current Step to Proposing, because StartRound is + // the only location where this logic happens. + p.CurrentRound = round + p.CurrentStep = Proposing + + // If we are not the proposer, then we trigger the propose timeout. + proposer := p.scheduler.Schedule(p.CurrentHeight, p.CurrentRound) + if !p.whoami.Equal(&proposer) { + p.timer.TimeoutPropose(p.CurrentHeight, p.CurrentRound) + return } - if precommit != nil { - if to == nil { - p.broadcaster.Broadcast(precommit) - } else { - p.broadcaster.Cast(*to, precommit) - } + + // If we are the proposer, then we emit a propose. + proposeValue := p.ValidValue + if proposeValue.Equal(&NilValue) { + proposeValue = p.proposer.Propose(p.CurrentHeight, p.CurrentRound) } + p.broadcaster.BroadcastPropose(Propose{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + ValidRound: p.ValidRound, + Value: proposeValue, + }) } -func (p *Process) resendLatestMessages(to *id.Signatory) { - if !p.state.Equal(DefaultState(p.state.Prevotes.f)) { - p.logger.Debugf("resending messages at current height=%v and current round=%v", p.state.CurrentHeight, p.state.CurrentRound) - p.resend(to, p.state.CurrentHeight, p.state.CurrentRound) - if p.state.CurrentRound > 0 { - p.logger.Debugf("resending messages at current height=%v and previous round=%v", p.state.CurrentHeight, p.state.CurrentRound-1) - p.resend(to, p.state.CurrentHeight, p.state.CurrentRound-1) - } else if p.state.CurrentHeight > 0 { - maxRound := block.Round(0) - for round := range p.state.Proposals.messages[p.state.CurrentHeight-1] { - if round > maxRound { - maxRound = round - } - } - p.logger.Debugf("resending messages at previous height=%v and previous round=%v", p.state.CurrentHeight-1, maxRound) - p.resend(to, p.state.CurrentHeight-1, maxRound) - } +// OnTimeoutPropose is used to notify the Process that a timeout has been +// activated. It must only be called after the TimeoutPropose method in the +// Timer has been called. +// +// L57: +// Function OnTimeoutPropose(height, round) +// if height = currentHeight ∧ round = currentRound ∧ currentStep = propose then +// broadcast〈PREVOTE, currentHeight, currentRound, nil +// currentStep ← prevote +func (p *Process) OnTimeoutPropose(height Height, round Round) { + if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Proposing { + p.broadcaster.BroadcastPrevote(Prevote{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: NilValue, + }) + p.stepToPrevoting() + } +} + +// OnTimeoutPrevote is used to notify the Process that a timeout has been +// activated. It must only be called after the TimeoutPrevote method in the +// Timer has been called. +// +// L61: +// Function OnTimeoutPrevote(height, round) +// if height = currentHeight ∧ round = currentRound ∧ currentStep = prevote then +// broadcast〈PREVOTE, currentHeight, currentRound, nil +// currentStep ← prevote +func (p *Process) OnTimeoutPrevote(height Height, round Round) { + if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Prevoting { + p.broadcaster.BroadcastPrecommit(Precommit{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: NilValue, + }) + p.stepToPrecommitting() + } +} + +// OnTimeoutPrecommit is used to notify the Process that a timeout has been +// activated. It must only be called after the TimeoutPrecommit method in the +// Timer has been called. +// +// L65: +// Function OnTimeoutPrecommit(height, round) +// if height = currentHeight ∧ round = currentRound then +// StartRound(currentRound + 1) +func (p *Process) OnTimeoutPrecommit(height Height, round Round) { + if height == p.CurrentHeight && round == p.CurrentRound { + p.StartRound(round + 1) + } +} + +// L22: +// upon〈PROPOSAL, currentHeight, currentRound, v, −1〉from proposer(currentHeight, currentRound) +// while currentStep = propose do +// if valid(v) ∧ (lockedRound = −1 ∨ lockedValue = v) then +// broadcast〈PREVOTE, currentHeight, currentRound, id(v) +// else +// broadcast〈PREVOTE, currentHeight, currentRound, nil +// currentStep ← prevote +// +// This method must be tried whenever a Propose is received at the current +// Ronud, the current Round changes, the current Step changes to Proposing, the +// LockedRound changes, or the the LockedValue changes. +func (p *Process) tryPrevoteUponPropose() { + if p.CurrentStep != Proposing { + return } -} -func (p *Process) startRound(round block.Round) { - p.state.CurrentRound = round - p.state.CurrentStep = StepPropose + propose, ok := p.ProposeLogs[p.CurrentRound] + if !ok { + return + } + if propose.ValidRound != InvalidRound { + return + } - // If process p is the proposer. - if p.signatory.Equal(p.scheduler.Schedule(p.state.CurrentHeight, p.state.CurrentRound)) { - var proposal block.Block - if p.state.ValidBlock.Hash() != block.InvalidHash { - proposal = p.state.ValidBlock - } else { - proposal = p.proposer.BlockProposal(p.state.CurrentHeight, p.state.CurrentRound) - } - propose := NewPropose( - p.state.CurrentHeight, - p.state.CurrentRound, - proposal, - p.state.ValidRound, - ) - - // Include the previous block for nodes to catch up - previousBlock, ok := p.blockchain.BlockAtHeight(p.state.CurrentHeight - 1) - if !ok { - panic("fail to get previous block from storage") - } - messages := p.state.Precommits.QueryMessagesByHeightWithHighestRound(p.state.CurrentHeight - 1) - commits := make([]Precommit, 0, 2*p.state.Precommits.F()+1) - for _, message := range messages { - commit := message.(*Precommit) - if commit.blockHash.Equal(previousBlock.Hash()) { - commits = append(commits, *commit) - if len(commits) >= 2*p.state.Precommits.F()+1 { - // Restrict the len of commits to 2F+1, as is expected by - // the nodes that will be receiving this message. - break - } - } - } - if len(commits) < 2*p.state.Precommits.F()+1 { - commits = []Precommit{} - } - propose.latestCommit = LatestCommit{ - Block: previousBlock, - Precommits: commits, - } - p.logger.Infof("🔊 proposed block=%v at height=%v and round=%v", propose.BlockHash(), propose.height, propose.round) - p.broadcaster.Broadcast(propose) + if p.LockedRound == InvalidRound || p.LockedValue.Equal(&propose.Value) { + p.broadcaster.BroadcastPrevote(Prevote{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: propose.Value, + }) } else { - p.scheduleTimeoutPropose(p.state.CurrentHeight, p.state.CurrentRound, p.timer.Timeout(StepPropose, p.state.CurrentRound)) + p.broadcaster.BroadcastPrevote(Prevote{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: NilValue, + }) } + p.stepToPrevoting() } -func (p *Process) handlePropose(propose *Propose) { - // Before inserting the Propose, we need to check whether or not the Propose - // is from the scheduled Proposer. Otherwise, we can safely ignore it. - var firstTime bool - var conflicting Message - if propose.Signatory().Equal(p.scheduler.Schedule(propose.Height(), propose.Round())) { - p.logger.Debugf("received propose at height=%v and round=%v", propose.height, propose.round) - _, firstTime, _, _, _, conflicting = p.state.Proposals.Insert(propose) - if conflicting != nil && p.catcher != nil { - p.catcher.DidReceiveMessageConflict(conflicting, propose) - } - } else { - // Ignore out-of-turn Proposes. - p.logger.Warnf("received propose at height=%v and round=%v from out-of-turn proposer=%v", propose.height, propose.round, propose.signatory) +// L28: +// +// upon〈PROPOSAL, currentHeight, currentRound, v, vr〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, vr, id(v)〉 +// while currentStep = propose ∧ (vr ≥ 0 ∧ vr < currentRound) do +// if valid(v) ∧ (lockedRound ≤ vr ∨ lockedValue = v) then +// broadcast〈PREVOTE, currentHeight, currentRound, id(v)〉 +// else +// broadcast〈PREVOTE, currentHeight, currentRound, nil〉 +// currentStep ← prevote +// +// This method must be tried whenever a Propose is received at the current Rond, +// a Prevote is received (at any Round), the current Round changes, the +// LockedRound changes, or the the LockedValue changes. +func (p *Process) tryPrevoteUponSufficientPrevotes() { + if p.CurrentStep != Proposing { return } - p.syncLatestCommit(propose.latestCommit) - - // upon Propose{currentHeight, currentRound, block, -1} - if propose.Height() == p.state.CurrentHeight && propose.Round() == p.state.CurrentRound && propose.ValidRound() == block.InvalidRound { - // from Schedule{currentHeight, currentRound} - if propose.Signatory().Equal(p.scheduler.Schedule(p.state.CurrentHeight, p.state.CurrentRound)) { - // while currentStep = StepPropose - if p.state.CurrentStep == StepPropose { - var prevote *Prevote - nilReasons, err := p.validator.IsBlockValid(propose.Block(), true) - if err == nil && (p.state.LockedRound == block.InvalidRound || p.state.LockedBlock.Equal(propose.Block())) { - prevote = NewPrevote( - p.state.CurrentHeight, - p.state.CurrentRound, - propose.Block().Hash(), - nilReasons, - ) - p.logger.Debugf("prevoted=%v at height=%v and round=%v", propose.BlockHash(), propose.height, propose.round) - } else { - prevote = NewPrevote( - p.state.CurrentHeight, - p.state.CurrentRound, - block.InvalidHash, - nilReasons, - ) - p.logger.Warnf("prevoted= at height=%v and round=%v (invalid propose: %v)", propose.height, propose.round, err) - } - p.state.CurrentStep = StepPrevote - p.broadcaster.Broadcast(prevote) - } - } + propose, ok := p.ProposeLogs[p.CurrentRound] + if !ok { + return + } + if propose.ValidRound == InvalidRound || propose.ValidRound >= p.CurrentRound { + return } - // Resend our prevote from the valid round if it exists in case of missed - // messages. - if propose.ValidRound() > block.InvalidRound { - prevote := p.state.Prevotes.QueryByHeightRoundSignatory(propose.Height(), propose.ValidRound(), p.signatory) - if prevote != nil { - p.broadcaster.Broadcast(prevote) + prevotesInValidRound := 0 + for _, prevote := range p.PrevoteLogs[propose.ValidRound] { + if prevote.Value.Equal(&propose.Value) { + prevotesInValidRound++ } } - - // upon f+1 *{currentHeight, round, *, *} and round > currentRound - n := p.numberOfMessagesAtCurrentHeight(propose.Round()) - if n > p.state.Prevotes.F() && propose.Height() == p.state.CurrentHeight && propose.Round() > p.state.CurrentRound { - p.startRound(propose.Round()) + if prevotesInValidRound < 2*p.f+1 { + return } - if propose.Height() == p.state.CurrentHeight { - if propose.Round() == p.state.CurrentRound { - // These conditions can only be true when the Propose was for the - // current height and round, so we only call them if the Propose was - // in fact for the current height and round. - p.checkProposeInCurrentHeightAndRoundWithPrevotes() - if firstTime { - p.checkProposeInCurrentHeightAndRoundWithPrevotesForTheFirstTime() - } - } - // This condition can only be true when the Propose was for the - // current height, so we only call it if the Propose was in fact for - // the current height. - p.checkProposeInCurrentHeightWithPrecommits(propose.Round()) + if p.LockedRound <= propose.ValidRound || p.LockedValue.Equal(&propose.Value) { + p.broadcaster.BroadcastPrevote(Prevote{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: propose.Value, + }) + } else { + p.broadcaster.BroadcastPrevote(Prevote{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: NilValue, + }) } + p.stepToPrevoting() } -func (p *Process) handlePrevote(prevote *Prevote) { - prevoteDebugStr := "" - if !prevote.blockHash.Equal(block.InvalidHash) { - prevoteDebugStr = prevote.blockHash.String() +// L34: +// +// upon 2f+ 1〈PREVOTE, currentHeight, currentRound, ∗〉 +// while currentStep = prevote for the first time do +// scheduleOnTimeoutPrevote(currentHeight, currentRound) to be executed after timeoutPrevote(currentRound) +// +// This method must be tried whenever a Prevote is received at the current +// Round, the current Round changes, or the current Step changes to Prevoting. +// It assumes that the Timer will eventually call the OnTimeoutPrevote method. +// This method must only succeed once in any current Round. +func (p *Process) tryTimeoutPrevoteUponSufficientPrevotes() { + if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) { + return } - p.logger.Debugf("received prevote=%v at height=%v and round=%v", prevoteDebugStr, prevote.height, prevote.round) - _, _, _, firstTimeExceeding2F, firstTimeExceeding2FOnBlockHash, conflicting := p.state.Prevotes.Insert(prevote) - if conflicting != nil && p.catcher != nil { - p.catcher.DidReceiveMessageConflict(conflicting, prevote) + if p.CurrentStep != Prevoting { + return } - if firstTimeExceeding2F && prevote.Height() == p.state.CurrentHeight && prevote.Round() == p.state.CurrentRound && p.state.CurrentStep == StepPrevote { - // upon 2f+1 Prevote{currentHeight, currentRound, *} while step = StepPrevote for the first time - p.scheduleTimeoutPrevote(p.state.CurrentHeight, p.state.CurrentRound, p.timer.Timeout(StepPrevote, p.state.CurrentRound)) + if len(p.PrevoteLogs[p.CurrentRound]) == 2*p.f+1 { + p.timer.TimeoutPrevote(p.CurrentHeight, p.CurrentRound) } + p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) +} - // upon f+1 Prevote{currentHeight, currentRound, nil} - if n := p.state.Prevotes.QueryByHeightRoundBlockHash(p.state.CurrentHeight, p.state.CurrentRound, block.InvalidHash); n > p.state.Prevotes.F() { - // if we are the proposer - if p.signatory.Equal(p.scheduler.Schedule(p.state.CurrentHeight, p.state.CurrentRound)) { - p.observer.DidReceiveSufficientNilPrevotes(p.state.Prevotes.QueryMessagesByHeightRound(p.state.CurrentHeight, p.state.CurrentRound), p.state.Prevotes.F()) - } +// L36: +// +// upon〈PROPOSAL, currentHeight, currentRound, v, ∗〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, currentRound, id(v)〉 +// while valid(v) ∧ currentStep ≥ prevote for the first time do +// if currentStep = prevote then +// lockedValue ← v +// lockedRound ← currentRound +// broadcast〈PRECOMMIT, currentHeight, currentRound, id(v))〉 +// currentStep ← precommit +// validValue ← v +// validRound ← currentRound +// +// This method must be tried whenever a Propose is received at the current +// Round, a Prevote is received at the current Round, the current Round changes, +// or the current Step changes to Prevoting or Precommitting. This method must +// only succeed once in any current Round. +func (p *Process) tryPrecommitUponSufficientPrevotes() { + if p.checkOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) { + return } - - // upon 2f+1 Prevote{currentHeight, currentRound, nil} while currentStep = StepPrevote - if n := p.state.Prevotes.QueryByHeightRoundBlockHash(p.state.CurrentHeight, p.state.CurrentRound, block.InvalidHash); n > 2*p.state.Prevotes.F() && p.state.CurrentStep == StepPrevote { - precommit := NewPrecommit( - p.state.CurrentHeight, - p.state.CurrentRound, - block.InvalidHash, - ) - p.logger.Debugf("precommited= at height=%v and round=%v (2f+1 prevote=)", precommit.height, precommit.round) - p.state.CurrentStep = StepPrecommit - p.broadcaster.Broadcast(precommit) + if p.CurrentStep < Prevoting { + return } - // upon f+1 *{currentHeight, round, *, *} and round > currentRound - n := p.numberOfMessagesAtCurrentHeight(prevote.Round()) - if n > p.state.Prevotes.F() && prevote.Height() == p.state.CurrentHeight && prevote.Round() > p.state.CurrentRound { - p.startRound(prevote.Round()) + propose, ok := p.ProposeLogs[p.CurrentRound] + if !ok { + return } - - if prevote.Height() == p.state.CurrentHeight { - // These conditions can only be true when the Prevote was for the - // current height (not necessarily the current round), so we only call - // them if the Prevote was in fact for the current height and round. - p.checkProposeInCurrentHeightAndRoundWithPrevotes() - if prevote.Round() == p.state.CurrentRound && firstTimeExceeding2FOnBlockHash { - p.checkProposeInCurrentHeightAndRoundWithPrevotesForTheFirstTime() + prevotesForValue := 0 + for _, prevote := range p.PrevoteLogs[p.CurrentRound] { + if prevote.Value.Equal(&propose.Value) { + prevotesForValue++ } } -} - -func (p *Process) handlePrecommit(precommit *Precommit) { - precommitDebugStr := "" - if !precommit.blockHash.Equal(block.InvalidHash) { - precommitDebugStr = precommit.blockHash.String() - } - p.logger.Debugf("received precommit=%v at height=%v and round=%v", precommitDebugStr, precommit.height, precommit.round) - // upon 2f+1 Precommit{currentHeight, currentRound, *} for the first time - _, _, _, firstTimeExceeding2F, _, conflicting := p.state.Precommits.Insert(precommit) - if conflicting != nil && p.catcher != nil { - p.catcher.DidReceiveMessageConflict(conflicting, precommit) - } - if firstTimeExceeding2F && precommit.Height() == p.state.CurrentHeight && precommit.Round() == p.state.CurrentRound { - p.scheduleTimeoutPrecommit(p.state.CurrentHeight, p.state.CurrentRound, p.timer.Timeout(StepPrecommit, p.state.CurrentRound)) + if prevotesForValue < 2*p.f+1 { + return } - // upon f+1 *{currentHeight, round, *, *} and round > currentRound - n := p.numberOfMessagesAtCurrentHeight(precommit.Round()) - if n > p.state.Precommits.F() && precommit.Height() == p.state.CurrentHeight && precommit.Round() > p.state.CurrentRound { - p.startRound(precommit.Round()) - } + if p.CurrentStep == Prevoting { + p.LockedValue = propose.Value + p.LockedRound = p.CurrentRound + p.broadcaster.BroadcastPrecommit(Precommit{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: propose.Value, + }) + p.stepToPrecommitting() - if precommit.Height() == p.state.CurrentHeight { - // This condition can only be true when the Precommit was for the - // current height, so we only call it if the Precommit was in fact for - // the current height. - p.checkProposeInCurrentHeightWithPrecommits(precommit.Round()) + // Beacuse the LockedValue and LockedRound have changed, we need to try + // this condition again. + defer func() { + p.tryPrevoteUponPropose() + p.tryPrevoteUponSufficientPrevotes() + }() } + p.ValidValue = propose.Value + p.ValidRound = p.CurrentRound + p.setOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) } -func (p *Process) handleResync(resync *Resync) { - p.logger.Debugf("received resync at height=%v and round=%v", resync.height, resync.round) - - // If we need to resend messages after a height and round that are ahead of - // us, then there is nothing to resend. - if p.state.CurrentHeight < resync.height { +// L44: +// +// upon 2f+ 1〈PREVOTE, currentHeight, currentRound, nil〉 +// while currentStep = prevote do +// broadcast〈PRECOMMIT, currentHeight, currentRound, nil〉 +// currentStep ← precommit +// +// This method must be tried whenever a Prevote is received at the current +// Round, the current Round changes, or the Step changes to Prevoting. +func (p *Process) tryPrecommitNilUponSufficientPrevotes() { + if p.CurrentStep != Prevoting { return } - if p.state.CurrentHeight == resync.height && p.state.CurrentRound < resync.round { - return + prevotesForNil := 0 + for _, prevote := range p.PrevoteLogs[p.CurrentRound] { + if prevote.Value.Equal(&NilValue) { + prevotesForNil++ + } } - - // Resend our latest messages to the requestor. - p.resendLatestMessages(&resync.signatory) -} - -// timeoutPropose checks if we have move to a new height, a new round or a new -// step after the timeout. If not, prevote for a invalid block and broadcast -// the vote, then move to prevote step. -func (p *Process) timeoutPropose(height block.Height, round block.Round) { - if height == p.state.CurrentHeight && round == p.state.CurrentRound && p.state.CurrentStep == StepPropose { - prevote := NewPrevote( - p.state.CurrentHeight, - p.state.CurrentRound, - block.InvalidHash, - nil, - ) - p.logger.Warnf("prevoted= at height=%v and round=%v (timeout)", prevote.height, prevote.round) - p.state.CurrentStep = StepPrevote - p.broadcaster.Broadcast(prevote) + if prevotesForNil == 2*p.f+1 { + p.broadcaster.BroadcastPrecommit(Precommit{ + Height: p.CurrentHeight, + Round: p.CurrentRound, + Value: NilValue, + }) + p.stepToPrecommitting() } } -func (p *Process) timeoutPrevote(height block.Height, round block.Round) { - if height == p.state.CurrentHeight && round == p.state.CurrentRound && p.state.CurrentStep == StepPrevote { - precommit := NewPrecommit( - p.state.CurrentHeight, - p.state.CurrentRound, - block.InvalidHash, - ) - p.logger.Warnf("precommitted= at height=%v and round=%v (timeout)", precommit.height, precommit.round) - p.state.CurrentStep = StepPrecommit - p.broadcaster.Broadcast(precommit) +// L47: +// +// upon 2f+ 1〈PRECOMMIT, currentHeight, currentRound, ∗〉for the first time do +// scheduleOnTimeoutPrecommit(currentHeight, currentRound) to be executed after timeoutPrecommit(currentRound) +// +// This method must be tried whenever a Precommit is received at the current +// Round, or the current Round changes. It assumes that the Timer will +// eventually call the OnTimeoutPrecommit method. This method must only succeed +// once in any current Round. +func (p *Process) tryTimeoutPrecommitUponSufficientPrecommits() { + if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) { + return + } + if len(p.PrecommitLogs[p.CurrentRound]) == 2*p.f+1 { + p.timer.TimeoutPrecommit(p.CurrentHeight, p.CurrentRound) + p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) } } -func (p *Process) timeoutPrecommit(height block.Height, round block.Round) { - if height == p.state.CurrentHeight && round == p.state.CurrentRound { - p.startRound(p.state.CurrentRound + 1) +// L49: +// +// upon〈PROPOSAL, currentHeight, r, v, ∗〉from proposer(currentHeight, r) AND 2f+ 1〈PRECOMMIT, currentHeight, r, id(v)〉 +// while decision[currentHeight] = nil do +// if valid(v) then +// decision[currentHeight] = v +// currentHeight ← currentHeight + 1 +// reset +// StartRound(0) +// +// This method must be tried whenever a Propose is received, or a Precommit is +// received. Because this method checks whichever Round is relevant (i.e. the +// Round of the Propose/Precommit), it does not need to be tried whenever the +// current Round changes. +// +// We can avoid explicitly checking for validity of the Propose value, because +// no Propose value is stored in the message logs unless it is valid. We can +// also avoid checking for a nil-decision at the current Height, because the +// only condition under which this would not be true is when the Process has +// progressed passed the Height in question (put another way, the fact that this +// method causes the Height to be incremented prevents it from being triggered +// multiple times). +func (p *Process) tryCommitUponSufficientPrecommits(round Round) { + propose, ok := p.ProposeLogs[round] + if !ok { + return } -} + precommitsForValue := 0 + for _, precommit := range p.PrecommitLogs[round] { + if precommit.Value.Equal(&propose.Value) { + precommitsForValue++ + } + } + if precommitsForValue == 2*p.f+1 { + p.committer.Commit(p.CurrentHeight, propose.Value) + p.CurrentHeight++ -func (p *Process) scheduleTimeoutPropose(height block.Height, round block.Round, duration time.Duration) { - go func() { - time.Sleep(duration) + // Reset lockedRound, lockedValue, validRound, and validValue to initial + // values. + p.LockedValue = NilValue + p.LockedRound = InvalidRound + p.ValidValue = NilValue + p.ValidRound = InvalidRound - p.mu.Lock() - defer p.mu.Unlock() + // Empty message logs in preparation for the new Height. + p.ProposeLogs = map[Round]Propose{} + p.PrevoteLogs = map[Round]map[id.Signatory]Prevote{} + p.PrecommitLogs = map[Round]map[id.Signatory]Precommit{} + p.OnceFlags = map[Round]OnceFlag{} - p.timeoutPropose(height, round) - }() + // Start from the first Round in the new Height. + p.StartRound(0) + } } -func (p *Process) scheduleTimeoutPrevote(height block.Height, round block.Round, duration time.Duration) { - go func() { - time.Sleep(duration) +// L55: +// +// upon f+ 1〈∗, currentHeight, r, ∗, ∗〉with r > currentRound do +// StartRound(r) +// +// This method must be tried whenever a Propose is received, a Prevote is +// received, or a Precommit is received. Because this method checks whichever +// Round is relevant (i.e. the Round of the Propose/Prevote/Precommit), and an +// increase in the current Round can only cause this condition to be closed, it +// does not need to be tried whenever the current Round changes. +func (p *Process) trySkipToFutureRound(round Round) { + if round <= p.CurrentRound { + return + } - p.mu.Lock() - defer p.mu.Unlock() + msgsInRound := 0 + if _, ok := p.ProposeLogs[round]; ok { + msgsInRound = 1 + } + msgsInRound += len(p.PrevoteLogs[round]) + msgsInRound += len(p.PrecommitLogs[round]) - p.timeoutPrevote(height, round) - }() + if msgsInRound == p.f+1 { + p.StartRound(round) + } } -func (p *Process) scheduleTimeoutPrecommit(height block.Height, round block.Round, duration time.Duration) { - go func() { - time.Sleep(duration) +// insertPropose after validating it and checking for duplicates. If the Propose +// was accepted and inserted, then it return true, otherwise it returns false. +func (p *Process) insertPropose(propose Propose) bool { + if propose.Height != p.CurrentHeight { + return false + } + + existingPropose, ok := p.ProposeLogs[propose.Round] + if ok { + // We have caught a Process attempting to broadcast two different + // Proposes at the same Height and Round. Even though we only + // explicitly check the Round, we know that the Proposes will have the + // same Height, because we only keep message logs for message with the + // same Height as the current Height of the Process. + if !propose.Equal(&existingPropose) { + p.catcher.CatchDoublePropose(propose, existingPropose) + } + return false + } + proposer := p.scheduler.Schedule(propose.Height, propose.Round) + if !proposer.Equal(&propose.From) { + return false + } - p.mu.Lock() - defer p.mu.Unlock() + // By never inserting a Propose that is not valid, we can avoid the validity + // checks elsewhere in the Process. + if !p.validator.Valid(propose.Value) { + return false + } - p.timeoutPrecommit(height, round) - }() + p.ProposeLogs[propose.Round] = propose + return true } -// checkProposeInCurrentHeightAndRoundWithPrevotes must only be called when a -// Propose or Prevote has been seen for the first time, and it is possible that -// a Propose and 2f+1 Prevotes have been seen where the Propose is at the -// current `block.Height` and `block.Round`, and the 2f+1 Prevotes are at the -// current `block.Height` and valid `block.Round` of the Propose. This can -// happen when a Propose is seen for the first time at the current -// `block.Height` and `block.Round`, or, when a Prevote is seen for the first -// time at the current `block.Height` and any `block.Round`. It is ok to call -// this function multiple times. -func (p *Process) checkProposeInCurrentHeightAndRoundWithPrevotes() { - // upon Propose{currentHeight, currentRound, block, validRound} from Schedule(currentHeight, currentRound) - m := p.state.Proposals.QueryByHeightRoundSignatory(p.state.CurrentHeight, p.state.CurrentRound, p.scheduler.Schedule(p.state.CurrentHeight, p.state.CurrentRound)) - if m == nil { - return +// insertPrevote after validating it and checking for duplicates. If the Prevote +// was accepted and inserted, then it return true, otherwise it returns false. +func (p *Process) insertPrevote(prevote Prevote) bool { + if prevote.Height != p.CurrentHeight { + return false } - propose := m.(*Propose) - - if propose.ValidRound() > block.InvalidRound { - // and 2f+1 Prevote{currentHeight, validRound, blockHash} - n := p.state.Prevotes.QueryByHeightRoundBlockHash(p.state.CurrentHeight, propose.ValidRound(), propose.BlockHash()) - if n > 2*p.state.Prevotes.F() { - // while step = StepPropose and validRound >= 0 and validRound < currentRound - if p.state.CurrentStep == StepPropose && propose.ValidRound() < p.state.CurrentRound { - var prevote *Prevote - nilReasons, err := p.validator.IsBlockValid(propose.Block(), true) - if err == nil && (p.state.LockedRound <= propose.ValidRound() || p.state.LockedBlock.Equal(propose.Block())) { - prevote = NewPrevote( - p.state.CurrentHeight, - p.state.CurrentRound, - propose.Block().Hash(), - nilReasons, - ) - p.logger.Debugf("prevoted=%v at height=%v and round=%v (2f+1 valid prevotes)", prevote.blockHash, prevote.height, prevote.round) - } else { - prevote = NewPrevote( - p.state.CurrentHeight, - p.state.CurrentRound, - block.InvalidHash, - nilReasons, - ) - p.logger.Warnf("prevoted= at height=%v and round=%v (invalid propose: %v)", prevote.height, prevote.round, err) - } - - p.state.CurrentStep = StepPrevote - p.broadcaster.Broadcast(prevote) - } - } + if _, ok := p.PrevoteLogs[prevote.Round]; !ok { + p.PrevoteLogs[prevote.Round] = map[id.Signatory]Prevote{} } -} -// checkProposeInCurrentHeightAndRoundWithPrevotesForTheFirstTime checks and -// reacts to a Propose and Prevote 2f+1 Prevotes having been seen for the first -// time at the current `block.Height` and `block.Round`. This can happen when a -// Propose is seen for the first time at the current `block.Height` and -// `block.Round`, or, when a Prevote is seen for the first time at the current -// `block.Height` and `block.Round`. This function can be called multiple times -// pre-emptively (when it is not yet the case that a Propose and 2f+1 Prevotes -// has been seen for the first time at the current `block.Height` and -// `block.Round`), but it must only be called once when the condition is true. -func (p *Process) checkProposeInCurrentHeightAndRoundWithPrevotesForTheFirstTime() { - // upon Propose{currentHeight, currentRound, block, *} from Schedule(currentHeight, currentRound) - m := p.state.Proposals.QueryByHeightRoundSignatory(p.state.CurrentHeight, p.state.CurrentRound, p.scheduler.Schedule(p.state.CurrentHeight, p.state.CurrentRound)) - if m == nil { - return - } - propose := m.(*Propose) - - // and 2f+1 Prevote{currentHeight, currentRound, blockHash} while Validate(block) and step >= StepPrevote for the first time - n := p.state.Prevotes.QueryByHeightRoundBlockHash(p.state.CurrentHeight, p.state.CurrentRound, propose.BlockHash()) - if n > 2*p.state.Prevotes.F() { - _, err := p.validator.IsBlockValid(propose.Block(), true) - if p.state.CurrentStep >= StepPrevote && err == nil { - p.state.ValidBlock = propose.Block() - p.state.ValidRound = p.state.CurrentRound - if p.state.CurrentStep == StepPrevote { - p.state.LockedBlock = propose.Block() - p.state.LockedRound = p.state.CurrentRound - p.state.CurrentStep = StepPrecommit - precommit := NewPrecommit( - p.state.CurrentHeight, - p.state.CurrentRound, - propose.Block().Hash(), - ) - p.logger.Debugf("precommitted=%v at height=%v and round=%v", precommit.blockHash, p.state.CurrentHeight, p.state.CurrentRound) - p.broadcaster.Broadcast(precommit) - } - } else { - p.logger.Warnf("nothing precommitted at height=%v, round=%v and step=%v (invalid block: %v)", propose.height, propose.round, p.state.CurrentStep, err) + existingPrevote, ok := p.PrevoteLogs[prevote.Round][prevote.From] + if ok { + // We have caught a Process attempting to broadcast two different + // Prevotes at the same Height and Round. Even though we only explicitly + // check the Round, we know that the Prevotes will have the same Height, + // because we only keep message logs for message with the same Height as + // the current Height of the Process. + if !prevote.Equal(&existingPrevote) { + p.catcher.CatchDoublePrevote(prevote, existingPrevote) } + return false } + + p.PrevoteLogs[prevote.Round][prevote.From] = prevote + return true } -// checkProposeInCurrentHeightWithPrecommits must only be called when a Propose -// or Precommit has been seen for the first time, and it is possible that a -// Propose and 2f+1 Precommits have been seen where the Propose is at the -// current `block.Height` and any `block.Round`, and the 2f+1 Precommits are at -// the current `block.Height` and the as `block.Round` as the Propose. This can -// happen when a Propose is seen for the first time at the current -// `block.Height`, or when a Precommit is seen for the first time at the current -// `block.Height`. It is ok to call this function multiple times. -func (p *Process) checkProposeInCurrentHeightWithPrecommits(round block.Round) { - // upon Propose{currentHeight, round, block, *} from Schedule(currentHeight, round) - m := p.state.Proposals.QueryByHeightRoundSignatory(p.state.CurrentHeight, round, p.scheduler.Schedule(p.state.CurrentHeight, round)) - if m == nil { - return +// insertPrecommit after validating it and checking for duplicates. If the +// Precommit was accepted and inserted, then it return true, otherwise it +// returns false. +func (p *Process) insertPrecommit(precommit Precommit) bool { + if precommit.Height != p.CurrentHeight { + return false + } + if _, ok := p.PrecommitLogs[precommit.Round]; !ok { + p.PrecommitLogs[precommit.Round] = map[id.Signatory]Precommit{} } - propose := m.(*Propose) - - // and 2f+1 Precommits{currentHeight, round, blockHash} - n := p.state.Precommits.QueryByHeightRoundBlockHash(p.state.CurrentHeight, round, propose.BlockHash()) - if n > 2*p.state.Precommits.F() { - // while !BlockExistsAtHeight(currentHeight) - if !p.blockchain.BlockExistsAtHeight(p.state.CurrentHeight) { - _, err := p.validator.IsBlockValid(propose.Block(), false) - if err == nil { - p.blockchain.InsertBlockAtHeight(p.state.CurrentHeight, propose.Block()) - p.state.CurrentHeight++ - p.state.Reset(p.state.CurrentHeight - 1) - if p.observer != nil { - p.observer.DidCommitBlock(p.state.CurrentHeight - 1) - } - p.logger.Infof("✅ committed block=%v at height=%v", propose.BlockHash(), propose.height) - p.startRound(0) - - // If we just committed a base block, then we need to - // resynchronise with other nodes, in case we have dropped - // Proposes from this new base that arrived bfeore the new base. - if propose.Block().Header().Kind() == block.Base { - p.broadcaster.Broadcast(NewResync(p.state.CurrentHeight, p.state.CurrentRound)) - } - } else { - p.logger.Warnf("nothing committed at height=%v and round=%v (invalid block: %v)", propose.height, propose.round, err) - } + + existingPrecommit, ok := p.PrecommitLogs[precommit.Round][precommit.From] + if ok { + // We have caught a Process attempting to broadcast two different + // Precommits at the same Height and Round. Even though we only + // explicitly check the Round, we know that the Precommits will have the + // same Height, because we only keep message logs for message with the + // same Height as the current Height of the Process. + if !precommit.Equal(&existingPrecommit) { + p.catcher.CatchDoublePrecommit(precommit, existingPrecommit) } + return false } -} -func (p *Process) numberOfMessagesAtCurrentHeight(round block.Round) int { - numUniqueProposals := p.state.Proposals.QueryByHeightRound(p.state.CurrentHeight, round) - numUniquePrevotes := p.state.Prevotes.QueryByHeightRound(p.state.CurrentHeight, round) - numUniquePrecommits := p.state.Precommits.QueryByHeightRound(p.state.CurrentHeight, round) - return numUniqueProposals + numUniquePrevotes + numUniquePrecommits + p.PrecommitLogs[precommit.Round][precommit.From] = precommit + return true } -func (p *Process) syncLatestCommit(latestCommit LatestCommit) { - // Check that they have not included too many signatories. This is required - // to protect against DoS attacks performed by including a massive number of - // Precommits. - if latestCommit.Precommits == nil || len(latestCommit.Precommits) != 2*p.state.Precommits.F()+1 { - return - } +// stepToPrevoting puts the Process into the Prevoting Step. This will also try +// other methods that might now have passing conditions. +func (p *Process) stepToPrevoting() { + p.CurrentStep = Prevoting - // Check that the latest commit is from the future. - if latestCommit.Block.Header().Height() <= p.state.CurrentHeight { - return - } + // Because the current Step of the Process has changed, new conditions might + // be open, so we try the relevant ones. Once flags protect us against + // double-tries where necessary. + p.tryPrecommitUponSufficientPrevotes() + p.tryPrecommitNilUponSufficientPrevotes() + p.tryTimeoutPrevoteUponSufficientPrevotes() +} - // Check the proposed block and previous block without historical data. It - // needs the validator to store the previous execute state. - _, err := p.validator.IsBlockValid(latestCommit.Block, false) - if err != nil { - p.logger.Warnf("error syncing to height=%v and round=%v (invalid block: %v)", latestCommit.Block.Header().Height(), latestCommit.Block.Header().Round(), err) - return - } +// stepToPrecommitting puts the Process into the Precommitting Step. This will +// also try other methods that might now have passing conditions. +func (p *Process) stepToPrecommitting() { + p.CurrentStep = Precommitting - // Validate the commits - signatories := map[id.Signatory]struct{}{} - baseBlock := p.blockchain.LatestBaseBlock() - for _, sig := range baseBlock.Header().Signatories() { - signatories[sig] = struct{}{} - } - for _, commit := range latestCommit.Precommits { - if err := Verify(&commit); err != nil { - return - } - if _, ok := signatories[commit.signatory]; !ok { - return - } - if !commit.blockHash.Equal(latestCommit.Block.Hash()) { - return - } - if commit.height != latestCommit.Block.Header().Height() { - return - } - if commit.round != latestCommit.Block.Header().Round() { - return - } - } + // Because the current Step of the Process has changed, new conditions might + // be open, so we try the relevant ones. Once flags protect us against + // double-tries where necessary. + p.tryPrecommitUponSufficientPrevotes() +} - // Check we have 2f+1 distinct commits - signatories = map[id.Signatory]struct{}{} - for _, commit := range latestCommit.Precommits { - signatories[commit.Signatory()] = struct{}{} - } - if len(signatories) < 2*p.state.Proposals.f+1 { - return - } +// checkOnceFlag returns true if the OnceFlag has already been set for the given +// Round. Otherwise, it returns false. +func (p *Process) checkOnceFlag(round Round, flag OnceFlag) bool { + return p.OnceFlags[round]&flag == flag +} - // if the commits are valid, store the block if we don't have one - if !p.blockchain.BlockExistsAtHeight(latestCommit.Block.Header().Height()) { - p.blockchain.InsertBlockAtHeight(latestCommit.Block.Header().Height(), latestCommit.Block) - } - p.logger.Infof("syncing from height=%v to height=%v", p.state.CurrentHeight, latestCommit.Block.Header().Height()+1) - p.state.CurrentHeight = latestCommit.Block.Header().Height() + 1 - p.state.CurrentRound = 0 - p.state.Reset(latestCommit.Block.Header().Height()) - p.startRound(p.state.CurrentRound) +// setOnceFlag set the OnceFlag for the given Round. +func (p *Process) setOnceFlag(round Round, flag OnceFlag) { + p.OnceFlags[round] |= flag } + +// A OnceFlag is used to guarantee that events only happen once in any given +// Round. +type OnceFlag uint16 + +// Enumerate all OnceFlag values. +const ( + OnceFlagTimeoutPrecommitUponSufficientPrecommits = OnceFlag(1) + OnceFlagTimeoutPrevoteUponSufficientPrevotes = OnceFlag(2) + OnceFlagPrecommitUponSufficientPrevotes = OnceFlag(4) +) diff --git a/process/process_suite_test.go b/process/process_suite_test.go index d686fdb3..6b9f6138 100644 --- a/process/process_suite_test.go +++ b/process/process_suite_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestProcess(t *testing.T) { +func TestProc(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Process Suite") } diff --git a/process/process_test.go b/process/process_test.go index 67935b4d..05f732af 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -1,945 +1 @@ package process_test - -import ( - "bytes" - "crypto/ecdsa" - cRand "crypto/rand" - "fmt" - "math/rand" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/id" - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/process" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Process", func() { - - newEcdsaKey := func() *ecdsa.PrivateKey { - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - Expect(err).NotTo(HaveOccurred()) - return privateKey - } - - Context("when marshaling/unmarshaling process", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - processOrigin := NewProcessOrigin(100) - processOrigin.State.CurrentHeight = block.Height(100) // make sure it's not proposing block. - process := processOrigin.ToProcess() - - data, err := surge.ToBinary(process) - Expect(err).NotTo(HaveOccurred()) - newProcess := processOrigin.ToProcess() - Expect(surge.FromBinary(data, newProcess)).Should(Succeed()) - - // Since state cannot be accessed from the process. We try to compared the - // marshalling bytes to check if they we get the same process. - newData, err := surge.ToBinary(newProcess) - Expect(err).NotTo(HaveOccurred()) - Expect(bytes.Equal(data, newData)).Should(BeTrue()) - }) - }) - - Context("when a new process is initialized", func() { - Context("when the process is the proposer", func() { - Context("when the valid block is nil", func() { - It("should propose a block generated proposer and broadcast it", func() { - // Init a default process to be modified - processOrigin := NewProcessOrigin(100) - process := processOrigin.ToProcess() - process.Start() - - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok := message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the proposer broadcast a propose message with height 1 and round 0 - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - proposal, ok := message.(*Propose) - Expect(ok).Should(BeTrue()) - Expect(proposal.Height()).Should(Equal(block.Height(1))) - Expect(proposal.Round()).Should(BeZero()) - }) - }) - - Context("when the valid block is not nil", func() { - It("should propose the valid block we have and broadcast it", func() { - // Init a default process to be modified - processOrigin := NewProcessOrigin(100) - block := processOrigin.Proposer.BlockProposal(0, 0) - processOrigin.State.ValidBlock = block - process := processOrigin.ToProcess() - process.StartRound(0) - - // Expect the proposer broadcast a propose message with zero height and round - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - proposal, ok := message.(*Propose) - Expect(ok).Should(BeTrue()) - proposal.Block().Equal(block) - }) - }) - }) - - Context("when the process is not proposer", func() { - Context("when we receive a propose from the proposer before the timeout expires", func() { - Context("when the block is valid", func() { - It("should broadcast a prevote to the proposal", func() { - // Initialise a default process. - processOrigin := NewProcessOrigin(100) - - // Replace the scheduler and start the process. - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - process := processOrigin.ToProcess() - - // Generate a valid proposal. - message := NewPropose(1, 0, RandomBlock(block.Standard), block.InvalidRound) - Expect(Sign(message, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(message) - - // Expect the proposer broadcasts a propose message with - // zero height and round. - var propose Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&propose)) - proposal, ok := propose.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(proposal.Height()).Should(Equal(block.Height(1))) - Expect(proposal.Round()).Should(BeZero()) - }) - }) - - Context("when the block is invalid", func() { - It("should broadcast a nil prevote", func() { - // Initialise a default process. - processOrigin := NewProcessOrigin(100) - - // Replace the broadcaster and start the process. - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - processOrigin.Validator = NewMockValidator(fmt.Errorf("")) - process := processOrigin.ToProcess() - - // Generate an invalid proposal. - message := NewPropose(1, 0, RandomBlock(block.Standard), block.InvalidRound) - Expect(Sign(message, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(message) - - // Ensure we receive a propose message with the zero - // height and round. - var propose Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&propose)) - proposal, ok := propose.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(proposal.Height()).Should(Equal(block.Height(1))) - Expect(proposal.Round()).Should(BeZero()) - }) - }) - - Context("when the valid block is not nil", func() { - It("should broadcast our prevote from that round", func() { - // Initialise a default process. - processOrigin := NewProcessOrigin(100) - - // Replace the broadcaster. - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - processOrigin.Validator = NewMockValidator(fmt.Errorf("")) - - // Insert a prevote for the valid round as this is the - // message we will be expected to resend later. - validRound := RandomRound() - prevote := NewPrevote(1, validRound, RandomBlock(block.Standard).Hash(), nil) - Expect(Sign(prevote, *processOrigin.PrivateKey)).ShouldNot(HaveOccurred()) - processOrigin.State.Prevotes.Insert(prevote) - - // Start the process. - process := processOrigin.ToProcess() - - // Generate a valid proposal with a valid round. - propose := NewPropose(1, 0, RandomBlock(block.Standard), validRound) - Expect(Sign(propose, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(propose) - - // Ensure we broadcast a prevote message for the valid - // round. - Eventually(processOrigin.BroadcastMessages).Should(Receive(&prevote)) - Expect(prevote.Height()).Should(Equal(block.Height(1))) - Expect(prevote.Round()).Should(Equal(validRound)) - }) - }) - }) - - Context("when we do not receive a propose during the timeout", func() { - It("should broadcast a nil prevote", func() { - By("before reboot") - - // Initialise a default process. - processOrigin := NewProcessOrigin(100) - - // Replace the broadcaster and start the process. - scheduler := NewMockScheduler(RandomSignatory()) - processOrigin.Scheduler = scheduler - process := processOrigin.ToProcess() - process.Start() - - // Store state for later use. - stateBytes, err := surge.ToBinary(process) - Expect(err).ToNot(HaveOccurred()) - - // Expect the validator to broadcast a nil prevote message. - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok := message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the proposer broadcast a propose message with zero height and round - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - prevote, ok := message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(block.Height(1))) - Expect(prevote.Round()).Should(BeZero()) - Expect(prevote.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - - By("after reboot") - - // Initialise a new process using the stored state and - // ensure it times out and broadcasts a nil prevote. - newProcessOrigin := NewProcessOrigin(100) - - state := DefaultState(100) - err = surge.FromBinary(stateBytes, &state) - Expect(err).ToNot(HaveOccurred()) - - newProcessOrigin.UpdateState(state) - - // Replace the broadcaster and start the new process. - newScheduler := NewMockScheduler(RandomSignatory()) - newProcessOrigin.Scheduler = newScheduler - newProcess := newProcessOrigin.ToProcess() - newProcess.Start() - - Eventually(newProcessOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok = message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the validator to broadcast a nil prevote message. - Eventually(newProcessOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - prevote, ok = message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(block.Height(1))) - Expect(prevote.Round()).Should(BeZero()) - Expect(prevote.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - }) - }) - }) - }) - - Context("when receive 2f + 1 prevote of a proposal at current height and round for the first time", func() { - Context("when the process is in prevote", func() { - It("should lock the proposal and round, and broadcast a precommit for it.", func() { - f := rand.Intn(100) + 1 - processOrigin := NewProcessOrigin(f) - - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin.State.CurrentStep = StepPrevote - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - process := processOrigin.ToProcess() - - // Handle the proposal - propose := NewPropose(height, round, RandomBlock(block.Standard), block.Round(rand.Intn(int(round)))) - Expect(Sign(propose, *privateKey)).Should(Succeed()) - process.HandleMessage(propose) - - // Send 2F +1 Prevote for this proposal - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, round, propose.BlockHash(), nil) - pk := newEcdsaKey() - Expect(Sign(prevote, *pk)).Should(Succeed()) - process.HandleMessage(prevote) - } - - // Expect the proposer broadcast a precommit message with - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - precommit, ok := message.(*Precommit) - Expect(ok).Should(BeTrue()) - Expect(precommit.Height()).Should(Equal(height)) - Expect(precommit.Round()).Should(Equal(round)) - Expect(precommit.BlockHash().Equal(propose.BlockHash())).Should(BeTrue()) - - // Expect the block is locked in the state - state := testutil.GetStateFromProcess(process, f) - Expect(state.LockedBlock.Equal(propose.Block())).Should(BeTrue()) - Expect(state.LockedRound).Should(Equal(round)) - Expect(state.ValidBlock.Equal(propose.Block())).Should(BeTrue()) - Expect(state.ValidRound).Should(Equal(round)) - }) - }) - - Context("when the process is in the precommit step", func() { - Context("when it receives 2*f+1 precommits for any proposal for the first time", func() { - It("should move to the next round if no consensus is reached within the timeout", func() { - By("before reboot") - - // Initialise a new process at the precommit step. - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentStep = StepPrecommit - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.Blockchain.InsertBlockAtHeight(height-1, RandomBlock(block.Standard)) - - process := processOrigin.ToProcess() - process.Start() - - // Handle random precommits. - for i := 0; i < 2*f+1; i++ { - precommit := NewPrecommit(height, round, RandomBlock(RandomBlockKind()).Hash()) - privateKey := newEcdsaKey() - Expect(Sign(precommit, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(precommit) - } - - // Store state for later use. - stateBytes, err := surge.ToBinary(process) - Expect(err).ToNot(HaveOccurred()) - - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok := message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the validator to broadcast a propose and move to - // the next round. - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - propose, ok := message.(*Propose) - Expect(ok).Should(BeTrue()) - Expect(propose.Height()).Should(Equal(height)) - Expect(propose.Round()).Should(Equal(round + 1)) - - By("after reboot") - - // Initialise a new process using the stored state and - // ensure it times out and broadcasts a nil proposal. - newProcessOrigin := NewProcessOrigin(f) - newProcessOrigin.State.CurrentStep = StepPrecommit - newProcessOrigin.State.CurrentHeight = height - newProcessOrigin.State.CurrentRound = round - newProcessOrigin.Blockchain.InsertBlockAtHeight(height-1, RandomBlock(block.Standard)) - - state := DefaultState(f) - err = surge.FromBinary(stateBytes, &state) - Expect(err).ToNot(HaveOccurred()) - newProcessOrigin.UpdateState(state) - - newProcess := newProcessOrigin.ToProcess() - newProcess.Start() - - Eventually(newProcessOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok = message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the validator to broadcast a propose and move to - // the next round. - Eventually(newProcessOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - propose, ok = message.(*Propose) - Expect(ok).Should(BeTrue()) - Expect(propose.Height()).Should(Equal(height)) - Expect(propose.Round()).Should(Equal(round + 1)) - }) - }) - - It("should put the proposal in the validBlock", func() { - f := rand.Intn(100) + 1 - processOrigin := NewProcessOrigin(f) - - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin.State.CurrentStep = StepPrecommit - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - process := processOrigin.ToProcess() - - // Handle the proposal - propose := NewPropose(height, round, RandomBlock(block.Standard), block.Round(rand.Intn(int(round)))) - Expect(Sign(propose, *privateKey)).Should(Succeed()) - process.HandleMessage(propose) - - // Send 2F +1 Prevote for this proposal - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, round, propose.BlockHash(), nil) - pk := newEcdsaKey() - Expect(Sign(prevote, *pk)).Should(Succeed()) - process.HandleMessage(prevote) - } - - // Expect the block is locked in the state - state := testutil.GetStateFromProcess(process, f) - Expect(state.LockedBlock.Equal(processOrigin.State.LockedBlock)).Should(BeTrue()) - Expect(state.LockedRound).Should(Equal(processOrigin.State.LockedRound)) - Expect(state.ValidBlock.Equal(propose.Block())).Should(BeTrue()) - Expect(state.ValidRound).Should(Equal(round)) - }) - }) - }) - - Context("when the process is in the prevote step", func() { - Context("when it receives 2*f+1 prevotes for any proposal for the first time", func() { - It("should send a nil precommit if no consensus is reached within the timeout", func() { - By("before reboot") - - // Initialise a new process at the prevote step. - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentStep = StepPrevote - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - process := processOrigin.ToProcess() - process.Start() - - // Handle random prevotes. - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, round, RandomBlock(RandomBlockKind()).Hash(), nil) - privateKey := newEcdsaKey() - Expect(Sign(prevote, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(prevote) - } - - // Store state for later use. - stateBytes, err := surge.ToBinary(process) - Expect(err).ToNot(HaveOccurred()) - - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok := message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the validator to broadcast a nil precommit. - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - precommit, ok := message.(*Precommit) - Expect(ok).Should(BeTrue()) - Expect(precommit.Height()).Should(Equal(height)) - Expect(precommit.Round()).Should(Equal(round)) - Expect(precommit.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - - By("after reboot") - - // Initialise a new process using the stored state and - // ensure it times out and broadcasts a nil precommit. - newProcessOrigin := NewProcessOrigin(100) - newProcessOrigin.State.CurrentStep = StepPrevote - newProcessOrigin.State.CurrentHeight = height - newProcessOrigin.State.CurrentRound = round - - state := DefaultState(100) - err = surge.FromBinary(stateBytes, &state) - Expect(err).ToNot(HaveOccurred()) - newProcessOrigin.UpdateState(state) - - newProcess := newProcessOrigin.ToProcess() - newProcess.Start() - - Eventually(newProcessOrigin.BroadcastMessages).Should(Receive(&message)) - _, ok = message.(*Resync) - Expect(ok).Should(BeTrue()) - - // Expect the validator to broadcast a nil precommit message. - Eventually(newProcessOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - precommit, ok = message.(*Precommit) - Expect(ok).Should(BeTrue()) - Expect(precommit.Height()).Should(Equal(height)) - Expect(precommit.Round()).Should(Equal(round)) - Expect(precommit.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - }) - }) - - Context("when it receives 2*f+1 nil prevotes for the current height and round", func() { - It("should broadcast a nil precommit and move to the precommit step", func() { - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentStep = StepPrevote - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - process := processOrigin.ToProcess() - - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, round, block.InvalidHash, nil) - privateKey := newEcdsaKey() - Expect(Sign(prevote, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(prevote) - } - - // Expect the proposer broadcast a precommit message with - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - precommit, ok := message.(*Precommit) - Expect(ok).Should(BeTrue()) - Expect(precommit.Height()).Should(Equal(height)) - Expect(precommit.Round()).Should(Equal(round)) - Expect(precommit.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - }) - }) - }) - - Context("when the process receives at least 2*f+1 precommits", func() { - Context("when starting a timer before executing the OnTimeoutPrecommit function", func() { - It("should start a round when nothing changes after the timeout", func() { - for _, step := range []Step{StepPropose, StepPrevote, StepPrecommit} { - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()) - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentStep = step - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.Blockchain.InsertBlockAtHeight(height-1, RandomBlock(block.Standard)) - process := processOrigin.ToProcess() - - for i := 0; i < 2*f+1; i++ { - precommit := NewPrecommit(height, round, RandomBlock(RandomBlockKind()).Hash()) - privateKey := newEcdsaKey() - Expect(Sign(precommit, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(precommit) - } - - // Expect the proposer broadcast a propose message with zero height and round - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - proposal, ok := message.(*Propose) - Expect(ok).Should(BeTrue()) - Expect(proposal.Height()).Should(Equal(height)) - Expect(proposal.Round()).Should(Equal(round + 1)) - - state := testutil.GetStateFromProcess(process, f) - Expect(state.CurrentRound).Should(Equal(round + 1)) - Expect(state.CurrentStep).Should(Equal(StepPropose)) - } - }) - }) - }) - - Context("when receiving f+1 of any message whose round is higher", func() { - It("should start that round", func() { - for _, t := range []MessageType{ - // NOTE: You should only ever receive 1 Propose for a height - // and round, so this test is not meaningful for Propose - // messages. - // - // ProposeMessageType, - // - PrevoteMessageType, - PrecommitMessageType, - } { - messageType := t - // Init a default process to be modified - f := rand.Intn(100) + 1 - height, round := RandomHeight(), RandomRound() - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - - // Replace the broadcaster and start the process - scheduler := NewMockScheduler(RandomSignatory()) - processOrigin.Scheduler = scheduler - process := processOrigin.ToProcess() - - // Send f + 1 message with higher round to the process - newRound := block.Round(rand.Intn(10)+1) + round - for i := 0; i < f+1; i++ { - message := RandomMessageWithHeightAndRound(height, newRound, messageType) - privateKey := newEcdsaKey() - Expect(Sign(message, *privateKey)).Should(Succeed()) - process.HandleMessage(message) - } - - // Expect the proposer broadcast a propose message with zero height and round - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - prevote, ok := message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(height)) - Expect(prevote.Round()).Should(Equal(newRound)) - Expect(prevote.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - } - }) - }) - - Context("when process in propose state", func() { - Context("when receive a proposal with a non-zero valid round and the valid round is less than current round", func() { - Context("when receive at least 2f+1 prevote of the proposal.", func() { - Context("when the proposal is valid ", func() { - Context("when lockedRound is less than or equal to the valid round", func() { - It("should broadcast a prevote to the proposal", func() { - // Init a default process to be modified - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()+1) // Round needs to be great than 0 - validRound := block.Round(rand.Intn(int(round))) - - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.State.CurrentStep = StepPropose - processOrigin.State.LockedRound = block.Round(rand.Intn(int(validRound))) - process := processOrigin.ToProcess() - - // Send the proposal - propose := NewPropose(height, round, RandomBlock(RandomBlockKind()), validRound) - Expect(Sign(propose, *processOrigin.PrivateKey)).Should(Succeed()) - process.HandleMessage(propose) - - // Send 2f + 1 prevotes - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, validRound, propose.BlockHash(), nil) - privateKey := newEcdsaKey() - Expect(Sign(prevote, *privateKey)).Should(Succeed()) - process.HandleMessage(prevote) - } - - // Expect the process broadcast a nil prevote - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - prevote, ok := message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(height)) - Expect(prevote.Round()).Should(Equal(round)) - Expect(prevote.BlockHash().Equal(propose.BlockHash())).Should(BeTrue()) - - // Step should be moved to prevote - state := testutil.GetStateFromProcess(process, f) - Expect(state.CurrentStep).Should(Equal(StepPrevote)) - }) - }) - - Context("when the proposed block is same as the locked block", func() { - It("should broadcast a prevote to the proposal", func() { - // Init a default process to be modified - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()+1) // Round needs to be great than 0 - validRound := block.Round(rand.Intn(int(round))) - - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.State.CurrentStep = StepPropose - processOrigin.State.LockedRound = validRound + 1 // make sure lockedRound is greater than the valid round - block := RandomBlock(RandomBlockKind()) - propose := NewPropose(height, round, block, validRound) - Expect(Sign(propose, *processOrigin.PrivateKey)).Should(Succeed()) - processOrigin.State.LockedBlock = block - - // Send the proposal - process := processOrigin.ToProcess() - process.HandleMessage(propose) - - // Send 2f + 1 prevotes - for i := 0; i < 2*f+1; i++ { - prevote := NewPrevote(height, validRound, propose.BlockHash(), nil) - privateKey := newEcdsaKey() - Expect(Sign(prevote, *privateKey)).To(Succeed()) - process.HandleMessage(prevote) - } - - // Expect the process broadcast prevote - var message Message - Eventually(processOrigin.BroadcastMessages, time.Second).Should(Receive(&message)) - prevote, ok := message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(height)) - Expect(prevote.Round()).Should(Equal(round)) - Expect(prevote.BlockHash().Equal(propose.BlockHash())).Should(BeTrue()) - - // Step should be moved to prevote - state := testutil.GetStateFromProcess(process, f) - Expect(state.CurrentStep).Should(Equal(StepPrevote)) - }) - }) - }) - - Context("when the proposal is invalid", func() { - It("should broadcast a nil prevote", func() { - // Init a default process to be modified - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()+1) // Round needs to be great than 0 - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.State.CurrentStep = StepPropose - processOrigin.Validator = NewMockValidator(fmt.Errorf("")) - process := processOrigin.ToProcess() - - // Send the proposal - propose := NewPropose(height, round, block.InvalidBlock, block.InvalidRound) - Expect(Sign(propose, *processOrigin.PrivateKey)).Should(Succeed()) - process.HandleMessage(propose) - - // Expect the process broadcast a nil prevote - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - prevote, ok := message.(*Prevote) - Expect(ok).Should(BeTrue()) - Expect(prevote.Height()).Should(Equal(height)) - Expect(prevote.Round()).Should(Equal(round)) - Expect(prevote.BlockHash().Equal(block.InvalidHash)).Should(BeTrue()) - - // Step should be moved to prevote - state := testutil.GetStateFromProcess(process, f) - Expect(state.CurrentStep).Should(Equal(StepPrevote)) - }) - }) - }) - }) - }) - - Context("when current block does not exist in the blockchain", func() { - Context("when receive 2f + 1 precommit of a proposal,", func() { - It("should finalize the block in blockchain, reset the state, and start from round 0 in height +1 ", func() { - for _, step := range []Step{StepPropose, StepPrevote, StepPrecommit} { - // Init a default process to be modified - f := rand.Intn(100) + 1 - height, round := block.Height(rand.Int()), block.Round(rand.Int()+1) // Round needs to be great than 0 - validRound := block.Round(rand.Intn(int(round + 1))) - - processOrigin := NewProcessOrigin(f) - processOrigin.State.CurrentHeight = height - processOrigin.State.CurrentRound = round - processOrigin.State.CurrentStep = step // step should not matter in this case. - processOrigin.State.LockedRound = block.Round(rand.Intn(int(validRound + 1))) - process := processOrigin.ToProcess() - - // Send the proposal - proposeRound := block.Round(rand.Intn(int(round + 1))) // if proposeRound > currentRound, it will start(proposeRound) - propose := NewPropose(height, proposeRound, RandomBlock(RandomBlockKind()), validRound) // round and valid round should not matter in this case - Expect(Sign(propose, *processOrigin.PrivateKey)).Should(Succeed()) - process.HandleMessage(propose) - - // Send 2f + 1 prevotes - for i := 0; i < 2*f+1; i++ { - precommit := NewPrecommit(height, proposeRound, propose.BlockHash()) - privateKey := newEcdsaKey() - Expect(Sign(precommit, *privateKey)).Should(Succeed()) - process.HandleMessage(precommit) - } - - // Expect process start a new round and start proposing - var message Message - Eventually(processOrigin.BroadcastMessages).Should(Receive(&message)) - - proposal, ok := message.(*Propose) - Expect(ok).Should(BeTrue()) - Expect(proposal.Height()).Should(Equal(height + 1)) - Expect(proposal.Round()).Should(BeZero()) - - // The proposal should be finalized in the blockchain storage. - Expect(processOrigin.Blockchain.BlockExistsAtHeight(height)).Should(BeTrue()) - - // Step should be reset and new height and 0 round - state := testutil.GetStateFromProcess(process, f) - Expect(state.CurrentHeight).Should(Equal(height + 1)) - Expect(state.CurrentRound).Should(BeZero()) - Expect(state.CurrentStep).Should(Equal(StepPropose)) - Expect(state.LockedBlock).Should(Equal(block.InvalidBlock)) - Expect(state.LockedRound).Should(Equal(block.InvalidRound)) - Expect(state.ValidBlock).Should(Equal(block.InvalidBlock)) - Expect(state.ValidRound).Should(Equal(block.InvalidRound)) - } - }) - }) - }) - - Context("when starting the process", func() { - It("should send a resync message", func() { - processOrigin := NewProcessOrigin(100) - process := processOrigin.ToProcess() - process.Start() - - // Expect the process to broadcast a resync message. - var message Message - Eventually(processOrigin.BroadcastMessages, 2*time.Second).Should(Receive(&message)) - _, ok := message.(*Resync) - Expect(ok).Should(BeTrue()) - }) - - Context("when the process has messages from a previous height", func() { - It("should resend the most recent proposal, prevote, and precommit", func() { - processOrigin := NewProcessOrigin(100) - - propose := RandomPropose() - Expect(Sign(propose, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - prevote := NewPrevote(propose.Height(), propose.Round(), propose.BlockHash(), nil) - Expect(Sign(prevote, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - precommit := NewPrecommit(propose.Height(), propose.Round(), propose.BlockHash()) - Expect(Sign(precommit, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - - processOrigin.Blockchain.InsertBlockAtHeight(propose.Height(), propose.Block()) - processOrigin.State.CurrentHeight = propose.Height() + 1 - processOrigin.State.CurrentRound = 0 - processOrigin.State.Proposals.Insert(propose) - processOrigin.State.Prevotes.Insert(prevote) - processOrigin.State.Precommits.Insert(precommit) - process := processOrigin.ToProcess() - - done := make(chan struct{}) - resentProposal := false - resentPrevote := false - resentPrecommit := false - go func() { - defer close(done) - for m := range processOrigin.BroadcastMessages { - switch m.(type) { - case *Propose: - Expect(m).To(Equal(propose)) - resentProposal = true - case *Prevote: - Expect(m).To(Equal(prevote)) - resentPrevote = true - case *Precommit: - Expect(m).To(Equal(precommit)) - resentPrecommit = true - } - if resentProposal && resentPrevote && resentPrecommit { - return - } - } - }() - - go process.Start() - <-done - }) - }) - - Context("when the process has messages from a current height", func() { - It("should resend the most recent proposal, prevote, and precommit", func() { - processOrigin := NewProcessOrigin(100) - - propose := RandomPropose() - Expect(Sign(propose, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - prevote := NewPrevote(propose.Height(), propose.Round(), propose.BlockHash(), nil) - Expect(Sign(prevote, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - precommit := NewPrecommit(propose.Height(), propose.Round(), propose.BlockHash()) - Expect(Sign(precommit, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - - processOrigin.Blockchain.InsertBlockAtHeight(propose.Height()-1, RandomBlock(block.Standard)) - processOrigin.Blockchain.InsertBlockAtHeight(propose.Height(), propose.Block()) - processOrigin.State.CurrentHeight = propose.Height() - processOrigin.State.CurrentRound = propose.Round() + 1 - processOrigin.State.Proposals.Insert(propose) - processOrigin.State.Prevotes.Insert(prevote) - processOrigin.State.Precommits.Insert(precommit) - process := processOrigin.ToProcess() - - done := make(chan struct{}) - resentProposal := false - resentPrevote := false - resentPrecommit := false - go func() { - defer close(done) - for m := range processOrigin.BroadcastMessages { - switch m.(type) { - case *Propose: - Expect(m).To(Equal(propose)) - resentProposal = true - case *Prevote: - Expect(m).To(Equal(prevote)) - resentPrevote = true - case *Precommit: - Expect(m).To(Equal(precommit)) - resentPrecommit = true - } - if resentProposal && resentPrevote && resentPrecommit { - return - } - } - }() - - go process.Start() - <-done - }) - }) - }) - - Context("when the process receives a resync message", func() { - It("should broadcast latest messages to the sender", func() { - // Initialise a default process. - processOrigin := NewProcessOrigin(100) - - // Replace the scheduler. - privateKey := newEcdsaKey() - scheduler := NewMockScheduler(id.NewSignatory(privateKey.PublicKey)) - processOrigin.Scheduler = scheduler - - // Insert random messages. - propose := RandomPropose() - Expect(Sign(propose, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - prevote := NewPrevote(propose.Height(), propose.Round(), propose.BlockHash(), nil) - Expect(Sign(prevote, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - precommit := NewPrecommit(propose.Height(), propose.Round(), propose.BlockHash()) - Expect(Sign(precommit, *processOrigin.PrivateKey)).ToNot(HaveOccurred()) - - processOrigin.Blockchain.InsertBlockAtHeight(propose.Height(), propose.Block()) - processOrigin.State.CurrentHeight = propose.Height() + 1 - processOrigin.State.CurrentRound = 0 - processOrigin.State.Proposals.Insert(propose) - processOrigin.State.Prevotes.Insert(prevote) - processOrigin.State.Precommits.Insert(precommit) - - // Start the process. - process := processOrigin.ToProcess() - process.Start() - - // Handle a resync message. - message := NewResync(0, 0) - Expect(Sign(message, *privateKey)).NotTo(HaveOccurred()) - process.HandleMessage(message) - - // Ensure the process broadcasts latest messages. - done := make(chan struct{}) - resentProposal := false - resentPrevote := false - resentPrecommit := false - go func() { - defer close(done) - for m := range processOrigin.BroadcastMessages { - switch m.(type) { - case *Propose: - Expect(m).To(Equal(propose)) - resentProposal = true - case *Prevote: - Expect(m).To(Equal(prevote)) - resentPrevote = true - case *Precommit: - Expect(m).To(Equal(precommit)) - resentPrecommit = true - } - if resentProposal && resentPrevote && resentPrecommit { - return - } - } - }() - - go process.Start() - <-done - }) - }) -}) diff --git a/process/state.go b/process/state.go index 936cc0e3..7c6fa472 100644 --- a/process/state.go +++ b/process/state.go @@ -1,63 +1,230 @@ package process import ( - "github.com/renproject/hyperdrive/block" + "bytes" + "fmt" + "io" + + "github.com/renproject/id" + "github.com/renproject/surge" ) -// The State of a Process. It is isolated from the Process so that it can be -// easily marshaled to/from JSON. +// The State of a Process. It should be saved after every method call on the +// Process, but should not be saved during method calls (interacting with the +// State concurently is unsafe). It is worth noting that the State does not +// contain a decision array, because it delegates this responsibility to the +// Committer interface. +// +// L1: +// +// Initialization: +// currentHeight := 0 /* current height, or consensus instance we are currently executing */ +// currentRound := 0 /* current round number */ +// currentStep ∈ {propose, prevote, precommit} +// decision[] := nil +// lockedValue := nil +// lockedRound := −1 +// validValue := nil +// validRound := −1 type State struct { - CurrentHeight block.Height `json:"currentHeight"` - CurrentRound block.Round `json:"currentRound"` - CurrentStep Step `json:"currentStep"` - - LockedBlock block.Block `json:"lockedBlock"` // the most recent block for which a precommit message has been sent - LockedRound block.Round `json:"lockedRound"` // the last round in which the process sent a precommit message that is not nil. - ValidBlock block.Block `json:"validBlock"` // store the most recent possible decision value - ValidRound block.Round `json:"validRound"` // is the last round in which valid value is updated - - Proposals *Inbox `json:"proposals"` - Prevotes *Inbox `json:"prevotes"` - Precommits *Inbox `json:"precommits"` + CurrentHeight Height `json:"currentHeight"` + CurrentRound Round `json:"currentRound"` + CurrentStep Step `json:"currentStep"` + LockedValue Value `json:"lockedValue"` // The most recent value for which a precommit message has been sent. + LockedRound Round `json:"lockedRound"` // The last round in which the process sent a precommit message that is not nil. + ValidValue Value `json:"validValue"` // The most recent possible decision value. + ValidRound Round `json:"validRound"` // The last round in which valid value is updated. } -// DefaultState returns a State with all values set to the default. See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func DefaultState(f int) State { +// DefaultState returns a State with all fields set to their default values. The +// Height default to 1, because the genesis block is assumed to exist at Height +// 0. +func DefaultState() State { return State{ - CurrentHeight: 1, // Skip the genesis block + CurrentHeight: 1, // Skip genesis. CurrentRound: 0, - CurrentStep: StepPropose, - LockedBlock: block.InvalidBlock, - LockedRound: block.InvalidRound, - ValidBlock: block.InvalidBlock, - ValidRound: block.InvalidRound, - Proposals: NewInbox(f, ProposeMessageType), - Prevotes: NewInbox(f, PrevoteMessageType), - Precommits: NewInbox(f, PrecommitMessageType), - } -} - -// Reset the State (not all values are reset). See -// https://arxiv.org/pdf/1807.04938.pdf for more information. -func (state *State) Reset(height block.Height) { - state.LockedBlock = block.InvalidBlock - state.LockedRound = block.InvalidRound - state.ValidBlock = block.InvalidBlock - state.ValidRound = block.InvalidRound - state.Proposals.Reset(height) - state.Prevotes.Reset(height) - state.Precommits.Reset(height) -} - -// Equal compares one State with another. -func (state *State) Equal(other State) bool { + CurrentStep: Proposing, + LockedValue: NilValue, + LockedRound: InvalidRound, + ValidValue: NilValue, + ValidRound: InvalidRound, + } +} + +// Equal compares two States. If they are equal, then it returns true, otherwise +// it returns false. +func (state State) Equal(other *State) bool { return state.CurrentHeight == other.CurrentHeight && state.CurrentRound == other.CurrentRound && state.CurrentStep == other.CurrentStep && - state.LockedBlock.Equal(other.LockedBlock) && + state.LockedValue.Equal(&other.LockedValue) && state.LockedRound == other.LockedRound && - state.ValidBlock.Equal(other.ValidBlock) && + state.ValidValue.Equal(&other.ValidValue) && state.ValidRound == other.ValidRound } +func (state State) SizeHint() int { + return surge.SizeHint(state.CurrentHeight) + + surge.SizeHint(state.CurrentRound) + + surge.SizeHint(state.CurrentStep) + + surge.SizeHint(state.LockedValue) + + surge.SizeHint(state.LockedRound) + + surge.SizeHint(state.ValidValue) + + surge.SizeHint(state.ValidRound) +} + +func (state State) Marshal(w io.Writer, m int) (int, error) { + m, err := surge.Marshal(w, state.CurrentHeight, m) + if err != nil { + return m, fmt.Errorf("marshaling current height=%v: %v", state.CurrentHeight, err) + } + m, err = surge.Marshal(w, state.CurrentRound, m) + if err != nil { + return m, fmt.Errorf("marshaling current round=%v: %v", state.CurrentRound, err) + } + m, err = surge.Marshal(w, state.CurrentStep, m) + if err != nil { + return m, fmt.Errorf("marshaling current step=%v: %v", state.CurrentStep, err) + } + m, err = surge.Marshal(w, state.LockedValue, m) + if err != nil { + return m, fmt.Errorf("marshaling locked value=%v: %v", state.LockedValue, err) + } + m, err = surge.Marshal(w, state.LockedRound, m) + if err != nil { + return m, fmt.Errorf("marshaling locked round=%v: %v", state.LockedRound, err) + } + m, err = surge.Marshal(w, state.ValidValue, m) + if err != nil { + return m, fmt.Errorf("marshaling valid value=%v: %v", state.ValidValue, err) + } + m, err = surge.Marshal(w, state.ValidRound, m) + if err != nil { + return m, fmt.Errorf("marshaling valid round=%v: %v", state.ValidRound, err) + } + return m, nil +} + +func (state *State) Unmarshal(r io.Reader, m int) (int, error) { + m, err := surge.Unmarshal(r, &state.CurrentHeight, m) + if err != nil { + return m, fmt.Errorf("unmarshaling current height: %v", err) + } + m, err = surge.Unmarshal(r, &state.CurrentRound, m) + if err != nil { + return m, fmt.Errorf("unmarshaling current round: %v", err) + } + m, err = surge.Unmarshal(r, &state.CurrentStep, m) + if err != nil { + return m, fmt.Errorf("unmarshaling current step: %v", err) + } + m, err = surge.Unmarshal(r, &state.LockedValue, m) + if err != nil { + return m, fmt.Errorf("unmarshaling locked value: %v", err) + } + m, err = surge.Unmarshal(r, &state.LockedRound, m) + if err != nil { + return m, fmt.Errorf("unmarshaling locked round: %v", err) + } + m, err = surge.Unmarshal(r, &state.ValidValue, m) + if err != nil { + return m, fmt.Errorf("unmarshaling valid value: %v", err) + } + m, err = surge.Unmarshal(r, &state.ValidRound, m) + if err != nil { + return m, fmt.Errorf("unmarshaling valid round: %v", err) + } + return m, nil +} + +// Step defines a typedef for uint8 values that represent the step of the state +// of a Process partaking in the consensus algorithm. +type Step uint8 + +// Enumerate step values. +const ( + Proposing = Step(0) + Prevoting = Step(1) + Precommitting = Step(2) +) + +// Height defines a typedef for int64 values that represent the height of a +// Value at which the consensus algorithm is attempting to reach consensus. +type Height int64 + +// Round defines a typedef for int64 values that represent the round of a Value +// at which the consensus algorithm is attempting to reach consensus. +type Round int64 + +const ( + // InvalidRound is a reserved int64 that represents an invalid Round. It is + // used when a Process is trying to represent that it does have have a + // LockedRound or ValidRound. + InvalidRound = Round(-1) +) + +// Value defines a typedef for hashes that represent the hashes of proposed +// values in the consensus algorithm. In the context of a blockchain, a Value +// would be a block. +type Value id.Hash + +// Equal compares two Values. If they are equal, then it returns true, otherwise +// it returns false. +func (v *Value) Equal(other *Value) bool { + return bytes.Equal(v[:], other[:]) +} + +var ( + // NilValue is a reserved hash that represents when a Process is + // prevoting/precommitting to nothing (i.e. the Process wants to progress to + // the next Round). + NilValue = Value(id.Hash{}) +) + +// Pid defines a type alias for hashes that represent the unique identity of a +// Process in the consensus algorithm. No distrinct Processes should ever have +// the same Pid, and a Process must maintain the same Pid for its entire life. +type Pid = id.Signatory + +// Pids defines a typedef for a slice of Pids. +type Pids []Pid + +// Equal compares two slices of Pids. If they are equal, the it returns true, +// otherwise it returns false. +func (pids Pids) Equal(other Pids) bool { + if len(pids) != len(other) { + return false + } + for i := range pids { + if !pids[i].Equal(&other[i]) { + return false + } + } + return true +} + +// Contains checks for the existence of a Pid in the slice of Pids. If the Pid +// is in the slice, then it returns true, otherwise it returns false. The +// complexity of this method is O(n), so it is recommended that the results are +// cached whenever the Pids slice is large. +func (pids Pids) Contains(pid Pid) bool { + for i := range pids { + if pids[i].Equal(&pid) { + return true + } + } + return false +} + +// Set returns the slice of Pids, converted into a PidSet. This is convenient +// for checking the existence of Pids when ordering does not matter. +func (pids Pids) Set() PidSet { + set := PidSet{} + for _, pid := range pids { + set[pid] = true + } + return set +} + +// PidSet defines a typedef for a map of Pids. +type PidSet map[Pid]bool diff --git a/process/state_test.go b/process/state_test.go index 34e40542..05f732af 100644 --- a/process/state_test.go +++ b/process/state_test.go @@ -1,71 +1 @@ package process_test - -import ( - "testing/quick" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/process" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("States", func() { - Context("when marshaling", func() { - Context("when marshaling a random state", func() { - It("should equal itself after marshaling and then unmarshaling", func() { - test := func() bool { - state := RandomState() - data, err := surge.ToBinary(state) - Expect(err).NotTo(HaveOccurred()) - - var newState State - Expect(surge.FromBinary(data, &newState)).Should(Succeed()) - return state.Equal(newState) && newState.Equal(state) - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when resetting a state", func() { - It("should only reset the locked block, locked round, valid block and valid round", func() { - test := func() bool { - state := RandomState() - heightBeforeReset := state.CurrentHeight - roundBeforeReset := state.CurrentRound - stepBeforeReset := state.CurrentStep - - state.Reset(0) - - Expect(state.CurrentHeight).Should(Equal(heightBeforeReset)) - Expect(state.CurrentRound).Should(Equal(roundBeforeReset)) - Expect(state.CurrentStep).Should(Equal(stepBeforeReset)) - Expect(state.LockedBlock).Should(Equal(block.InvalidBlock)) - Expect(state.LockedRound).Should(Equal(block.InvalidRound)) - Expect(state.ValidBlock).Should(Equal(block.InvalidBlock)) - Expect(state.ValidRound).Should(Equal(block.InvalidRound)) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when resetting the default states", func() { - It("should return a state that is equal to the default ", func() { - test := func() bool { - state := DefaultState(10) - state.Reset(0) - - return state.Equal(DefaultState(10)) - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) -}) diff --git a/replica/broadcast.go b/replica/broadcast.go deleted file mode 100644 index 6ce21cfc..00000000 --- a/replica/broadcast.go +++ /dev/null @@ -1,76 +0,0 @@ -package replica - -import ( - "crypto/ecdsa" - "fmt" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/id" -) - -// A Broadcaster is used to send signed, shard-specific, Messages to one or all -// Replicas in the network. -// -// For the consensus algorithm to work correctly, it is assumed that all honest -// replicas will eventually deliver all messages to all other honest replicas. -// The specific message ordering is not important. In practice, the Prevote -// messages are the only messages that must guarantee delivery when guaranteeing -// correctness. -// -// For crash-resilience, the implementation of Broadcast should ensure that two -// different Propose messages are not broadcast for the same shard, height, and -// round. This can be done by storing Proposes (and checking for their -// existence) before sending the Propose to the network proper. -type Broadcaster interface { - Broadcast(Message) - Cast(id.Signatory, Message) -} - -type signer struct { - broadcaster Broadcaster - shard Shard - privKey ecdsa.PrivateKey -} - -// newSigner returns a `process.Broadcaster` that accepts `process.Messages`, -// signs them, associates them with a Shard, and re-broadcasts them. -func newSigner(broadcaster Broadcaster, shard Shard, privKey ecdsa.PrivateKey) process.Broadcaster { - return &signer{ - broadcaster: broadcaster, - shard: shard, - privKey: privKey, - } -} - -// Broadcast implements the `process.Broadcaster` interface. -func (broadcaster *signer) Broadcast(m process.Message) { - if err := process.Sign(m, broadcaster.privKey); err != nil { - panic(fmt.Errorf("invariant violation: error broadcasting message: %v", err)) - } - broadcaster.broadcaster.Broadcast(SignMessage(m, broadcaster.shard, broadcaster.privKey)) -} - -// Cast implements the `process.Broadcaster` interface. -func (broadcaster *signer) Cast(to id.Signatory, m process.Message) { - if err := process.Sign(m, broadcaster.privKey); err != nil { - panic(fmt.Errorf("invariant violation: error broadcasting message: %v", err)) - } - broadcaster.broadcaster.Cast(to, SignMessage(m, broadcaster.shard, broadcaster.privKey)) -} - -// SignMessage with the Shard included. It is assumed that the `process.Message` -// is already signed. -func SignMessage(m process.Message, shard Shard, privKey ecdsa.PrivateKey) Message { - mWithShard := Message{ - Message: m, - Shard: shard, - } - mWithShardHash := mWithShard.SigHash() - signature, err := crypto.Sign(mWithShardHash[:], &privKey) - if err != nil { - panic(fmt.Errorf("invariant violation: error broadcasting message: %v", err)) - } - copy(mWithShard.Signature[:], signature) - return mWithShard -} diff --git a/replica/broadcast_test.go b/replica/broadcast_test.go deleted file mode 100644 index 94741055..00000000 --- a/replica/broadcast_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package replica - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rand" - "testing/quick" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/testutil" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/id" -) - -type mockBroadcaster struct { - broadcastMessages chan<- Message - castMessages chan<- Message -} - -func (m *mockBroadcaster) Broadcast(message Message) { - m.broadcastMessages <- message -} - -func (m *mockBroadcaster) Cast(to id.Signatory, message Message) { - m.castMessages <- message -} - -func newMockBroadcaster() (Broadcaster, chan Message, chan Message) { - broadcastMessages := make(chan Message, 1) - castMessages := make(chan Message, 1) - return &mockBroadcaster{ - broadcastMessages: broadcastMessages, - castMessages: castMessages, - }, broadcastMessages, castMessages -} - -var _ = Describe("Broadcaster", func() { - Context("when broadcasting a message", func() { - It("should sign the message and then broadcast it", func() { - test := func(shard Shard) bool { - key, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) - Expect(err).ToNot(HaveOccurred()) - broadcaster, broadcastMessages, _ := newMockBroadcaster() - signer := newSigner(broadcaster, shard, *key) - - msg := RandomMessage(RandomMessageType(true)) - signer.Broadcast(msg) - - var message Message - Eventually(broadcastMessages, 2*time.Second).Should(Receive(&message)) - Expect(bytes.Equal(message.Shard[:], shard[:])).Should(BeTrue()) - Expect(process.Verify(message.Message)).Should(Succeed()) - - return true - - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when casting a message", func() { - It("should sign the message and then cast it", func() { - test := func(shard Shard) bool { - key, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) - Expect(err).ToNot(HaveOccurred()) - broadcaster, _, castMessages := newMockBroadcaster() - signer := newSigner(broadcaster, shard, *key) - - msg := RandomMessage(RandomMessageType(true)) - signer.Cast(id.Signatory{}, msg) - - var message Message - Eventually(castMessages, 2*time.Second).Should(Receive(&message)) - Expect(bytes.Equal(message.Shard[:], shard[:])).Should(BeTrue()) - Expect(process.Verify(message.Message)).Should(Succeed()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) -}) diff --git a/replica/marshal.go b/replica/marshal.go deleted file mode 100644 index 47e5c4b3..00000000 --- a/replica/marshal.go +++ /dev/null @@ -1,94 +0,0 @@ -package replica - -import ( - "bytes" - "crypto/sha256" - "fmt" - "io" - - "github.com/renproject/hyperdrive/process" - "github.com/renproject/id" - "github.com/renproject/surge" -) - -// SigHash of the Message including the Shard. -func (message Message) SigHash() id.Hash { - m := surge.MaxBytes - buf := new(bytes.Buffer) - buf.Grow(surge.SizeHint(message.Message.Type()) + - surge.SizeHint(message.Message) + - surge.SizeHint(message.Shard)) - m, err := surge.Marshal(buf, uint64(message.Message.Type()), m) - if err != nil { - panic(fmt.Errorf("bad message sighash: bad marshal: %v", err)) - } - if m, err = surge.Marshal(buf, message.Message, m); err != nil { - panic(fmt.Errorf("bad message sighash: bad marshal: %v", err)) - } - if m, err = surge.Marshal(buf, message.Shard, m); err != nil { - panic(fmt.Errorf("bad message sighash: bad marshal: %v", err)) - } - return id.Hash(sha256.Sum256(buf.Bytes())) -} - -// SizeHint returns the number of bytes requires to store this message in -// binary. -func (message Message) SizeHint() int { - return surge.SizeHint(message.Message.Type()) + - surge.SizeHint(message.Message) + - surge.SizeHint(message.Shard) + - surge.SizeHint(message.Signature) -} - -// Marshal this message into binary. -func (message Message) Marshal(w io.Writer, m int) (int, error) { - m, err := surge.Marshal(w, uint64(message.Message.Type()), m) - if err != nil { - return m, err - } - if m, err = surge.Marshal(w, message.Message, m); err != nil { - return m, err - } - if m, err = surge.Marshal(w, message.Shard, m); err != nil { - return m, err - } - return surge.Marshal(w, message.Signature, m) -} - -// Unmarshal into this message from binary. -func (message *Message) Unmarshal(r io.Reader, m int) (int, error) { - var messageType process.MessageType - m, err := surge.Unmarshal(r, &messageType, m) - if err != nil { - return m, err - } - - switch messageType { - case process.ProposeMessageType: - propose := new(process.Propose) - m, err = propose.Unmarshal(r, m) - message.Message = propose - case process.PrevoteMessageType: - prevote := new(process.Prevote) - m, err = prevote.Unmarshal(r, m) - message.Message = prevote - case process.PrecommitMessageType: - precommit := new(process.Precommit) - m, err = precommit.Unmarshal(r, m) - message.Message = precommit - case process.ResyncMessageType: - resync := new(process.Resync) - m, err = resync.Unmarshal(r, m) - message.Message = resync - default: - return m, fmt.Errorf("unexpected message type %d", messageType) - } - if err != nil { - return m, err - } - - if m, err = surge.Unmarshal(r, &message.Shard, m); err != nil { - return m, err - } - return surge.Unmarshal(r, &message.Signature, m) -} diff --git a/replica/marshal_test.go b/replica/marshal_test.go deleted file mode 100644 index 5ec442e0..00000000 --- a/replica/marshal_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package replica_test - -import ( - "github.com/renproject/surge" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/replica" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Marshaling", func() { - Context("when marshaling and unmarshaling a message", func() { - It("should equal itself after binary marshaling/unmarshaling", func() { - for i := 0; i < 10; i++ { - message := Message{ - Message: RandomMessage(RandomMessageType(true)), - Shard: Shard{}, - } - messageBytes, err := surge.ToBinary(message) - Expect(err).ToNot(HaveOccurred()) - - var newMessage Message - Expect(surge.FromBinary(messageBytes, &newMessage)).To(Succeed()) - Expect(newMessage).To(Equal(message)) - } - }) - }) -}) diff --git a/replica/mq.go b/replica/mq.go deleted file mode 100644 index c2970b07..00000000 --- a/replica/mq.go +++ /dev/null @@ -1,166 +0,0 @@ -package replica - -import ( - "fmt" - "sort" - "sync" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/id" -) - -// A MessageQueue is used to queue messages, and order them by height. It will -// drop messages that are too far in the future. This helps protect honest -// Replicas from being spammed by malicious Replicas with lots of "future" -// messages in an attempt to waste processing time. It also makes the Process -// easier to analyse, because it can assume that all messages will come in for -// height H before height >H. -// -// The MessageQueue is not expected to protect against malicious Replicas that -// spam lots of "future" rounds in any given height (this must be done -// externally to the MessageQueue), and it is not expected to protect against -// malicious Replicas that spam lots of "past" heights. -type MessageQueue interface { - Push(process.Message) - PopUntil(block.Height) []process.Message - Peek() block.Height - Len() int -} - -type messageQueue struct { - // mu protects the entire message queue. - mu *sync.Mutex - - // once is used to make sure that no signatory sends multiple messages with - // the same type at the same height and round. - once map[process.MessageType]map[block.Height]map[block.Round]map[id.Signatory]bool - - // queue stores all messages, ordered first by height and then by round. - queue []process.Message - queueMax int -} - -// NewMessageQueue returns a new MessageQueue with a maximum capacity. Above -// this capacity, messages with the greater height will be dropped. -func NewMessageQueue(cap int) MessageQueue { - if cap <= 0 { - panic(fmt.Sprintf("message queue capacity too low: expected cap>0, got cap=%v", cap)) - } - return &messageQueue{ - mu: new(sync.Mutex), - once: map[process.MessageType]map[block.Height]map[block.Round]map[id.Signatory]bool{}, - queue: make([]process.Message, 0, cap), - queueMax: cap, - } -} - -func (mq *messageQueue) Push(message process.Message) { - mq.mu.Lock() - defer mq.mu.Unlock() - - if _, ok := mq.once[message.Type()][message.Height()][message.Round()][message.Signatory()]; ok { - // Ignore messages that appear in the "once" filter to protect against - // malicious duplicates. - return - } - - // If the queue is at maximum capacity, then we need to make room for the - // new message, or ignore the new message. - if len(mq.queue) >= mq.queueMax { - if mq.queue[len(mq.queue)-1].Height() < message.Height() { - // Drop the new message because it is too far in the future. - return - } - if mq.queue[len(mq.queue)-1].Height() == message.Height() { - if mq.queue[len(mq.queue)-1].Round() <= message.Round() { - // Drop the new message because it is too far in the future. We - // use the "or equals" check here to favour messages that are - // already in the queue. - return - } - } - // Truncate the queue to make room for the newer message. - mq.queue = mq.queue[:len(mq.queue)-1] - } - - // Write the message to the "once" filter to protect against malicious - // duplicates. - if _, ok := mq.once[message.Type()]; !ok { - mq.once[message.Type()] = map[block.Height]map[block.Round]map[id.Signatory]bool{} - } - if _, ok := mq.once[message.Type()][message.Height()]; !ok { - mq.once[message.Type()][message.Height()] = map[block.Round]map[id.Signatory]bool{} - } - if _, ok := mq.once[message.Type()][message.Height()][message.Round()]; !ok { - mq.once[message.Type()][message.Height()][message.Round()] = map[id.Signatory]bool{} - } - mq.once[message.Type()][message.Height()][message.Round()][message.Signatory()] = true - - // Find a place to insert the message in the queue. - insertAt := sort.Search(len(mq.queue), func(i int) bool { - if message.Height() < mq.queue[i].Height() { - return true - } - if message.Height() > mq.queue[i].Height() { - return false - } - return message.Round() < mq.queue[i].Round() - }) - mq.queue = append(mq.queue[:insertAt], append([]process.Message{message}, mq.queue[insertAt:]...)...) -} - -// PopUntil returns all messages up to, and including, the given height. -func (mq *messageQueue) PopUntil(height block.Height) []process.Message { - mq.mu.Lock() - defer mq.mu.Unlock() - - // There is nothing to return if the queue is empty. - if len(mq.queue) == 0 || mq.queue[0].Height() > height { - return []process.Message{} - } - - // Store the beginning and end heights that will be dropped from the queue - // so that, at the end, we can drop them from the "once" filter. - beginHeight := mq.queue[0].Height() - endHeight := height - - // Avoid allocations by returning the front end of the queue. - n := len(mq.queue) - for i, message := range mq.queue { - if message.Height() > height { - n = i - break - } - } - messages := mq.queue[:n] - mq.queue = mq.queue[n:] - - // Drop all space in the filter. - for h := beginHeight; h < endHeight; h++ { - delete(mq.once[process.ProposeMessageType], h) - delete(mq.once[process.PrevoteMessageType], h) - delete(mq.once[process.PrecommitMessageType], h) - delete(mq.once[process.ResyncMessageType], h) - } - return messages -} - -// Peek returns the smallest height in the queue. If there are no messages in -// the queue, it returns an invalid height. -func (mq *messageQueue) Peek() block.Height { - mq.mu.Lock() - defer mq.mu.Unlock() - - if len(mq.queue) == 0 { - return block.InvalidHeight - } - return mq.queue[0].Height() -} - -func (mq *messageQueue) Len() int { - mq.mu.Lock() - defer mq.mu.Unlock() - - return len(mq.queue) -} diff --git a/replica/mq_test.go b/replica/mq_test.go deleted file mode 100644 index 66d8760e..00000000 --- a/replica/mq_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package replica_test - -import ( - "math/rand" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/replica" - "github.com/renproject/hyperdrive/testutil" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Message queue", func() { - Context("when initialising", func() { - Context("when capacity is less than or equal to zero", func() { - It("should panic", func() { - Expect(func() { replica.NewMessageQueue(-1) }).To(Panic()) - Expect(func() { replica.NewMessageQueue(0) }).To(Panic()) - }) - }) - }) - - Context("when pushing messages", func() { - Context("when peeking", func() { - It("should return the lowest height", func() { - mq := replica.NewMessageQueue(100) - minHeight := block.Height(1<<63 - 1) - for i := 0; i < 100; i++ { - m := testutil.RandomMessage(testutil.RandomMessageType(true)) - if m.Height() < minHeight { - minHeight = m.Height() - } - mq.Push(m) - } - Expect(mq.Peek()).To(Equal(minHeight)) - }) - }) - - It("should pop messages in height order", func() { - mq := replica.NewMessageQueue(100) - for i := 0; i < 100; i++ { - m := testutil.RandomMessageWithHeightAndRound(block.Height(i+1), block.Round(rand.Int63()), testutil.RandomMessageType(true)) - mq.Push(m) - } - messages := mq.PopUntil(50) - Expect(messages).To(HaveLen(50)) - Expect(messages[0].Height()).To(Equal(block.Height(1))) - Expect(messages[len(messages)-1].Height()).To(Equal(block.Height(50))) - - for i := range messages { - if i < len(messages)-1 { - Expect(messages[i].Height() < messages[i+1].Height()).To(BeTrue()) - } - } - }) - - It("should not insert the same message more than once", func() { - mq := replica.NewMessageQueue(100) - for i := 0; i < 100; i++ { - m := testutil.RandomMessageWithHeightAndRound(block.Height(i+1), block.Round(rand.Int63()), testutil.RandomMessageType(true)) - mq.Push(m) - mq.Push(m) - mq.Push(m) - } - Expect(mq.Len()).To(Equal(100)) - }) - - Context("when capacity is full", func() { - Context("when new messages are higher", func() { - It("should drop the biggest heights", func() { - mq := replica.NewMessageQueue(100) - // Insert too many messages. - for i := 0; i < 200; i++ { - m := testutil.RandomMessageWithHeightAndRound(block.Height(i+1), block.Round(rand.Int63()), testutil.RandomMessageType(true)) - mq.Push(m) - } - // Expect the length to be equal to the initial capacity. - Expect(mq.Len()).To(Equal(100)) - - // Pop the first 50 messages and expect them to have the first - // 50 heights, indicating that the highest 100 messages were - // dropped. - messages := mq.PopUntil(50) - Expect(messages).To(HaveLen(50)) - Expect(messages[0].Height()).To(Equal(block.Height(1))) - Expect(messages[len(messages)-1].Height()).To(Equal(block.Height(50))) - - for i := range messages { - if i < len(messages)-1 { - Expect(messages[i].Height() < messages[i+1].Height()).To(BeTrue()) - } - } - }) - }) - - Context("when new messages are lower", func() { - It("should drop the biggest heights", func() { - mq := replica.NewMessageQueue(100) - // Insert too many messages. - for i := 200; i >= 0; i-- { - m := testutil.RandomMessageWithHeightAndRound(block.Height(i+1), block.Round(rand.Int63()), testutil.RandomMessageType(true)) - mq.Push(m) - } - // Expect the length to be equal to the initial capacity. - Expect(mq.Len()).To(Equal(100)) - - // Pop the first 50 messages and expect them to have the first - // 50 heights, indicating that the highest 100 messages were - // dropped. - messages := mq.PopUntil(50) - Expect(messages).To(HaveLen(50)) - Expect(messages[0].Height()).To(Equal(block.Height(1))) - Expect(messages[len(messages)-1].Height()).To(Equal(block.Height(50))) - - for i := range messages { - if i < len(messages)-1 { - Expect(messages[i].Height() < messages[i+1].Height()).To(BeTrue()) - } - } - }) - }) - }) - }) - - Context("when peeking", func() { - Context("when the queue is empty", func() { - It("should return an invalid height", func() { - mq := replica.NewMessageQueue(100) - Expect(mq.Peek()).To(Equal(block.InvalidHeight)) - }) - }) - }) -}) diff --git a/replica/opt.go b/replica/opt.go new file mode 100644 index 00000000..e4250f20 --- /dev/null +++ b/replica/opt.go @@ -0,0 +1,49 @@ +package replica + +import ( + "io" + + "github.com/renproject/hyperdrive/mq" + "github.com/renproject/hyperdrive/timer" + "github.com/sirupsen/logrus" +) + +type Options struct { + Logger logrus.FieldLogger + TimerOpts timer.Options + MessageQueueOpts mq.Options +} + +func DefaultOptions() Options { + return Options{ + Logger: loggerWithFields(logrus.New()), + TimerOpts: timer.DefaultOptions(), + MessageQueueOpts: mq.DefaultOptions(), + } +} + +func (opts Options) WithLogLevel(level logrus.Level) Options { + logger := logrus.New() + logger.SetLevel(level) + opts.Logger = loggerWithFields(logger) + return opts +} + +func (opts Options) WithLogOutput(output io.Writer) Options { + logger := logrus.New() + logger.SetOutput(output) + opts.Logger = loggerWithFields(logger) + return opts +} + +func (opts Options) WithTimerOptions(timerOpts timer.Options) Options { + opts.TimerOpts = timerOpts + return opts +} + +func loggerWithFields(logger *logrus.Logger) logrus.FieldLogger { + return logger. + WithField("lib", "hyperdrive"). + WithField("pkg", "replica"). + WithField("com", "replica") +} diff --git a/replica/opt_test.go b/replica/opt_test.go new file mode 100644 index 00000000..01608c1a --- /dev/null +++ b/replica/opt_test.go @@ -0,0 +1 @@ +package replica_test diff --git a/replica/rebase.go b/replica/rebase.go deleted file mode 100644 index 6f6c31b2..00000000 --- a/replica/rebase.go +++ /dev/null @@ -1,279 +0,0 @@ -package replica - -import ( - "fmt" - "sync" - "time" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/schedule" - "github.com/renproject/id" -) - -// BlockStorage extends the `process.Blockchain` interface with the -// functionality to load the last committed `block.Standard`, and the last -// committed `block.Base`. -type BlockStorage interface { - Blockchain(shard Shard) process.Blockchain - LatestBlock(shard Shard) block.Block - LatestBaseBlock(shard Shard) block.Block -} - -type BlockIterator interface { - // NextBlock returns the `block.Txs`, `block.Plan` and the parent - // `block.State` for the given `block.Height`. - NextBlock(block.Kind, block.Height, Shard) (block.Txs, block.Plan, block.State) - - // MissedBaseBlocksInRange must return an upper bound estimate for the - // number of missed base blocks between (exclusive) two blocks (identified - // by their block hash). This is used to prevent forking by old signatories. - // This function should not include the "end" block as a missed block if it - // is a rebasing Propose as if this function is being called, it has clearly - // not been missed. For example, if the range is heights 10 - 20 and there - // was expected to be a base block at 18, then this function is expected to - // return 1. However, if there was expected to be a block at height 20, - // then this function would return 0. - MissedBaseBlocksInRange(begin, end id.Hash) int -} - -type Validator interface { - IsBlockValid(block block.Block, checkHistory bool, shard Shard) (process.NilReasons, error) -} - -type Observer interface { - DidCommitBlock(block.Height, Shard) - DidReceiveSufficientNilPrevotes(messages process.Messages, f int) - IsSignatory(Shard) bool -} - -type shardRebaser struct { - mu *sync.Mutex - - expectedKind block.Kind - expectedRebaseSigs id.Signatories - numSignatories int - - scheduler schedule.Scheduler - blockStorage BlockStorage - blockIterator BlockIterator - validator Validator - observer Observer - shard Shard -} - -func newShardRebaser(scheduler schedule.Scheduler, blockStorage BlockStorage, blockIterator BlockIterator, validator Validator, observer Observer, shard Shard, numSignatories int) *shardRebaser { - return &shardRebaser{ - mu: new(sync.Mutex), - - expectedKind: block.Standard, - expectedRebaseSigs: nil, - numSignatories: numSignatories, - - scheduler: scheduler, - blockStorage: blockStorage, - blockIterator: blockIterator, - validator: validator, - observer: observer, - shard: shard, - } -} - -func (rebaser *shardRebaser) BlockProposal(height block.Height, round block.Round) block.Block { - rebaser.mu.Lock() - defer rebaser.mu.Unlock() - - parent := rebaser.blockStorage.LatestBlock(rebaser.shard) - base := rebaser.blockStorage.LatestBaseBlock(rebaser.shard) - - // Check that the base `block.Block` is a valid - if base.Header().Kind() != block.Base { - panic(fmt.Errorf("invariant violation: latest base block=%v has unexpected kind=%v", base.Hash(), base.Header().Kind())) - } - if base.Header().Signatories() == nil || len(base.Header().Signatories()) == 0 { - panic(fmt.Errorf("invariant violation: latest base block=%v has unexpected empty signatories", base.Hash())) - } - - var expectedSigs id.Signatories - - switch rebaser.expectedKind { - case block.Standard: - // Standard `block.Blocks` must not propose any `id.Signatories` in - // their `block.Header` - expectedSigs = nil - case block.Rebase, block.Base: - // Rebase/base `block.Blocks` must propose new `id.Signatories` in their - // `block.Header` - expectedSigs = make(id.Signatories, len(rebaser.expectedRebaseSigs)) - copy(expectedSigs, rebaser.expectedRebaseSigs) - default: - panic(fmt.Errorf("invariant violation: must not propose block kind=%v", rebaser.expectedKind)) - } - - txs, plan, prevState := rebaser.blockIterator.NextBlock( - rebaser.expectedKind, - height, - rebaser.shard, - ) - - header := block.NewHeader( - rebaser.expectedKind, - parent.Hash(), - base.Hash(), - txs.Hash(), - plan.Hash(), - prevState.Hash(), - height, - round, - block.Timestamp(time.Now().Unix()), - expectedSigs, - ) - - return block.New(header, txs, plan, prevState) -} - -func (rebaser *shardRebaser) IsBlockValid(proposedBlock block.Block, checkHistory bool) (process.NilReasons, error) { - rebaser.mu.Lock() - defer rebaser.mu.Unlock() - - nilReasons := make(process.NilReasons) - - // Check the expected `block.Kind` - if proposedBlock.Header().Kind() != rebaser.expectedKind { - return nilReasons, fmt.Errorf("unexpected block kind: expected %v, got %v", rebaser.expectedKind, proposedBlock.Header().Kind()) - } - switch proposedBlock.Header().Kind() { - case block.Standard: - if proposedBlock.Header().Signatories() != nil && len(proposedBlock.Header().Signatories()) != 0 { - return nilReasons, fmt.Errorf("expected standard block to have nil/empty signatories") - } - - case block.Rebase: - if len(proposedBlock.Header().Signatories()) != rebaser.numSignatories { - return nilReasons, fmt.Errorf("unexpected number of signatories in rebase block: expected %d, got %d", rebaser.numSignatories, len(proposedBlock.Header().Signatories())) - } - if !proposedBlock.Header().Signatories().Equal(rebaser.expectedRebaseSigs) { - return nilReasons, fmt.Errorf("unexpected signatories in rebase block: expected %d, got %d", len(rebaser.expectedRebaseSigs), len(proposedBlock.Header().Signatories())) - } - - case block.Base: - if !proposedBlock.Header().Signatories().Equal(rebaser.expectedRebaseSigs) { - return nilReasons, fmt.Errorf("unexpected signatories in base block: expected %d, got %d", len(rebaser.expectedRebaseSigs), len(proposedBlock.Header().Signatories())) - } - if proposedBlock.Txs() != nil && len(proposedBlock.Txs()) != 0 { - return nilReasons, fmt.Errorf("expected base block to have nil/empty txs") - } - if proposedBlock.Plan() != nil && len(proposedBlock.Plan()) != 0 { - return nilReasons, fmt.Errorf("expected base block to have nil/empty plan") - } - - default: - panic(fmt.Errorf("invariant violation: must not propose block kind=%v", rebaser.expectedKind)) - } - - // Check the expected `block.Hash` - if !proposedBlock.Header().TxsRef().Equal(proposedBlock.Txs().Hash()) { - if !(proposedBlock.Txs() == nil && proposedBlock.Header().TxsRef().Equal(id.Hash{})) { - return nilReasons, fmt.Errorf("unexpected txs hash for proposed block") - } - } - if !proposedBlock.Header().PlanRef().Equal(proposedBlock.Plan().Hash()) { - if !(proposedBlock.Plan() == nil && proposedBlock.Header().PlanRef().Equal(id.Hash{})) { - return nilReasons, fmt.Errorf("unexpected plan hash for proposed block") - } - } - if !proposedBlock.Header().PrevStateRef().Equal(proposedBlock.PreviousState().Hash()) { - if !(proposedBlock.PreviousState() == nil && proposedBlock.Header().PrevStateRef().Equal(id.Hash{})) { - return nilReasons, fmt.Errorf("unexpected previous state hash for proposed block") - } - } - if !proposedBlock.Hash().Equal(block.NewBlockHash(proposedBlock.Header(), proposedBlock.Txs(), proposedBlock.Plan(), proposedBlock.PreviousState())) { - return nilReasons, fmt.Errorf("unexpected block hash for proposed block") - } - - // Check against the parent `block.Block` - if checkHistory { - parentBlock, ok := rebaser.blockStorage.Blockchain(rebaser.shard).BlockAtHeight(proposedBlock.Header().Height() - 1) - if !ok { - return nilReasons, fmt.Errorf("block at height=%d not found", proposedBlock.Header().Height()-1) - } - if proposedBlock.Header().Timestamp() <= parentBlock.Header().Timestamp() { - return nilReasons, fmt.Errorf("expected timestamp for proposed block to be greater than parent block") - } - if proposedBlock.Header().Timestamp() > block.Timestamp(time.Now().Unix()) { - return nilReasons, fmt.Errorf("expected timestamp for proposed block to be less than current time") - } - if !proposedBlock.Header().ParentHash().Equal(parentBlock.Hash()) { - return nilReasons, fmt.Errorf("expected parent hash for proposed block to equal parent block hash") - } - - // Check that the parent is the most recently finalised - latestBlock := rebaser.blockStorage.LatestBlock(rebaser.shard) - if !parentBlock.Hash().Equal(latestBlock.Hash()) { - return nilReasons, fmt.Errorf("expected parent block hash to equal latest block hash") - } - if parentBlock.Hash().Equal(block.InvalidHash) { - return nilReasons, fmt.Errorf("parent block hash should not be invalid") - } - } - - // Check against the base `block.Block` - baseBlock := rebaser.blockStorage.LatestBaseBlock(rebaser.shard) - if !proposedBlock.Header().BaseHash().Equal(baseBlock.Hash()) { - return nilReasons, fmt.Errorf("expected base hash for proposed block to equal base block hash") - } - - // Pass to the next `process.Validator` - if rebaser.validator != nil { - return rebaser.validator.IsBlockValid(proposedBlock, checkHistory, rebaser.shard) - } - return nilReasons, nil -} - -func (rebaser *shardRebaser) DidCommitBlock(height block.Height) { - rebaser.mu.Lock() - defer rebaser.mu.Unlock() - - committedBlock, ok := rebaser.blockStorage.Blockchain(rebaser.shard).BlockAtHeight(height) - if !ok { - panic(fmt.Errorf("invariant violation: missing block at height=%v", height)) - } - - switch committedBlock.Header().Kind() { - case block.Standard: - case block.Rebase: - rebaser.expectedKind = block.Base - rebaser.expectedRebaseSigs = committedBlock.Header().Signatories() - case block.Base: - if rebaser.scheduler != nil { - rebaser.scheduler.Rebase(rebaser.expectedRebaseSigs) - } - rebaser.expectedKind = block.Standard - rebaser.expectedRebaseSigs = nil - } - if rebaser.observer != nil { - rebaser.observer.DidCommitBlock(height, rebaser.shard) - } -} - -func (rebaser *shardRebaser) DidReceiveSufficientNilPrevotes(messages process.Messages, f int) { - if rebaser.observer != nil { - rebaser.observer.DidReceiveSufficientNilPrevotes(messages, f) - } -} - -func (rebaser *shardRebaser) rebase(sigs id.Signatories) { - rebaser.mu.Lock() - defer rebaser.mu.Unlock() - - if rebaser.expectedKind != block.Standard { - // Handle duplicate rebase calls - if !sigs.Equal(rebaser.expectedRebaseSigs) { - panic("invariant violation: must not rebase while rebasing") - } - return - } - - rebaser.expectedKind = block.Rebase - rebaser.expectedRebaseSigs = sigs -} diff --git a/replica/rebase_test.go b/replica/rebase_test.go deleted file mode 100644 index f6136374..00000000 --- a/replica/rebase_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package replica - -import ( - "crypto/ecdsa" - cRand "crypto/rand" - "math/rand" - "testing/quick" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/renproject/hyperdrive/schedule" - . "github.com/renproject/hyperdrive/testutil" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/id" -) - -var _ = Describe("shardRebaser", func() { - - commitBlock := func(storage BlockStorage, shard Shard, block block.Block) { - bc := storage.Blockchain(shard) - bc.InsertBlockAtHeight(block.Header().Height(), block) - } - - Context("initializing a new shardRebaser", func() { - It("should implements the process.Proposer", func() { - test := func(shard Shard, numSignatories int) bool { - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - rebaser := newShardRebaser(nil, store, iter, nil, nil, shard, numSignatories) - - parent := store.LatestBlock(shard) - base := store.LatestBaseBlock(shard) - round := RandomRound() - b := rebaser.BlockProposal(initHeight+1, round) - - Expect(b.Header().Kind()).Should(Equal(block.Standard)) - Expect(b.Header().ParentHash().Equal(parent.Hash())).Should(BeTrue()) - Expect(b.Header().BaseHash().Equal(base.Hash())).Should(BeTrue()) - Expect(b.Header().Height()).Should(Equal(initHeight + 1)) - Expect(b.Header().Round()).Should(Equal(round)) - Expect(b.Header().Signatories()).Should(BeNil()) - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should implements the process.Validator", func() { - test := func(shard Shard, numSignatories int) bool { - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - validator := newMockValidator(nil) - rebaser := newShardRebaser(nil, store, iter, validator, nil, shard, numSignatories) - - // Generate a valid propose block. - parent := store.LatestBlock(shard) - base := store.LatestBaseBlock(shard) - header := RandomBlockHeaderJSON(block.Standard) - header.Height = initHeight + 1 - header.BaseHash = base.Hash() - header.ParentHash = parent.Hash() - header.Timestamp = block.Timestamp(time.Now().Unix()) - proposedBlock := block.New(header.ToBlockHeader(), nil, nil, nil) - - _, err := rebaser.IsBlockValid(proposedBlock, true) - return err == nil - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should implement the process.Observer", func() { - test := func(shard Shard, numSignatories int) bool { - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - observer := newMockObserver() - rebaser := newShardRebaser(nil, store, iter, nil, observer, shard, numSignatories) - - rebaser.DidCommitBlock(0) - rebaser.DidCommitBlock(initHeight) - messages := make(process.Messages, 10) - for i := 0; i < len(messages); i++ { - messages[i] = RandomMessage(RandomMessageType(true)) - } - rebaser.DidReceiveSufficientNilPrevotes(messages, 0) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - - Context("when rebasing", func() { - It("should be ready to receive a new rebase block", func() { - test := func(shard Shard, sigs id.Signatories) bool { - store, _, _ := initStorage(shard) - iter := mockBlockIterator{} - rebaser := newShardRebaser(nil, store, iter, nil, nil, shard, len(sigs)) - - rebaser.rebase(sigs) - Expect(rebaser.expectedKind).Should(Equal(block.Rebase)) - Expect(rebaser.expectedRebaseSigs.Equal(sigs)).Should(BeTrue()) - - // Should be able to handle multiple rebase calls - rebaser.rebase(sigs) - Expect(rebaser.expectedKind).Should(Equal(block.Rebase)) - Expect(rebaser.expectedRebaseSigs.Equal(sigs)).Should(BeTrue()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should return a valid rebase block when proposing", func() { - test := func(shard Shard, sigs id.Signatories) bool { - if len(sigs) == 0 { - return true - } - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - rebaser := newShardRebaser(nil, store, iter, nil, nil, shard, len(sigs)) - - rebaser.rebase(sigs) - parent := store.LatestBlock(shard) - base := store.LatestBaseBlock(shard) - - round := RandomRound() - rebaseBlock := rebaser.BlockProposal(initHeight+1, round) - Expect(rebaseBlock.Header().Kind()).Should(Equal(block.Rebase)) - Expect(rebaseBlock.Header().ParentHash().Equal(parent.Hash())).Should(BeTrue()) - Expect(rebaseBlock.Header().BaseHash().Equal(base.Hash())).Should(BeTrue()) - Expect(rebaseBlock.Header().Height()).Should(Equal(initHeight + 1)) - Expect(rebaseBlock.Header().Round()).Should(Equal(round)) - Expect(rebaseBlock.Header().Signatories().Equal(sigs)).Should(BeTrue()) - - commitBlock(store, shard, rebaseBlock) - rebaser.DidCommitBlock(initHeight + 1) - - baseBlock := rebaser.BlockProposal(initHeight+2, round) - Expect(baseBlock.Header().Kind()).Should(Equal(block.Base)) - Expect(baseBlock.Header().ParentHash().Equal(rebaseBlock.Hash())).Should(BeTrue()) - Expect(baseBlock.Header().BaseHash().Equal(base.Hash())).Should(BeTrue()) - Expect(baseBlock.Header().Height()).Should(Equal(initHeight + 2)) - Expect(baseBlock.Header().Round()).Should(Equal(round)) - Expect(baseBlock.Header().Signatories().Equal(sigs)).Should(BeTrue()) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should valid a block only if it's a rebase block", func() { - test := func(shard Shard, sigs id.Signatories) bool { - if len(sigs) == 0 { - return true - } - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - rebaser := newShardRebaser(nil, store, iter, nil, nil, shard, len(sigs)) - rebaser.rebase(sigs) - - // Generate a valid rebase block. - parent := store.LatestBlock(shard) - base := store.LatestBaseBlock(shard) - header := RandomBlockHeaderJSON(block.Rebase) - header.Height = initHeight + 1 - header.BaseHash = base.Hash() - header.ParentHash = parent.Hash() - header.Timestamp = block.Timestamp(time.Now().Unix() - 1) - header.Signatories = sigs - rebaseBlock := block.New(header.ToBlockHeader(), nil, nil, nil) - _, err := rebaser.IsBlockValid(rebaseBlock, true) - Expect(err).Should(BeNil()) - - // After the block been committed - commitBlock(store, shard, rebaseBlock) - rebaser.DidCommitBlock(initHeight + 1) - - // Generate a valid base block. - parent = rebaseBlock - baseHeader := RandomBlockHeaderJSON(block.Base) - baseHeader.Height = initHeight + 2 - baseHeader.BaseHash = base.Hash() - baseHeader.ParentHash = parent.Hash() - baseHeader.Timestamp = block.Timestamp(time.Now().Unix()) - baseHeader.Signatories = sigs - baseBlock := block.New(baseHeader.ToBlockHeader(), nil, nil, nil) - - _, err = rebaser.IsBlockValid(baseBlock, true) - return err == nil - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should change signatories after a base block", func() { - test := func(shard Shard, sigs id.Signatories) bool { - if len(sigs) == 0 { - return true - } - store, initHeight, _ := initStorage(shard) - iter := mockBlockIterator{} - scheduler := schedule.RoundRobin(id.Signatories{}) - rebaser := newShardRebaser(scheduler, store, iter, nil, nil, shard, len(sigs)) - rebaser.rebase(sigs) - - // Rebasing should not immediate cause a rebase of the - // scheduler. - Expect(scheduler.Schedule(0, 0)).To(Equal(block.InvalidSignatory)) - - // Generate a valid rebase block. - parent := store.LatestBlock(shard) - base := store.LatestBaseBlock(shard) - header := RandomBlockHeaderJSON(block.Rebase) - header.Height = initHeight + 1 - header.BaseHash = base.Hash() - header.ParentHash = parent.Hash() - header.Timestamp = block.Timestamp(time.Now().Unix() - 1) - header.Signatories = sigs - rebaseBlock := block.New(header.ToBlockHeader(), nil, nil, nil) - _, err := rebaser.IsBlockValid(rebaseBlock, true) - Expect(err).Should(BeNil()) - - // After the block been committed - commitBlock(store, shard, rebaseBlock) - rebaser.DidCommitBlock(initHeight + 1) - - // Generate a valid base block. - parent = rebaseBlock - baseHeader := RandomBlockHeaderJSON(block.Base) - baseHeader.Height = initHeight + 2 - baseHeader.BaseHash = base.Hash() - baseHeader.ParentHash = parent.Hash() - baseHeader.Timestamp = block.Timestamp(time.Now().Unix()) - baseHeader.Signatories = sigs - baseBlock := block.New(baseHeader.ToBlockHeader(), nil, nil, nil) - _, err = rebaser.IsBlockValid(baseBlock, true) - Expect(err).Should(BeNil()) - - // After the base block is commited... - commitBlock(store, shard, baseBlock) - rebaser.DidCommitBlock(initHeight + 2) - - // ...the scheduler should be rebased. - Expect(scheduler.Schedule(0, 0)).To(Equal(sigs[0])) - - return true - } - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) -}) - -func initStorage(shard Shard) (BlockStorage, block.Height, []*ecdsa.PrivateKey) { - sigs := make(id.Signatories, 7) - keys := make([]*ecdsa.PrivateKey, 7) - for i := range sigs { - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - if err != nil { - panic(err) - } - keys[i] = privateKey - sigs[i] = id.NewSignatory(privateKey.PublicKey) - } - store := newMockBlockStorage(sigs) - initHeight := block.Height(rand.Intn(100)) - - // Init the genesis block at height 0 - bc := store.Blockchain(shard) - - // Init standard blocks from block 1 to initHeight - for i := 1; i <= int(initHeight); i++ { - b := RandomBlock(block.Standard) - bc.InsertBlockAtHeight(block.Height(i), b) - } - return store, initHeight, keys -} diff --git a/replica/replica.go b/replica/replica.go index f96862f7..b418689d 100644 --- a/replica/replica.go +++ b/replica/replica.go @@ -1,374 +1,158 @@ package replica import ( - "bytes" - "crypto/ecdsa" - "encoding/base64" - "fmt" - "io" - "time" + "context" - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/abi" - "github.com/renproject/hyperdrive/block" + "github.com/renproject/hyperdrive/mq" "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/schedule" + "github.com/renproject/hyperdrive/scheduler" + "github.com/renproject/hyperdrive/timer" "github.com/renproject/id" - "github.com/sirupsen/logrus" ) -type Shards []Shard - -// Shard uniquely identifies the Shard being maintained by the Replica. -type Shard [32]byte - -// Equal compares one Shard with another. -func (shard Shard) Equal(other Shard) bool { - return bytes.Equal(shard[:], other[:]) -} - -// String implements the `fmt.Stringer` interface. -func (shard Shard) String() string { - return base64.RawStdEncoding.EncodeToString(shard[:]) -} - -func (shard Shard) SizeHint() int { - return 32 -} - -func (shard Shard) Marshal(w io.Writer, m int) (int, error) { - return abi.Bytes32(shard).Marshal(w, m) -} - -func (shard *Shard) Unmarshal(r io.Reader, m int) (int, error) { - return (*abi.Bytes32)(shard).Unmarshal(r, m) -} - -type Messages []Message - -// A Message sent/received by a Replica is composed of a Shard and the -// underlying `process.Message` data. It is expected that a Replica will sign -// the underlying `process.Message` data before sending the Message. -type Message struct { - Message process.Message - Shard Shard - Signature id.Signature -} - -// ProcessStorage saves and restores `process.State` to persistent memory. This -// guarantees that in the event of an unexpected shutdown, the Replica will only -// drop the `process.Message` that was currently being handling. -type ProcessStorage interface { - SaveState(state *process.State, shard Shard) - RestoreState(state *process.State, shard Shard) -} - -// Options define a set of properties that can be used to parameterise the -// Replica and its behaviour. -type Options struct { - // Logging - Logger logrus.FieldLogger - - // Timeout options for proposing, prevoting, and precommiting - BackOffExp float64 - BackOffBase time.Duration - BackOffMax time.Duration - - // MaxMessageQueueSize is the maximum number of "future" messages that the - // Replica will buffer in memory. - MaxMessageQueueSize int -} - -func (options *Options) setZerosToDefaults() { - if options.Logger == nil { - options.Logger = logrus.StandardLogger() - } - if options.BackOffExp == 0 { - options.BackOffExp = 1.6 - } - if options.BackOffBase == time.Duration(0) { - options.BackOffBase = 20 * time.Second - } - if options.BackOffMax == time.Duration(0) { - options.BackOffMax = 5 * time.Minute - } - if options.MaxMessageQueueSize == 0 { - options.MaxMessageQueueSize = 512 - } -} - -type Replicas []Replica - // A Replica represents one Process in a replicated state machine that is bound // to a specific Shard. It signs Messages before sending them to other Replicas, // and verifies Messages before accepting them from other Replicas. type Replica struct { - options Options - shard Shard - p *process.Process - numSignatories int - blockStorage BlockStorage + opts Options + + proc process.Process + procsAllowed map[id.Signatory]bool - scheduler schedule.Scheduler - rebaser *shardRebaser - cache baseBlockCache + onTimeoutPropose <-chan timer.Timeout + onTimeoutPrevote <-chan timer.Timeout + onTimeoutPrecommit <-chan timer.Timeout - messageQueue MessageQueue + onPropose chan process.Propose + onPrevote chan process.Prevote + onPrecommit chan process.Precommit + mq mq.MessageQueue } -func New(options Options, pStorage ProcessStorage, blockStorage BlockStorage, blockIterator BlockIterator, validator Validator, observer Observer, broadcaster Broadcaster, scheduler schedule.Scheduler, catcher process.Catcher, shard Shard, privKey ecdsa.PrivateKey) Replica { - options.setZerosToDefaults() - latestBase := blockStorage.LatestBaseBlock(shard) - numSignatories := len(latestBase.Header().Signatories()) - if numSignatories%3 != 1 || numSignatories < 4 { - panic(fmt.Errorf("invariant violation: number of nodes needs to be 3f +1, got %v", numSignatories)) - } - shardRebaser := newShardRebaser(scheduler, blockStorage, blockIterator, validator, observer, shard, numSignatories) +func New(opts Options, whoami id.Signatory, signatories []id.Signatory, propose process.Proposer, validate process.Validator, commit process.Committer, catch process.Catcher, broadcast process.Broadcaster) *Replica { - // Create a Process in the default state and then restore it - p := process.New( - options.Logger, - id.NewSignatory(privKey.PublicKey), - blockStorage.Blockchain(shard), - process.DefaultState((numSignatories-1)/3), - newSaveRestorer(pStorage, shard), - shardRebaser, - shardRebaser, - shardRebaser, - newSigner(broadcaster, shard, privKey), + f := len(signatories) / 3 + onTimeoutPropose := make(chan timer.Timeout, 10) + onTimeoutPrevote := make(chan timer.Timeout, 10) + onTimeoutPrecommit := make(chan timer.Timeout, 10) + timer := timer.NewLinearTimer(opts.TimerOpts, onTimeoutPropose, onTimeoutPrevote, onTimeoutPrecommit) + scheduler := scheduler.NewRoundRobin(signatories) + proc := process.New( + whoami, + f, + timer, scheduler, - newBackOffTimer(options.BackOffExp, options.BackOffBase, options.BackOffMax), - catcher, + propose, + validate, + broadcast, + commit, + catch, ) - p.Restore() - - return Replica{ - options: options, - shard: shard, - p: p, - numSignatories: numSignatories, - blockStorage: blockStorage, - scheduler: scheduler, - rebaser: shardRebaser, - cache: newBaseBlockCache(latestBase), - - messageQueue: NewMessageQueue(options.MaxMessageQueueSize), + procsAllowed := make(map[id.Signatory]bool) + for _, signatory := range signatories { + procsAllowed[signatory] = true } -} -func (replica *Replica) Start() { - replica.p.Start() -} + return &Replica{ + opts: opts, -func (replica *Replica) HandleMessage(m Message) { - // Check that Message is from our shard. If it is not, then there is no - // point processing the message. - if !replica.shard.Equal(m.Shard) { - replica.options.Logger.Warnf("bad message: expected shard=%v, got shard=%v", replica.shard, m.Shard) - return - } + proc: proc, + procsAllowed: procsAllowed, - // Ignore non-Resync messages from heights that the process has already - // progressed through. Messages at these earlier heights have no affect on - // consensus, and so there is no point wasting time processing them. - if m.Message.Height() < replica.p.CurrentHeight() { - if _, ok := m.Message.(*process.Resync); !ok { - replica.options.Logger.Debugf("ignore message: expected height>=%v, got height=%v", replica.p.CurrentHeight(), m.Message.Height()) - return - } - // Fall-through to the remaining logic. - } + onTimeoutPropose: onTimeoutPropose, + onTimeoutPrevote: onTimeoutPrevote, + onTimeoutPrecommit: onTimeoutPrecommit, - // Check that the Message sender is from our Shard (this can be a moderately - // expensive operation, so we cache the result until a new `block.Base` is - // detected) - replica.cache.fillBaseBlock(replica.blockStorage.LatestBaseBlock(replica.shard)) - if !replica.cache.signatoryInBaseBlock(m.Message.Signatory()) { - return - } - if err := replica.verifySignedMessage(m); err != nil { - replica.options.Logger.Warnf("bad message: unverified: %v", err) - return + onPropose: make(chan process.Propose, opts.MessageQueueOpts.MaxCapacity), + onPrevote: make(chan process.Prevote, opts.MessageQueueOpts.MaxCapacity), + onPrecommit: make(chan process.Precommit, opts.MessageQueueOpts.MaxCapacity), + mq: mq.New(opts.MessageQueueOpts), } +} - // Resync messages can be handled immediately, as long as they are not from - // a future height and their timestamps do not differ greatly from the - // current time. - if m.Message.Type() == process.ResyncMessageType { - if m.Message.Height() > replica.p.CurrentHeight() { - // We cannot respond to resync messages from future heights with - // anything that is useful, so we ignore it. - replica.options.Logger.Debugf("ignore message: resync height=%v compared to current height=%v", m.Message.Height(), replica.p.CurrentHeight()) +func (replica *Replica) Run(ctx context.Context) { + replica.proc.Start() + for { + select { + case <-ctx.Done(): return - } - // Filter Resync messages by timestamp. If they're too old, or too far - // in the future, then ignore them. The total window of time is 20 - // seconds, approximately the latency expected for globally distributed - // message passing. - now := block.Timestamp(time.Now().Unix()) - timestamp := m.Message.(*process.Resync).Timestamp() - delta := now - timestamp - if delta < 0 { - delta = -delta - } - if delta > 10 { - replica.options.Logger.Debugf("ignore message: resync timestamp=%v compared to now=%v", timestamp, now) - return - } - replica.p.HandleMessage(m.Message) - return - } - - // Make sure that the Process state gets saved. We do this here - // because Resync cannot cause state changes, so there is no - // reason to save after handling a Resync message. - defer replica.p.Save() - // Messages from the current height can be handled immediately. - if m.Message.Height() == replica.p.CurrentHeight() { - replica.p.HandleMessage(m.Message) - } + case timeout := <-replica.onTimeoutPropose: + replica.proc.OnTimeoutPropose(timeout.Height, timeout.Round) + case timeout := <-replica.onTimeoutPrevote: + replica.proc.OnTimeoutPrevote(timeout.Height, timeout.Round) + case timeout := <-replica.onTimeoutPrecommit: + replica.proc.OnTimeoutPrecommit(timeout.Height, timeout.Round) - // Messages from the future must be put into the height-ordered message - // queue. - if m.Message.Height() > replica.p.CurrentHeight() { - if m.Message.Type() != process.ProposeMessageType { - // We only want to queue non-Propose messages, because Propose messages - // can have a LatestCommit message to fast-forward the process. - replica.messageQueue.Push(m.Message) - } else { - // If the Propose is at a future height, then we need to make sure - // that no base blocks have been missed. Otherwise, reject the - // Propose, and wait until the appropriate one has been seen. - baseBlockHash := replica.blockStorage.LatestBaseBlock(m.Shard).Hash() - blockHash := m.Message.BlockHash() - numMissingBaseBlocks := replica.rebaser.blockIterator.MissedBaseBlocksInRange(baseBlockHash, blockHash) - if numMissingBaseBlocks == 0 { - // If we have missed a base block, we drop the Propose. The - // Propose that justifies the next base block will eventually be - // seen by this Replica and we can begin accepting Proposes from - // the new base. - - // In this condition, we haven't missed any base blocks, so we - // can proceed as usual. - replica.p.HandleMessage(m.Message) + case propose := <-replica.onPropose: + if !replica.filterHeight(propose.Height) { + continue } - } - } - - queued := replica.messageQueue.PopUntil(replica.p.CurrentHeight()) - for queued != nil && len(queued) > 0 { - baseBlockHash := replica.blockStorage.LatestBaseBlock(m.Shard).Hash() - for _, message := range queued { - // We need to make sure that no base blocks have been missed while - // the message was sitting on the queue. If we have missed a base - // block, we drop the message. - blockHash := message.BlockHash() - numMissingBaseBlocks := replica.rebaser.blockIterator.MissedBaseBlocksInRange(baseBlockHash, blockHash) - if numMissingBaseBlocks == 0 { - // Otherwise, we handle the Message. After all Messages that can - // be handled have been handled, this function will end, and the - // Process will be saved. This protects the Process from - // crashing part way through handling a Message and ending up in - // a partially saved State caused by "saving on the go". We - // could save between each message, but this would have a large - // performance footprint (and is ultimately unnecessary, because - // we do not expect crashes). - replica.p.HandleMessage(message) + if !replica.filterFrom(propose.From) { + continue + } + replica.mq.InsertPropose(propose) + case prevote := <-replica.onPrevote: + if !replica.filterHeight(prevote.Height) { + continue + } + if !replica.filterFrom(prevote.From) { + continue } + replica.mq.InsertPrevote(prevote) + case precommit := <-replica.onPrecommit: + if !replica.filterHeight(precommit.Height) { + continue + } + if !replica.filterFrom(precommit.From) { + continue + } + replica.mq.InsertPrecommit(precommit) } - queued = replica.messageQueue.PopUntil(replica.p.CurrentHeight()) + replica.flush() } } -func (replica *Replica) Rebase(sigs id.Signatories) { - if len(sigs) != replica.numSignatories { - panic(fmt.Errorf("invariant violation: number of signatories must not change: expected %v, got %v", replica.numSignatories, len(sigs))) - } - if len(sigs)%3 != 1 || len(sigs) < 4 { - panic(fmt.Errorf("invariant violation: number of nodes needs to be 3f +1, got %v", len(sigs))) +func (replica *Replica) Propose(ctx context.Context, propose process.Propose) { + select { + case <-ctx.Done(): + case replica.onPropose <- propose: } - replica.rebaser.rebase(sigs) } -func (replica *Replica) verifySignedMessage(m Message) error { - // Verify the Shard information - mHash := m.SigHash() - mPubKey, err := crypto.SigToPub(mHash[:], m.Signature[:]) - if err != nil { - return fmt.Errorf("sigToPub: %v", err) - } - mSignatory := id.NewSignatory(*mPubKey) - if !m.Message.Signatory().Equal(mSignatory) { - return fmt.Errorf("bad signatory: expected signatory=%v, got signatory=%v", m.Message.Signatory(), mSignatory) - } - // Verify that the Message is actually signed by the claimed `id.Signatory` - if err := process.Verify(m.Message); err != nil { - return err +func (replica *Replica) Prevote(ctx context.Context, prevote process.Prevote) { + select { + case <-ctx.Done(): + case replica.onPrevote <- prevote: } - return nil } -type saveRestorer struct { - pStorage ProcessStorage - shard Shard -} - -func newSaveRestorer(pStorage ProcessStorage, shard Shard) *saveRestorer { - return &saveRestorer{ - pStorage: pStorage, - shard: shard, +func (replica *Replica) Precommit(ctx context.Context, precommit process.Precommit) { + select { + case <-ctx.Done(): + case replica.onPrecommit <- precommit: } } -func (saveRestorer *saveRestorer) Save(state *process.State) { - saveRestorer.pStorage.SaveState(state, saveRestorer.shard) -} -func (saveRestorer *saveRestorer) Restore(state *process.State) { - saveRestorer.pStorage.RestoreState(state, saveRestorer.shard) +func (replica *Replica) filterHeight(height process.Height) bool { + return height >= replica.proc.CurrentHeight } -type baseBlockCache struct { - lastBaseBlockHeight block.Height - lastBaseBlockHash id.Hash - lastBaseBlockSigs id.Signatories - sigsCache map[id.Signatory]bool +func (replica *Replica) filterFrom(from id.Signatory) bool { + return replica.procsAllowed[from] } -func newBaseBlockCache(baseBlock block.Block) baseBlockCache { - cache := baseBlockCache{ - lastBaseBlockHeight: -1, - sigsCache: map[id.Signatory]bool{}, - } - cache.fillBaseBlock(baseBlock) - return cache -} - -func (cache *baseBlockCache) fillBaseBlock(baseBlock block.Block) { - if baseBlock.Header().Height() <= cache.lastBaseBlockHeight { - return - } - if baseBlock.Hash().Equal(cache.lastBaseBlockHash) { - return - } - cache.lastBaseBlockHeight = baseBlock.Header().Height() - cache.lastBaseBlockHash = baseBlock.Hash() - cache.lastBaseBlockSigs = baseBlock.Header().Signatories() - cache.sigsCache = map[id.Signatory]bool{} -} - -func (cache *baseBlockCache) signatoryInBaseBlock(sig id.Signatory) bool { - inBaseBlock, ok := cache.sigsCache[sig] - if ok { - return inBaseBlock - } - for _, baseBlockSig := range cache.lastBaseBlockSigs { - if baseBlockSig.Equal(sig) { - cache.sigsCache[sig] = true - return true +func (replica *Replica) flush() { + for { + n := replica.mq.Consume( + replica.proc.CurrentHeight, + replica.proc.Propose, + replica.proc.Prevote, + replica.proc.Precommit, + ) + if n == 0 { + return } } - cache.sigsCache[sig] = false - return false } diff --git a/replica/replica_suite_test.go b/replica/replica_suite_test.go index 297a0f33..d5361882 100644 --- a/replica/replica_suite_test.go +++ b/replica/replica_suite_test.go @@ -1,116 +1,13 @@ package replica import ( - "sync" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/id" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" ) func TestReplica(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Replica Suite") } - -type mockBlockStorage struct { - mu *sync.RWMutex - sigs id.Signatories - shards map[Shard]*MockBlockchain -} - -func newMockBlockStorage(sigs id.Signatories) BlockStorage { - return &mockBlockStorage{ - mu: new(sync.RWMutex), - sigs: sigs, - shards: map[Shard]*MockBlockchain{}, - } -} - -func (m *mockBlockStorage) Blockchain(shard Shard) process.Blockchain { - m.mu.Lock() - defer m.mu.Unlock() - - blockchain, ok := m.shards[shard] - if !ok { - m.shards[shard] = NewMockBlockchain(m.sigs) - return m.shards[shard] - } - return blockchain -} - -func (m *mockBlockStorage) LatestBlock(shard Shard) block.Block { - m.mu.RLock() - defer m.mu.RUnlock() - - blockchain, ok := m.shards[shard] - if !ok { - return block.InvalidBlock - } - - return blockchain.LatestBlock(block.Invalid) -} - -func (m *mockBlockStorage) LatestBaseBlock(shard Shard) block.Block { - m.mu.RLock() - defer m.mu.RUnlock() - - blockchain, ok := m.shards[shard] - if !ok { - return block.InvalidBlock - } - - return blockchain.LatestBlock(block.Base) -} - -type mockBlockIterator struct { -} - -func (m mockBlockIterator) NextBlock(kind block.Kind, height block.Height, shard Shard) (block.Txs, block.Plan, block.State) { - return RandomBytesSlice(), RandomBytesSlice(), RandomBytesSlice() -} - -func (m mockBlockIterator) MissedBaseBlocksInRange(begin, end id.Hash) int { - return 0 // mockBlockIterator does not support rebasing. -} - -type mockValidator struct { - valid error -} - -func (m mockValidator) IsBlockValid(block.Block, bool, Shard) (process.NilReasons, error) { - return nil, m.valid -} - -func newMockValidator(valid error) Validator { - return mockValidator{valid: valid} -} - -type mockObserver struct { -} - -func newMockObserver() Observer { - return mockObserver{} -} - -func (m mockObserver) DidCommitBlock(block.Height, Shard) { -} -func (m mockObserver) IsSignatory(Shard) bool { - return true -} -func (m mockObserver) DidReceiveSufficientNilPrevotes(process.Messages, int) { -} - -type mockProcessStorage struct { -} - -func (m mockProcessStorage) SaveState(state *process.State, shard Shard) { -} - -func (m mockProcessStorage) RestoreState(state *process.State, shard Shard) { -} diff --git a/replica/replica_test.go b/replica/replica_test.go index 451573e5..d89da8a5 100644 --- a/replica/replica_test.go +++ b/replica/replica_test.go @@ -1,201 +1 @@ package replica - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rand" - "io/ioutil" - "reflect" - "testing/quick" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/schedule" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/surge" - "github.com/sirupsen/logrus" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - . "github.com/renproject/hyperdrive/testutil" -) - -var _ = Describe("Replica", func() { - - newEcdsaKey := func() *ecdsa.PrivateKey { - privateKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) - Expect(err).NotTo(HaveOccurred()) - return privateKey - } - - Context("shard", func() { - Context("when comparing two shard", func() { - It("should be stringified to same text if two shards are equal and vice versa", func() { - test := func(shard1, shard2 Shard) bool { - shard := shard1 - Expect(shard.Equal(shard1)).Should(BeTrue()) - Expect(shard1.Equal(shard)).Should(BeTrue()) - Expect(shard.String()).Should(Equal(shard1.String())) - - Expect(shard1.Equal(shard2)).Should(BeFalse()) - Expect(shard1.String()).ShouldNot(Equal(shard2.String())) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("replica", func() { - Context("when marshaling/unmarshaling message", func() { - It("should equal itself after binary marshaling and then unmarshaling", func() { - message := Message{ - Message: RandomMessage(RandomMessageType(true)), - Shard: Shard{}, - } - - data, err := surge.ToBinary(message) - Expect(err).NotTo(HaveOccurred()) - newMessage := Message{} - Expect(surge.FromBinary(data, &newMessage)).Should(Succeed()) - - newData, err := surge.ToBinary(newMessage) - Expect(err).NotTo(HaveOccurred()) - Expect(bytes.Equal(data, newData)).Should(BeTrue()) - }) - }) - - Context("when sending messages to replica", func() { - It("should only pass message to process when it's a valid message", func() { - test := func(shard, wrongShard Shard) bool { - store, _, keys := initStorage(shard) - pstore := mockProcessStorage{} - broadcaster, _, _ := newMockBroadcaster() - scheduler := schedule.RoundRobin(store.LatestBaseBlock(shard).Header().Signatories()) - replica := New(Options{}, pstore, store, mockBlockIterator{}, nil, nil, broadcaster, scheduler, process.CatchAndIgnore(), shard, *newEcdsaKey()) - - pMessage := RandomMessage(process.ProposeMessageType) - numStored := 0 - // Only one proposer is valid, so only one propose should - // end up stored in the Process state. - for _, key := range keys { - Expect(process.Sign(pMessage, *key)).Should(Succeed()) - message := SignMessage(pMessage, shard, *key) - replica.HandleMessage(message) - - // Expect the message not been inserted into the specific inbox, - // which indicating the message not passed to the process. - state := testutil.GetStateFromProcess(replica.p, 2) - stored := state.Proposals.QueryByHeightRoundSignatory(pMessage.Height(), pMessage.Round(), pMessage.Signatory()) - if reflect.DeepEqual(stored, pMessage) { - numStored++ - } - } - Expect(numStored).To(Equal(1)) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should reject message of different shard", func() { - test := func(shard, wrongShard Shard) bool { - store, _, keys := initStorage(shard) - pstore := mockProcessStorage{} - broadcaster, _, _ := newMockBroadcaster() - scheduler := schedule.RoundRobin(store.LatestBaseBlock(shard).Header().Signatories()) - replica := New(Options{}, pstore, store, mockBlockIterator{}, nil, nil, broadcaster, scheduler, process.CatchAndIgnore(), shard, *newEcdsaKey()) - logger := logrus.StandardLogger() - logger.SetOutput(ioutil.Discard) - replica.options.Logger = logger - - pMessage := RandomSignedMessage(process.ProposeMessageType) - message := SignMessage(pMessage, wrongShard, *keys[0]) - replica.HandleMessage(message) - - // Expect the message not been inserted into the specific inbox, - // which indicating the message not passed to the process. - state := testutil.GetStateFromProcess(replica.p, 2) - stored := state.Proposals.QueryByHeightRoundSignatory(pMessage.Height(), pMessage.Round(), pMessage.Signatory()) - Expect(stored).Should(BeNil()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - - It("should reject message whose signatory is not valid", func() { - test := func(shard Shard) bool { - store, _, keys := initStorage(shard) - pstore := mockProcessStorage{} - broadcaster, _, _ := newMockBroadcaster() - scheduler := schedule.RoundRobin(store.LatestBaseBlock(shard).Header().Signatories()) - replica := New(Options{}, pstore, store, mockBlockIterator{}, nil, nil, broadcaster, scheduler, process.CatchAndIgnore(), shard, *newEcdsaKey()) - - pMessage := RandomSignedMessage(process.ProposeMessageType) - message := SignMessage(pMessage, shard, *keys[0]) - replica.HandleMessage(message) - - // Expect the message not been inserted into the specific inbox, - // which indicating the message not passed to the process. - state := testutil.GetStateFromProcess(replica.p, 2) - stored := state.Proposals.QueryByHeightRoundSignatory(pMessage.Height(), pMessage.Round(), pMessage.Signatory()) - Expect(stored).Should(BeNil()) - - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) - }) - - Context("when sending resync messages to replica", func() { - Context("when the resync message is greater than the current height", func() { - It("should resend nothing", func() { - store, _, keys := initStorage(Shard{}) - pstore := mockProcessStorage{} - broadcaster, _, castMessages := newMockBroadcaster() - replica := New(Options{}, pstore, store, mockBlockIterator{}, nil, nil, broadcaster, schedule.RoundRobin(testutil.RandomSignatories()), nil, Shard{}, *newEcdsaKey()) - logger := logrus.StandardLogger() - logger.SetOutput(ioutil.Discard) - replica.options.Logger = logger - - message := SignMessage(process.NewResync(block.Height(999), block.Round(0)), Shard{}, *keys[0]) - replica.HandleMessage(message) - select { - case <-time.After(time.Second): - case <-castMessages: - Expect(func() { panic("should resent nothing") }).ToNot(Panic()) - } - }) - }) - - Context("when outside the current time", func() { - It("should resend nothing", func() { - store, _, keys := initStorage(Shard{}) - pstore := mockProcessStorage{} - broadcaster, _, castMessages := newMockBroadcaster() - replica := New(Options{}, pstore, store, mockBlockIterator{}, nil, nil, broadcaster, schedule.RoundRobin(testutil.RandomSignatories()), nil, Shard{}, *newEcdsaKey()) - logger := logrus.StandardLogger() - logger.SetOutput(ioutil.Discard) - replica.options.Logger = logger - - message := SignMessage(process.NewResync(block.Height(0), block.Round(0)), Shard{}, *keys[0]) - time.Sleep(11 * time.Second) - replica.HandleMessage(message) - select { - case <-time.After(time.Second): - case <-castMessages: - Expect(func() { panic("should resent nothing") }).ToNot(Panic()) - } - }) - }) - }) -}) diff --git a/replica/time.go b/replica/time.go deleted file mode 100644 index 101adf92..00000000 --- a/replica/time.go +++ /dev/null @@ -1,44 +0,0 @@ -package replica - -import ( - "math" - "time" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" -) - -type backOffTimer struct { - exp float64 - base time.Duration - max time.Duration -} - -func newBackOffTimer(exp float64, base time.Duration, max time.Duration) process.Timer { - return &backOffTimer{ - exp: exp, - base: base, - max: max, - } -} - -func (timer *backOffTimer) Timeout(step process.Step, round block.Round) time.Duration { - if round == 0 { - return timer.base - } - multiplier := math.Pow(timer.exp, float64(round)) - var duration time.Duration - - // Make sure it doesn't overflow - durationFloat := float64(timer.base) * multiplier - if durationFloat > math.MaxInt64 { - duration = time.Duration(math.MaxInt64) - } else { - duration = time.Duration(durationFloat) - } - - if duration > timer.max { - return timer.max - } - return duration -} diff --git a/replica/time_test.go b/replica/time_test.go deleted file mode 100644 index dc9133fc..00000000 --- a/replica/time_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package replica - -import ( - "math/rand" - "testing/quick" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" -) - -func randomStep() process.Step { - return process.Step(rand.Intn(3) + 1) -} - -var _ = Describe("timer", func() { - Context("when using a backOffTimer", func() { - It("should return a timeout of certain in a backoff manner", func() { - test := func() bool { - max := time.Duration(rand.Int()) - base := time.Duration(rand.Intn(int(max))) - exp := rand.Float64() + 1 - timer := newBackOffTimer(exp, base, max) - - Expect(timer.Timeout(randomStep(), 0)).Should(Equal(base)) - - preTimeout := time.Duration(0) - for round := block.Round(1); round < 20; round++ { - timeout := timer.Timeout(randomStep(), round) - Expect(timeout).Should(BeNumerically("<=", max)) - Expect(timeout).Should(BeNumerically(">=", preTimeout)) - preTimeout = timeout - } - return true - } - - Expect(quick.Check(test, nil)).Should(Succeed()) - }) - }) -}) diff --git a/schedule/schedule.go b/schedule/schedule.go deleted file mode 100644 index f1468291..00000000 --- a/schedule/schedule.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package schedule defines interfaces and implementations for scheduling -// different Processes as Block proposers. At any given Height and Round, -// exactly one Process is expected to take responsibility for proposing a Block, -// and this is determined by the Scheduler. -// -// It is important that all Processes agree on the schedule. That is, at any -// given Height and Round, all Processes must arrive at the same decision -// regarding which Process is expected to be the proposer. This is most commonly -// done by making the schedule deterministic based and locally computable. This -// means that Processes do not have to invoke a consensus algorithm in order to -// agree on the schedule (although, this is possible to do, using Block N to -// agree on the schedule for Block N+1). -package schedule - -import ( - "sync" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/id" -) - -// A Scheduler is used to determine which Signatory is expected to propose a -// Block at any given Height and Round. It supports rebasing, allowing the -// underlying Signatories to be changed over time. Schedulers are expected to be -// safe for concurrent use. -type Scheduler interface { - // Schedule returns the Signatory that is expected to propose a Block at the - // given Height and Round. If no Signatory is expected to propose a Block, - // then it returns the InvalidSignatory. - // - // Schedule must return the same Signatory for the same Height and Round for - // all consensus participants. For performance, it should be locally - // computable; there should be no communication between consensus - // participants. - // - // proposer := scheduler.Schedule(propose.Height(), propose.Round()) - // Expect(propose.Signatory()).To(Equal(proposer)) - // - Schedule(block.Height, block.Round) id.Signatory - - // Rebase tells the Scheduler that the underlying Signatories has been - // changed. The Scheduler must only ever schedule Signatories from that most - // recent rebase. - // - // if block.Kind() == Rebase { - // scheduler.Rebase(block.Header().Signatories()) - // } - // - Rebase(id.Signatories) -} - -type roundRobin struct { - signatoriesMu *sync.Mutex - signatories id.Signatories -} - -// RoundRobin returns a Scheduler that uses a round-robin scheduling algorithm -// to select a Signatory. Round-robin scheduling has the advantage of being very -// easy to implement and understand, but has the disadvantage of being unfair. -// As such, it should be avoided when the proposer is expected to receive a -// larger Block reward than non-proposers. -func RoundRobin(signatories id.Signatories) Scheduler { - rr := &roundRobin{ - signatoriesMu: new(sync.Mutex), - } - rr.Rebase(signatories) - return rr -} - -// Schedule a Signatory using the sum of the Height and Round, modulo the number -// of Signatories, to select a Signatory. -func (rr *roundRobin) Schedule(height block.Height, round block.Round) id.Signatory { - rr.signatoriesMu.Lock() - defer rr.signatoriesMu.Unlock() - - if len(rr.signatories) == 0 { - // When there are no signatories, there is no valid Signatory to select. - // As per the requirements for the Scheduler interface, we return the - // InvalidSignatory. - return block.InvalidSignatory - } - if height == block.InvalidHeight { - // When the height is invalid, there is no valid Signatory to select. - return block.InvalidSignatory - } - if round == block.InvalidRound { - // When the round is invalid, there is no valid Signatory to select. - return block.InvalidSignatory - } - - return rr.signatories[(uint64(height)+uint64(round))%uint64(len(rr.signatories))] -} - -// Rebase will replace the underlying Signatories from which the round-robin -// Scheduler will select proposers. -func (rr *roundRobin) Rebase(signatories id.Signatories) { - rr.signatoriesMu.Lock() - defer rr.signatoriesMu.Unlock() - - // Copy signatories into the scheduler to avoid manipulation of the slice, - // external to the scheduler, from affecting the scheduler. - rr.signatories = make(id.Signatories, len(signatories)) - copy(rr.signatories, signatories) -} diff --git a/schedule/schedule_test.go b/schedule/schedule_test.go deleted file mode 100644 index 678a3bf6..00000000 --- a/schedule/schedule_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package schedule_test - -import ( - "testing/quick" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/schedule" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/id" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Round-robin scheduler", func() { - Context("when scheduling `n` times", func() { - Context("when only incrementing the height", func() { - It("should schedule each signatories `n` times", func() { - f := func(n int) bool { - // Clamp n to be positive, and less than 100. - if n < 0 { - n = -n - } - n = n % 100 - - // Setup the scheduler. - signatories := testutil.RandomSignatories() - scheduler := schedule.RoundRobin(signatories) - - // Call the Schedule method n times for each signatory, - // incrementing the Height by exactly 1 each time. - counts := map[id.Signatory]int{} - for i := 0; i < n*len(signatories); i++ { - proposer := scheduler.Schedule(block.Height(i), block.Round(0)) - counts[proposer]++ - } - - // Expect each Signatory to have been returned n times. - for _, signatory := range signatories { - Expect(counts[signatory]).To(Equal(n)) - } - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when only incrementing the round", func() { - It("should schedule each signatories `n` times", func() { - f := func(n int) bool { - // Clamp n to be positive, and less than 100. - if n < 0 { - n = -n - } - n = n % 100 - - // Setup the scheduler. - signatories := testutil.RandomSignatories() - scheduler := schedule.RoundRobin(signatories) - - // Call the Schedule method n times for each signatory, - // incrementing the Round by exactly 1 each time. - counts := map[id.Signatory]int{} - for i := 0; i < n*len(signatories); i++ { - proposer := scheduler.Schedule(block.Height(0), block.Round(i)) - counts[proposer]++ - } - - // Expect each Signatory to have been returned n times. - for _, signatory := range signatories { - Expect(counts[signatory]).To(Equal(n)) - } - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the underlying signatories are nil", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(nil) - proposer := scheduler.Schedule(testutil.RandomHeight(), testutil.RandomRound()) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the underlying signatories are empty", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(id.Signatories{}) - proposer := scheduler.Schedule(testutil.RandomHeight(), testutil.RandomRound()) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the height is invalid", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(testutil.RandomSignatories()) - proposer := scheduler.Schedule(block.InvalidHeight, testutil.RandomRound()) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the round is invalid", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(testutil.RandomSignatories()) - proposer := scheduler.Schedule(testutil.RandomHeight(), block.InvalidRound) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - }) - - Context("when rebasing", func() { - Context("when scheduling after the rebase", func() { - It("should schedule new signatories, and not schedule old signatories", func() { - f := func(n int) bool { - // Clamp n to be positive, and less than 100. - if n < 0 { - n = -n - } - n = n % 100 - - // Setup the scheduler. - oldSignatories := testutil.RandomSignatories() - scheduler := schedule.RoundRobin(oldSignatories) - - // Change to new signatories. - newSignatories := testutil.RandomSignatories() - scheduler.Rebase(newSignatories) - - // Call the Schedule method n time, incrementing the Round - // by exactly 1 each time. - counts := map[id.Signatory]int{} - for i := 0; i < n*len(newSignatories); i++ { - proposer := scheduler.Schedule(block.Height(i), block.Round(0)) - counts[proposer]++ - } - - // Expect the signatories to be scheduled. - for _, signatory := range newSignatories { - Expect(counts[signatory]).To(Equal(n)) - } - // Expect none of the old signatories to be scheduled. - for _, signatory := range oldSignatories { - Expect(counts[signatory]).To(Equal(0)) - } - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the new underlying signatories are nil", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(testutil.RandomSignatories()) - scheduler.Rebase(nil) - proposer := scheduler.Schedule(testutil.RandomHeight(), testutil.RandomRound()) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - - Context("when the new underlying signatories are empty", func() { - It("should return the `InvalidSignatory`", func() { - f := func() bool { - scheduler := schedule.RoundRobin(testutil.RandomSignatories()) - scheduler.Rebase(id.Signatories{}) - proposer := scheduler.Schedule(testutil.RandomHeight(), testutil.RandomRound()) - Expect(proposer).To(Equal(block.InvalidSignatory)) - return true - } - Expect(quick.Check(f, nil)).To(Succeed()) - }) - }) - }) -}) diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go new file mode 100644 index 00000000..d5f0ebd5 --- /dev/null +++ b/scheduler/scheduler.go @@ -0,0 +1,51 @@ +// Package scheduler defines interfaces and implementations for scheduling +// different processes as value proposers. At any given height and round, +// exactly one process is expected to take responsibility for proposing a value, +// and this is determined by the Scheduler. +// +// It is important that all processes agree on the schedule. That is, at any +// given height and round, all processes must arrive at the same decision +// regarding which process is expected to be the proposer. This is most commonly +// done by making the schedule deterministic and locally computable. This means +// that processes do not have to invoke a consensus algorithm in order to agree +// on the schedule (although, this is possible to do, using values from height N +// to agree on the schedule for height N+1). +package scheduler + +import ( + "github.com/renproject/hyperdrive/process" + "github.com/renproject/id" +) + +type RoundRobin struct { + signatories []id.Signatory +} + +// NewRoundRobin returns a Scheduler that uses a simple round-robin scheduling +// algorithm to select a proposer. Round-robin scheduling has the advantage of +// being very easy to implement and understand, but has the disadvantage of +// being unfair. As such, it should be avoided when the proposer is expected to +// receive a reward. +func NewRoundRobin(signatories []id.Signatory) process.Scheduler { + copied := make([]id.Signatory, len(signatories)) + copy(copied[:], signatories) + return &RoundRobin{ + signatories: copied, + } +} + +// Schedule a proposer using the sum of the height and round, modulo the number +// of candidate processes, as an index into the current slice of candidate +// processes. +func (rr *RoundRobin) Schedule(height process.Height, round process.Round) id.Signatory { + if len(rr.signatories) == 0 { + panic("no processes to schedule") + } + if height <= 0 { + panic("invalid height") + } + if round <= process.InvalidRound { + panic("invalid round") + } + return rr.signatories[(uint64(height)+uint64(round))%uint64(len(rr.signatories))] +} diff --git a/schedule/schedule_suite_test.go b/scheduler/scheduler_suite_test.go similarity index 72% rename from schedule/schedule_suite_test.go rename to scheduler/scheduler_suite_test.go index 431a2893..aec6399e 100644 --- a/schedule/schedule_suite_test.go +++ b/scheduler/scheduler_suite_test.go @@ -1,4 +1,4 @@ -package schedule_test +package scheduler_test import ( "testing" @@ -9,5 +9,5 @@ import ( func TestSchedule(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Schedule Suite") + RunSpecs(t, "Scheduler Suite") } diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go new file mode 100644 index 00000000..904dcbc3 --- /dev/null +++ b/scheduler/scheduler_test.go @@ -0,0 +1 @@ +package scheduler_test diff --git a/testutil/block.go b/testutil/block.go deleted file mode 100644 index b629ca3e..00000000 --- a/testutil/block.go +++ /dev/null @@ -1,132 +0,0 @@ -package testutil - -import ( - "math/rand" - "time" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/id" -) - -// RandomBytesSlice returns a random bytes slice. -func RandomBytesSlice() []byte { - length := rand.Intn(256) - slice := make([]byte, length) - _, err := rand.Read(slice) - if err != nil { - panic(err) - } - return slice -} - -// RandomBlockKind returns a random valid block kind. -func RandomBlockKind() block.Kind { - return block.Kind(rand.Intn(3) + 1) -} - -// BlockHeaderJSON is almost a copy of the block.Header struct except all fields are exposed. -// This is for the convenience of initializing and marshaling. -type BlockHeaderJSON struct { - Kind block.Kind `json:"kind"` - ParentHash id.Hash `json:"parentHash"` - BaseHash id.Hash `json:"baseHash"` - TxsRef id.Hash `json:"txsRef"` - PlanRef id.Hash `json:"planRef"` - PrevStateRef id.Hash `json:"prevStateRef"` - Height block.Height `json:"height"` - Round block.Round `json:"round"` - Timestamp block.Timestamp `json:"timestamp"` - Signatories id.Signatories `json:"signatories"` -} - -// ToBlockHeader converts the BlockHeaderJSON object to a block.Header. -func (header BlockHeaderJSON) ToBlockHeader() block.Header { - return block.NewHeader( - header.Kind, - header.ParentHash, - header.BaseHash, - header.TxsRef, - header.PlanRef, - header.PrevStateRef, - header.Height, - header.Round, - header.Timestamp, - header.Signatories, - ) -} - -// RandomBlockHeaderJSON returns a valid BlockHeaderJSON of the given kind block. -func RandomBlockHeaderJSON(kind block.Kind) BlockHeaderJSON { - parentHash := RandomHash() - baseHash := RandomHash() - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - timestamp := block.Timestamp(rand.Intn(int(time.Now().Unix()))) - var signatories id.Signatories - switch kind { - case block.Standard: - signatories = id.Signatories{} - case block.Rebase, block.Base: - for len(signatories) == 0 { - signatories = RandomSignatories() - } - } - return BlockHeaderJSON{ - Kind: kind, - ParentHash: parentHash, - BaseHash: baseHash, - Height: height, - Round: round, - Timestamp: timestamp, - Signatories: signatories, - } -} - -// RandomBlockHeader generates a random block.Header of the given kind which -// guarantee to be valid. -func RandomBlockHeader(kind block.Kind) block.Header { - return RandomBlockHeaderJSON(kind).ToBlockHeader() -} - -// BlockHeaderJSON is almost a copy of the block.Header struct except all fields are exposed. -// This is for the convenience of initializing and marshaling. -type BlockJSON struct { - Hash id.Hash `json:"hash"` - Header block.Header `json:"header"` - Txs block.Txs `json:"txs"` - Plan block.Plan `json:"plan"` - PrevState block.State `json:"prevState"` -} - -func RandomBlock(kind block.Kind) block.Block { - header := RandomBlockHeader(kind) - var txs block.Txs - var plan block.Plan - switch kind { - case block.Standard: - txs = RandomBytesSlice() - if txs.String() == "" { - txs = []byte{} - } - plan = RandomBytesSlice() - if plan.String() == "" { - plan = []byte{} - } - case block.Rebase, block.Base: - txs = []byte{} - plan = []byte{} - } - var prevState block.State = RandomBytesSlice() - if prevState.String() == "" { - prevState = []byte{} - } - return block.New(header, txs, plan, prevState) -} - -func RandomHeight() block.Height { - return block.Height(rand.Int()) -} - -func RandomRound() block.Round { - return block.Round(rand.Int()) -} diff --git a/testutil/id.go b/testutil/id.go deleted file mode 100644 index 40e64041..00000000 --- a/testutil/id.go +++ /dev/null @@ -1,77 +0,0 @@ -package testutil - -import ( - "crypto/rand" - "fmt" - mrand "math/rand" - "time" - - "github.com/renproject/id" -) - -func init() { - mrand.Seed(time.Now().Unix()) -} - -func RandomHash() id.Hash { - hash := id.Hash{} - _, err := rand.Read(hash[:]) - if err != nil { - panic(fmt.Sprintf("cannot create random hash, err = %v", err)) - } - return hash -} - -func RandomHashes() id.Hashes { - length := mrand.Intn(30) - hashes := make(id.Hashes, length) - for i := 0; i < length; i++ { - _, err := rand.Read(hashes[i][:]) - if err != nil { - panic(fmt.Sprintf("cannot create random hash, err = %v", err)) - } - } - return hashes -} - -func RandomSignature() id.Signature { - signature := id.Signature{} - _, err := rand.Read(signature[:]) - if err != nil { - panic(fmt.Sprintf("cannot create random signature, err = %v", err)) - } - return signature -} - -func RandomSignatures() id.Signatures { - length := mrand.Intn(30) - sigs := make(id.Signatures, length) - for i := 0; i < length; i++ { - _, err := rand.Read(sigs[i][:]) - if err != nil { - panic(fmt.Sprintf("cannot create random signature, err = %v", err)) - } - } - return sigs -} - -func RandomSignatory() id.Signatory { - signatory := id.Signatory{} - _, err := rand.Read(signatory[:]) - if err != nil { - panic(fmt.Sprintf("cannot create random signatory, err = %v", err)) - } - return signatory -} - -func RandomSignatories() id.Signatories { - length := mrand.Intn(30) - sigs := make(id.Signatories, length) - for i := 0; i < length; i++ { - _, err := rand.Read(sigs[i][:]) - if err != nil { - panic(fmt.Sprintf("cannot create random signatory, err = %v", err)) - } - } - return sigs -} diff --git a/testutil/process.go b/testutil/process.go deleted file mode 100644 index ebf39979..00000000 --- a/testutil/process.go +++ /dev/null @@ -1,449 +0,0 @@ -package testutil - -import ( - "crypto/ecdsa" - cRand "crypto/rand" - "math/rand" - "sync" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/schedule" - "github.com/renproject/id" - "github.com/renproject/surge" - "github.com/sirupsen/logrus" -) - -func RandomStep() process.Step { - return process.Step(rand.Intn(3) + 1) -} - -// RandomState returns a random `process.State`. -func RandomState() process.State { - step := rand.Intn(3) + 1 - f := rand.Intn(100) + 1 - - return process.State{ - CurrentHeight: block.Height(rand.Int63() + 1), - CurrentRound: block.Round(rand.Int63()), - CurrentStep: process.Step(step), - - LockedBlock: RandomBlock(RandomBlockKind()), - LockedRound: block.Round(rand.Int63()), - ValidBlock: RandomBlock(RandomBlockKind()), - ValidRound: block.Round(rand.Int63()), - - Proposals: process.NewInbox(f, process.ProposeMessageType), - Prevotes: process.NewInbox(f, process.PrevoteMessageType), - Precommits: process.NewInbox(f, process.PrecommitMessageType), - } -} - -func RandomSignedMessage(t process.MessageType) process.Message { - message := RandomMessage(t) - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - if err != nil { - panic(err) - } - if err := process.Sign(message, *privateKey); err != nil { - panic(err) - } - - return message -} - -func RandomMessage(t process.MessageType) process.Message { - switch t { - case process.ProposeMessageType: - return RandomPropose() - case process.PrevoteMessageType: - return RandomPrevote() - case process.PrecommitMessageType: - return RandomPrecommit() - case process.ResyncMessageType: - return RandomResync() - default: - panic("unknown message type") - } -} - -func RandomMessageWithHeightAndRound(height block.Height, round block.Round, t process.MessageType) process.Message { - var msg process.Message - switch t { - case process.ProposeMessageType: - validRound := block.Round(rand.Int63()) - block := RandomBlock(RandomBlockKind()) - msg = process.NewPropose(height, round, block, validRound) - case process.PrevoteMessageType: - hash := RandomHash() - msg = process.NewPrevote(height, round, hash, nil) - case process.PrecommitMessageType: - hash := RandomHash() - msg = process.NewPrecommit(height, round, hash) - case process.ResyncMessageType: - msg = process.NewResync(height, round) - default: - panic("unknown message type") - } - return msg -} - -func RandomSingedMessageWithHeightAndRound(height block.Height, round block.Round, t process.MessageType) process.Message { - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - if err != nil { - panic(err) - } - msg := RandomMessageWithHeightAndRound(height, round, t) - process.Sign(msg, *privateKey) - return msg -} - -func RandomPropose() *process.Propose { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - validRound := block.Round(rand.Int63()) - block := RandomBlock(RandomBlockKind()) - - return process.NewPropose(height, round, block, validRound) -} - -func RandomPrevote() *process.Prevote { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - hash := RandomHash() - nilReasons := make(process.NilReasons) - nilReasons["key1"] = []byte("val1") - nilReasons["key2"] = []byte("val2") - nilReasons["key3"] = []byte("val3") - return process.NewPrevote(height, round, hash, nilReasons) -} - -func RandomPrecommit() *process.Precommit { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - hash := RandomHash() - return process.NewPrecommit(height, round, hash) -} - -func RandomResync() *process.Resync { - height := block.Height(rand.Int63()) - round := block.Round(rand.Int63()) - return process.NewResync(height, round) -} - -func RandomMessageType(includeResync bool) process.MessageType { - var index int - if includeResync { - index = rand.Intn(4) - } else { - index = rand.Intn(3) - } - switch index { - case 0: - return process.ProposeMessageType - case 1: - return process.PrevoteMessageType - case 2: - return process.PrecommitMessageType - case 3: - return process.ResyncMessageType - default: - panic("unexpected message type") - } -} - -func RandomInbox(f int, t process.MessageType) *process.Inbox { - inbox := process.NewInbox(f, t) - numMsgs := rand.Intn(100) - for i := 0; i < numMsgs; i++ { - inbox.Insert(RandomMessage(t)) - } - return inbox -} - -type ProcessOrigin struct { - PrivateKey *ecdsa.PrivateKey - Signatory id.Signatory - Blockchain process.Blockchain - State process.State - BroadcastMessages chan process.Message - CastMessages chan process.Message - - SaveRestorer process.SaveRestorer - Proposer process.Proposer - Validator process.Validator - Scheduler schedule.Scheduler - Broadcaster process.Broadcaster - Timer process.Timer - Observer process.Observer -} - -func NewProcessOrigin(f int) ProcessOrigin { - privateKey, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - if err != nil { - panic(err) - } - sig := id.NewSignatory(privateKey.PublicKey) - broadcastMessages := make(chan process.Message, 128) - castMessages := make(chan process.Message, 128) - signatories := make(id.Signatories, f) - for i := range signatories { - key, err := ecdsa.GenerateKey(crypto.S256(), cRand.Reader) - if err != nil { - panic(err) - } - signatories[i] = id.NewSignatory(key.PublicKey) - } - signatories[0] = sig - - return ProcessOrigin{ - PrivateKey: privateKey, - Signatory: sig, - Blockchain: NewMockBlockchain(signatories), - State: process.DefaultState(f), - BroadcastMessages: broadcastMessages, - CastMessages: castMessages, - - SaveRestorer: NewMockSaveRestorer(), - Proposer: NewMockProposer(privateKey), - Validator: NewMockValidator(nil), - Scheduler: NewMockScheduler(sig), - Broadcaster: NewMockBroadcaster(broadcastMessages, castMessages), - Timer: NewMockTimer(1 * time.Second), - Observer: MockObserver{}, - } -} - -func (p *ProcessOrigin) UpdateState(state process.State) { - p.State = state -} - -func (p ProcessOrigin) ToProcess() *process.Process { - return process.New( - logrus.StandardLogger(), - p.Signatory, - p.Blockchain, - p.State, - p.SaveRestorer, - p.Proposer, - p.Validator, - p.Observer, - p.Broadcaster, - p.Scheduler, - p.Timer, - process.CatchAndIgnore(), - ) -} - -type MockBlockchain struct { - mu *sync.RWMutex - blocks map[block.Height]block.Block - states map[block.Height]block.State -} - -func NewMockBlockchain(signatories id.Signatories) *MockBlockchain { - blocks := map[block.Height]block.Block{} - states := map[block.Height]block.State{} - if signatories != nil { - genesisblock := GenesisBlock(signatories) - blocks[0] = genesisblock - states[0] = block.State{} - } - - return &MockBlockchain{ - mu: new(sync.RWMutex), - blocks: blocks, - states: states, - } -} - -func (bc *MockBlockchain) InsertBlockAtHeight(height block.Height, block block.Block) { - bc.mu.Lock() - defer bc.mu.Unlock() - - bc.blocks[height] = block -} - -func (bc *MockBlockchain) InsertBlockStateAtHeight(height block.Height, state block.State) { - bc.mu.Lock() - defer bc.mu.Unlock() - - bc.states[height] = state -} - -func (bc *MockBlockchain) BlockAtHeight(height block.Height) (block.Block, bool) { - bc.mu.RLock() - defer bc.mu.RUnlock() - - block, ok := bc.blocks[height] - return block, ok -} - -func (bc *MockBlockchain) LatestBaseBlock() block.Block { - bc.mu.RLock() - defer bc.mu.RUnlock() - - block, ok := bc.blocks[0] - if !ok { - panic("no genesis block") - } - return block -} - -func (bc *MockBlockchain) StateAtHeight(height block.Height) (block.State, bool) { - bc.mu.RLock() - defer bc.mu.RUnlock() - - state, ok := bc.states[height] - return state, ok -} - -func (bc *MockBlockchain) BlockExistsAtHeight(height block.Height) bool { - bc.mu.RLock() - defer bc.mu.RUnlock() - - _, ok := bc.blocks[height] - return ok -} - -func (bc *MockBlockchain) LatestBlock(kind block.Kind) block.Block { - bc.mu.RLock() - defer bc.mu.RUnlock() - - h, b := block.Height(-1), block.Block{} - for height, blk := range bc.blocks { - if height > h { - if blk.Header().Kind() == kind || kind == block.Invalid { - b = blk - h = height - } - } - } - - return b -} - -type MockSaveRestorer struct { -} - -func NewMockSaveRestorer() process.SaveRestorer { - return &MockSaveRestorer{} -} - -func (m *MockSaveRestorer) Save(state *process.State) { -} -func (m *MockSaveRestorer) Restore(state *process.State) { -} - -type MockProposer struct { - Key *ecdsa.PrivateKey -} - -func NewMockProposer(key *ecdsa.PrivateKey) process.Proposer { - return &MockProposer{Key: key} -} - -func (m *MockProposer) BlockProposal(height block.Height, round block.Round) block.Block { - header := RandomBlockHeaderJSON(RandomBlockKind()) - header.Height = height - header.Round = round - return block.New(header.ToBlockHeader(), RandomBytesSlice(), RandomBytesSlice(), RandomBytesSlice()) -} - -type MockValidator struct { - valid error -} - -func NewMockValidator(valid error) process.Validator { - return MockValidator{valid: valid} -} - -func (m MockValidator) IsBlockValid(block.Block, bool) (process.NilReasons, error) { - return nil, m.valid -} - -type MockObserver struct { -} - -func (m MockObserver) DidCommitBlock(block.Height) { -} - -func (m MockObserver) DidReceiveSufficientNilPrevotes(process.Messages, int) { -} - -type MockBroadcaster struct { - broadcastMessages chan<- process.Message - castMessages chan<- process.Message -} - -func NewMockBroadcaster(broadcastMessages, castMessages chan<- process.Message) process.Broadcaster { - return &MockBroadcaster{ - broadcastMessages: broadcastMessages, - castMessages: castMessages, - } -} - -func (m *MockBroadcaster) Broadcast(message process.Message) { - m.broadcastMessages <- message -} - -func (m *MockBroadcaster) Cast(to id.Signatory, message process.Message) { - m.castMessages <- message -} - -type MockScheduler struct { - sig id.Signatory -} - -func NewMockScheduler(sig id.Signatory) schedule.Scheduler { - return &MockScheduler{sig: sig} -} - -func (m *MockScheduler) Schedule(block.Height, block.Round) id.Signatory { - return m.sig -} - -func (m *MockScheduler) Rebase(sigs id.Signatories) { - panic("MockScheduler.Rebase is not supported") -} - -type MockTimer struct { - timeout time.Duration -} - -func NewMockTimer(timeout time.Duration) process.Timer { - return &MockTimer{ - timeout: timeout, - } -} - -func (timer *MockTimer) Timeout(step process.Step, round block.Round) time.Duration { - return timer.timeout -} - -func GetStateFromProcess(p *process.Process, f int) process.State { - data, err := surge.ToBinary(p) - if err != nil { - panic(err) - } - state := process.DefaultState(f) - if err := surge.FromBinary(data, &state); err != nil { - panic(err) - } - return state -} - -func GenesisBlock(signatories id.Signatories) block.Block { - header := block.NewHeader( - block.Base, - id.Hash{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, - id.Hash{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, - id.Hash{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, - id.Hash{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, - id.Hash{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, - 0, 0, 0, signatories, - ) - return block.New(header, nil, nil, nil) -} diff --git a/testutil/replica/replica.go b/testutil/replica/replica.go deleted file mode 100644 index c041be16..00000000 --- a/testutil/replica/replica.go +++ /dev/null @@ -1,249 +0,0 @@ -package testutil_replica - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rand" - "crypto/sha256" - "fmt" - mrand "math/rand" - "sync" - "time" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/replica" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/id" - "github.com/renproject/phi" - "github.com/renproject/surge" -) - -func Contain(list []int, target int) bool { - for _, num := range list { - if num == target { - return true - } - } - return false -} - -func SleepRandomSeconds(min, max int) { - if max == min { - time.Sleep(time.Duration(min) * time.Second) - } else { - duration := time.Duration(mrand.Intn(max-min) + min) - time.Sleep(duration * time.Second) - } -} - -func RandomShard() replica.Shard { - shard := replica.Shard{} - _, err := rand.Read(shard[:]) - if err != nil { - panic(fmt.Sprintf("cannot create random shard, err = %v", err)) - } - return shard -} - -type MockBlockIterator struct { - store *MockPersistentStorage - timeout bool -} - -func NewMockBlockIterator(store *MockPersistentStorage, timeout bool) *MockBlockIterator { - return &MockBlockIterator{ - store: store, - timeout: timeout, - } -} - -func (m *MockBlockIterator) NextBlock(kind block.Kind, height block.Height, shard replica.Shard) (block.Txs, block.Plan, block.State) { - // Sleep before continuing if we are expected to timeout. - if m.timeout { - time.Sleep(5 * time.Second) - } - - blockchain := m.store.MockBlockchain(shard) - state, ok := blockchain.StateAtHeight(height - 1) - if !ok { - return testutil.RandomBytesSlice(), testutil.RandomBytesSlice(), nil - } - - switch kind { - case block.Standard: - return testutil.RandomBytesSlice(), testutil.RandomBytesSlice(), state - default: - panic("unknown block kind") - } -} - -func (m *MockBlockIterator) MissedBaseBlocksInRange(begin, end id.Hash) int { - return 0 // MockBlockIterator does not support rebasing. -} - -type MockValidator struct { - store *MockPersistentStorage -} - -func NewMockValidator(store *MockPersistentStorage) replica.Validator { - return &MockValidator{ - store: store, - } -} - -func (m *MockValidator) IsBlockValid(b block.Block, checkHistory bool, shard replica.Shard) (process.NilReasons, error) { - height := b.Header().Height() - prevState := b.PreviousState() - - blockchain := m.store.MockBlockchain(shard) - if !checkHistory { - return nil, nil - } - - state, ok := blockchain.StateAtHeight(height - 1) - if !ok { - return nil, fmt.Errorf("failed to get state at height %d", height-1) - } - if !bytes.Equal(prevState, state) { - return nil, fmt.Errorf("invalid previous state") - } - return nil, nil -} - -type MockObserver struct { - store *MockPersistentStorage - isSignatory bool -} - -func NewMockObserver(store *MockPersistentStorage, isSignatory bool) replica.Observer { - return &MockObserver{ - store: store, - isSignatory: isSignatory, - } -} - -func (m MockObserver) DidCommitBlock(height block.Height, shard replica.Shard) { - blockchain := m.store.MockBlockchain(shard) - b, ok := blockchain.BlockAtHeight(height) - if !ok { - panic("DidCommitBlock should be called only when the block has been added to storage") - } - digest := sha256.Sum256(b.Txs()) - blockchain.InsertBlockStateAtHeight(height, digest[:]) - - // Insert executed state of the previous height - prevBlock, ok := blockchain.BlockAtHeight(height - 1) - if !ok { - panic(fmt.Sprintf("cannot find block of height %v, %v", height-1, prevBlock)) - } - blockchain.InsertBlockStateAtHeight(height-1, prevBlock.PreviousState()) -} - -func (observer *MockObserver) IsSignatory(replica.Shard) bool { - return observer.isSignatory -} - -func (observer *MockObserver) DidReceiveSufficientNilPrevotes(process.Messages, int) { -} - -type MockBroadcaster struct { - min, max int - - cons map[id.Signatory]chan []byte - active map[id.Signatory]bool - activeMu *sync.RWMutex - - signatories map[id.Signatory]int -} - -func NewMockBroadcaster(keys []*ecdsa.PrivateKey, min, max int) *MockBroadcaster { - cons := map[id.Signatory]chan []byte{} - signatories := map[id.Signatory]int{} - for i, key := range keys { - sig := id.NewSignatory(key.PublicKey) - messages := make(chan []byte, 1024) - cons[sig] = messages - signatories[sig] = i - } - - return &MockBroadcaster{ - min: min, - max: max, - - cons: cons, - active: map[id.Signatory]bool{}, - activeMu: new(sync.RWMutex), - signatories: signatories, - } -} - -func (m *MockBroadcaster) Broadcast(message replica.Message) { - func() { - // If the sender is offline, it cannot send messages to other nodes. - m.activeMu.RLock() - defer m.activeMu.RUnlock() - - if !m.active[message.Message.Signatory()] { - return - } - }() - - messageBytes, err := surge.ToBinary(message) - if err != nil { - panic(err) - } - phi.ParForAll(m.cons, func(to id.Signatory) { - m.sendMessage(to, messageBytes) - }) -} - -func (m *MockBroadcaster) Cast(to id.Signatory, message replica.Message) { - func() { - // If the sender is offline, it cannot send messages to other nodes. - m.activeMu.RLock() - defer m.activeMu.RUnlock() - - if !m.active[message.Message.Signatory()] { - return - } - }() - - messageBytes, err := surge.ToBinary(message) - if err != nil { - panic(err) - } - m.sendMessage(to, messageBytes) -} - -func (m *MockBroadcaster) sendMessage(receiver id.Signatory, message []byte) { - messages := m.cons[receiver] - time.Sleep(time.Duration(mrand.Intn(m.max-m.min)+m.min) * time.Millisecond) // Simulate network latency. - - // If the receiver is offline, it cannot receive any messages from other - // nodes. - m.activeMu.RLock() - defer m.activeMu.RUnlock() - - if m.active[receiver] { - go func() { messages <- message }() - } -} - -func (m *MockBroadcaster) Messages(sig id.Signatory) chan []byte { - return m.cons[sig] -} - -func (m *MockBroadcaster) EnablePeer(sig id.Signatory) { - m.activeMu.Lock() - defer m.activeMu.Unlock() - - m.active[sig] = true -} - -func (m *MockBroadcaster) DisablePeer(sig id.Signatory) { - m.activeMu.Lock() - defer m.activeMu.Unlock() - - m.active[sig] = false -} diff --git a/testutil/replica/storage.go b/testutil/replica/storage.go deleted file mode 100644 index 14abf961..00000000 --- a/testutil/replica/storage.go +++ /dev/null @@ -1,105 +0,0 @@ -package testutil_replica - -import ( - "fmt" - "sync" - - "github.com/renproject/hyperdrive/block" - "github.com/renproject/hyperdrive/process" - "github.com/renproject/hyperdrive/replica" - "github.com/renproject/hyperdrive/testutil" - "github.com/renproject/surge" -) - -type MockPersistentStorage struct { - mu *sync.RWMutex - processes map[replica.Shard][]byte - blockchains map[replica.Shard]*testutil.MockBlockchain -} - -func NewMockPersistentStorage(shards replica.Shards) *MockPersistentStorage { - blockchains := map[replica.Shard]*testutil.MockBlockchain{} - for _, shard := range shards { - blockchains[shard] = testutil.NewMockBlockchain(nil) - } - return &MockPersistentStorage{ - mu: new(sync.RWMutex), - processes: map[replica.Shard][]byte{}, - blockchains: blockchains, - } -} - -func (store *MockPersistentStorage) SaveState(state *process.State, shard replica.Shard) { - data, err := surge.ToBinary(state) - if err != nil { - panic(fmt.Sprintf("failed to marshal state: %v", err)) - - } - store.mu.Lock() - defer store.mu.Unlock() - store.processes[shard] = data -} - -func (store *MockPersistentStorage) RestoreState(state *process.State, shard replica.Shard) { - store.mu.RLock() - defer store.mu.RUnlock() - - data, ok := store.processes[shard] - if !ok { - return - } - if err := surge.FromBinary(data, state); err != nil { - panic(fmt.Sprintf("failed to unmarshal state: %v", err)) - } -} - -func (store *MockPersistentStorage) Blockchain(shard replica.Shard) process.Blockchain { - store.mu.Lock() - defer store.mu.Unlock() - - _, ok := store.blockchains[shard] - if !ok { - store.blockchains[shard] = testutil.NewMockBlockchain(nil) - } - return store.blockchains[shard] -} - -func (store *MockPersistentStorage) MockBlockchain(shard replica.Shard) *testutil.MockBlockchain { - store.mu.Lock() - defer store.mu.Unlock() - - _, ok := store.blockchains[shard] - if !ok { - store.blockchains[shard] = testutil.NewMockBlockchain(nil) - } - return store.blockchains[shard] -} - -func (store *MockPersistentStorage) LatestBlock(shard replica.Shard) block.Block { - store.mu.RLock() - defer store.mu.RUnlock() - - blockchain := store.blockchains[shard] - return blockchain.LatestBlock(block.Invalid) -} - -func (store *MockPersistentStorage) LatestBaseBlock(shard replica.Shard) block.Block { - store.mu.Lock() - defer store.mu.Unlock() - - blockchain, ok := store.blockchains[shard] - if !ok { - return block.InvalidBlock - } - return blockchain.LatestBlock(block.Base) -} - -func (store *MockPersistentStorage) Init(gb block.Block) { - store.mu.Lock() - defer store.mu.Unlock() - - for _, bc := range store.blockchains { - bc.InsertBlockAtHeight(block.Height(0), gb) - bc.InsertBlockStateAtHeight(block.Height(0), nil) - } -} diff --git a/timer/opt.go b/timer/opt.go new file mode 100644 index 00000000..d609bf3e --- /dev/null +++ b/timer/opt.go @@ -0,0 +1,58 @@ +package timer + +import ( + "io" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + DefaultTimeout = 20 * time.Second + DefaultTimeoutScaling = 0.5 +) + +type Options struct { + Logger logrus.FieldLogger + Timeout time.Duration + TimeoutScaling float64 +} + +func DefaultOptions() Options { + return Options{ + Logger: loggerWithFields(logrus.New()), + Timeout: DefaultTimeout, + TimeoutScaling: DefaultTimeoutScaling, + } +} + +func (opts Options) WithLogLevel(level logrus.Level) Options { + logger := logrus.New() + logger.SetLevel(level) + opts.Logger = loggerWithFields(logger) + return opts +} + +func (opts Options) WithLogOutput(output io.Writer) Options { + logger := logrus.New() + logger.SetOutput(output) + opts.Logger = loggerWithFields(logger) + return opts +} + +func (opts Options) WithTimeout(timeout time.Duration) Options { + opts.Timeout = timeout + return opts +} + +func (opts Options) WithTimeoutScaling(timeoutScaling float64) Options { + opts.TimeoutScaling = timeoutScaling + return opts +} + +func loggerWithFields(logger *logrus.Logger) logrus.FieldLogger { + return logger. + WithField("lib", "hyperdrive"). + WithField("pkg", "timer"). + WithField("com", "timer") +} diff --git a/timer/opt_test.go b/timer/opt_test.go new file mode 100644 index 00000000..44754904 --- /dev/null +++ b/timer/opt_test.go @@ -0,0 +1 @@ +package timer_test diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 00000000..6dbf65db --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,53 @@ +package timer + +import ( + "time" + + "github.com/renproject/hyperdrive/process" +) + +type Timeout struct { + Height process.Height + Round process.Round +} + +type LinearTimer struct { + opts Options + onTimeoutPropose chan<- Timeout + onTimeoutPrevote chan<- Timeout + onTimeoutPrecommit chan<- Timeout +} + +func NewLinearTimer(opts Options, onTimeoutPropose, onTimeoutPrevote, onTimeoutPrecommit chan<- Timeout) process.Timer { + return &LinearTimer{ + opts: opts, + onTimeoutPropose: onTimeoutPropose, + onTimeoutPrevote: onTimeoutPrevote, + onTimeoutPrecommit: onTimeoutPrecommit, + } +} + +func (t *LinearTimer) TimeoutPropose(height process.Height, round process.Round) { + go func() { + time.Sleep(t.timeoutDuration(height, round)) + t.onTimeoutPropose <- Timeout{Height: height, Round: round} + }() +} + +func (t *LinearTimer) TimeoutPrevote(height process.Height, round process.Round) { + go func() { + time.Sleep(t.timeoutDuration(height, round)) + t.onTimeoutPrevote <- Timeout{Height: height, Round: round} + }() +} + +func (t *LinearTimer) TimeoutPrecommit(height process.Height, round process.Round) { + go func() { + time.Sleep(t.timeoutDuration(height, round)) + t.onTimeoutPrecommit <- Timeout{Height: height, Round: round} + }() +} + +func (t *LinearTimer) timeoutDuration(height process.Height, round process.Round) time.Duration { + return t.opts.Timeout + t.opts.Timeout*time.Duration(float64(round)*t.opts.TimeoutScaling) +} diff --git a/block/block_suite_test.go b/timer/timer_suite_test.go similarity index 58% rename from block/block_suite_test.go rename to timer/timer_suite_test.go index 86e0a43f..db0ddd47 100644 --- a/block/block_suite_test.go +++ b/timer/timer_suite_test.go @@ -1,4 +1,4 @@ -package block_test +package timer_test import ( "testing" @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestBlock(t *testing.T) { +func TestTimer(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Block Suite") + RunSpecs(t, "Timer Suite") } diff --git a/timer/timer_test.go b/timer/timer_test.go new file mode 100644 index 00000000..7159c21a --- /dev/null +++ b/timer/timer_test.go @@ -0,0 +1 @@ +package timer_test \ No newline at end of file