Skip to content

Commit 40d1c98

Browse files
author
Alexander Hentschel
committed
wip
1 parent 02932e0 commit 40d1c98

File tree

12 files changed

+185
-1127
lines changed

12 files changed

+185
-1127
lines changed

consensus/follower.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"github.com/rs/zerolog"
77

88
"github.com/onflow/flow-go/consensus/hotstuff"
9-
"github.com/onflow/flow-go/consensus/hotstuff/follower"
109
"github.com/onflow/flow-go/consensus/hotstuff/validator"
1110
"github.com/onflow/flow-go/consensus/recovery"
1211
"github.com/onflow/flow-go/model/flow"
@@ -16,9 +15,16 @@ import (
1615

1716
// TODO: this needs to be integrated with proper configuration and bootstrapping.
1817

19-
func NewFollower(log zerolog.Logger, committee hotstuff.DynamicCommittee, headers storage.Headers, updater module.Finalizer,
20-
verifier hotstuff.Verifier, notifier hotstuff.FinalizationConsumer, rootHeader *flow.Header,
21-
rootQC *flow.QuorumCertificate, finalized *flow.Header, pending []*flow.Header,
18+
func NewFollower(log zerolog.Logger,
19+
committee hotstuff.DynamicCommittee,
20+
headers storage.Headers,
21+
updater module.Finalizer,
22+
verifier hotstuff.Verifier,
23+
notifier hotstuff.FinalizationConsumer,
24+
rootHeader *flow.Header,
25+
rootQC *flow.QuorumCertificate,
26+
finalized *flow.Header,
27+
pending []*flow.Header,
2228
) (*hotstuff.FollowerLoop, error) {
2329

2430
forks, err := NewForks(finalized, headers, updater, notifier, rootHeader, rootQC)
@@ -35,14 +41,8 @@ func NewFollower(log zerolog.Logger, committee hotstuff.DynamicCommittee, header
3541
return nil, fmt.Errorf("could not recover hotstuff follower state: %w", err)
3642
}
3743

38-
// initialize the follower logic
39-
logic, err := follower.New(log, validator, forks)
40-
if err != nil {
41-
return nil, fmt.Errorf("could not create follower logic: %w", err)
42-
}
43-
4444
// initialize the follower loop
45-
loop, err := hotstuff.NewFollowerLoop(log, logic)
45+
loop, err := hotstuff.NewFollowerLoop(log, forks)
4646
if err != nil {
4747
return nil, fmt.Errorf("could not create follower loop: %w", err)
4848
}

consensus/hotstuff/follower/follower.go

Lines changed: 0 additions & 82 deletions
This file was deleted.

consensus/hotstuff/follower_loop.go

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hotstuff
22

33
import (
4+
"fmt"
45
"time"
56

67
"github.com/rs/zerolog"
@@ -18,24 +19,28 @@ import (
1819
// Concurrency safe.
1920
type FollowerLoop struct {
2021
*component.ComponentManager
21-
log zerolog.Logger
22-
followerLogic FollowerLogic
23-
proposals chan *model.Proposal
22+
log zerolog.Logger
23+
certifiedBlocks chan *model.CertifiedBlock
24+
forks Forks
2425
}
2526

2627
var _ component.Component = (*FollowerLoop)(nil)
2728
var _ module.HotStuffFollower = (*FollowerLoop)(nil)
2829

29-
// NewFollowerLoop creates an instance of EventLoop
30-
func NewFollowerLoop(log zerolog.Logger, followerLogic FollowerLogic) (*FollowerLoop, error) {
30+
// NewFollowerLoop creates an instance of HotStuffFollower
31+
func NewFollowerLoop(log zerolog.Logger, forks Forks) (*FollowerLoop, error) {
32+
// We can't afford to drop messages since it undermines liveness, but we also want to avoid blocking
33+
// the compliance layer. Generally, the follower loop should be able to process inbound blocks faster
34+
// than they pass through the compliance layer. Nevertheless, in the worst case we will fill the
35+
// channel and block the compliance layer's workers. Though, that should happen only if compliance
36+
// engine receives large number of blocks in short periods of time (e.g. when catching up for).
3137
// TODO(active-pacemaker) add metrics for length of inbound channels
32-
// we will use a buffered channel to avoid blocking of caller
33-
proposals := make(chan *model.Proposal, 1000)
38+
certifiedBlocks := make(chan *model.CertifiedBlock, 1000)
3439

3540
fl := &FollowerLoop{
36-
log: log,
37-
followerLogic: followerLogic,
38-
proposals: proposals,
41+
log: log.With().Str("hotstuff", "FollowerLoop").Logger(),
42+
certifiedBlocks: certifiedBlocks,
43+
forks: forks,
3944
}
4045

4146
fl.ComponentManager = component.NewComponentManagerBuilder().
@@ -45,27 +50,36 @@ func NewFollowerLoop(log zerolog.Logger, followerLogic FollowerLogic) (*Follower
4550
return fl, nil
4651
}
4752

48-
// SubmitProposal feeds a new block proposal (header) into the FollowerLoop.
49-
// This method blocks until the proposal is accepted to the event queue.
53+
// AddCertifiedBlock appends the given certified block to the tree of pending
54+
// blocks and updates the latest finalized block (if finalization progressed).
55+
// Unless the parent is below the pruning threshold (latest finalized view), we
56+
// require that the parent has previously been added.
5057
//
51-
// Block proposals must be submitted in order, i.e. a proposal's parent must
52-
// have been previously processed by the FollowerLoop.
53-
func (fl *FollowerLoop) SubmitProposal(proposal *model.Proposal) {
58+
// Notes:
59+
// - Under normal operations, this method is non-blocking. The follower internally
60+
// queues incoming blocks and processes them in its own worker routine. However,
61+
// when the inbound queue is, we block until there is space in the queue. This
62+
// behaviours is intentional, because we cannot drop blocks (otherwise, we would
63+
// cause disconnected blocks). Instead we simply block the compliance layer to
64+
// avoid any pathological edge cases.
65+
// - Blocks whose views are below the latest finalized view are dropped.
66+
// - Inputs are idempotent (repetitions are no-ops).
67+
func (fl *FollowerLoop) AddCertifiedBlock(certifiedBlock *model.CertifiedBlock) {
5468
received := time.Now()
5569

5670
select {
57-
case fl.proposals <- proposal:
71+
case fl.certifiedBlocks <- certifiedBlock:
5872
case <-fl.ComponentManager.ShutdownSignal():
5973
return
6074
}
6175

6276
// the busy duration is measured as how long it takes from a block being
6377
// received to a block being handled by the event handler.
6478
busyDuration := time.Since(received)
65-
fl.log.Debug().Hex("block_id", logging.ID(proposal.Block.BlockID)).
66-
Uint64("view", proposal.Block.View).
67-
Dur("busy_duration", busyDuration).
68-
Msg("busy duration to handle a proposal")
79+
fl.log.Debug().Hex("block_id", logging.ID(certifiedBlock.ID())).
80+
Uint64("view", certifiedBlock.View()).
81+
Dur("wait_time", busyDuration).
82+
Msg("wait time to queue inbound certified block")
6983
}
7084

7185
// loop will synchronously process all events.
@@ -83,12 +97,15 @@ func (fl *FollowerLoop) loop(ctx irrecoverable.SignalerContext, ready component.
8397
}
8498

8599
select {
86-
case p := <-fl.proposals:
87-
err := fl.followerLogic.AddBlock(p)
100+
case b := <-fl.certifiedBlocks:
101+
err := fl.forks.AddCertifiedBlock(b)
102+
if err != nil {
103+
}
88104
if err != nil { // all errors are fatal
105+
err = fmt.Errorf("finalization logic failes to process certified block %v: %w", b.ID(), err)
89106
fl.log.Error().
90-
Hex("block_id", logging.ID(p.Block.BlockID)).
91-
Uint64("view", p.Block.View).
107+
Hex("block_id", logging.ID(b.ID())).
108+
Uint64("view", b.View()).
92109
Err(err).
93110
Msg("irrecoverable follower loop error")
94111
ctx.Throw(err)

consensus/hotstuff/forks.go

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import (
55
"github.com/onflow/flow-go/model/flow"
66
)
77

8-
// Forks maintains an in-memory data-structure of all proposals whose view-number is larger or equal to
8+
// FinalityProof represents a finality proof for a Block. By convention, a FinalityProof
9+
// is immutable. Finality in Jolteon/HotStuff is determined by the 2-chain rule:
10+
//
11+
// There exists a _certified_ block C, such that Block.View + 1 = C.View
12+
type FinalityProof struct {
13+
Block *model.Block
14+
CertifiedChild model.CertifiedBlock
15+
}
16+
17+
// Forks maintains an in-memory data-structure of all blocks whose view-number is larger or equal to
918
// the latest finalized block. The latest finalized block is defined as the finalized block with the largest view number.
1019
// When adding blocks, Forks automatically updates its internal state (including finalized blocks).
1120
// Furthermore, blocks whose view number is smaller than the latest finalized block are pruned automatically.
@@ -16,29 +25,71 @@ import (
1625
// and ignore the block.
1726
type Forks interface {
1827

19-
// GetProposalsForView returns all BlockProposals at the given view number.
20-
GetProposalsForView(view uint64) []*model.Proposal
28+
// GetBlocksForView returns all known blocks for the given view
29+
GetBlocksForView(view uint64) []*model.Block
2130

22-
// GetProposal returns (BlockProposal, true) if the block with the specified
23-
// id was found (nil, false) otherwise.
24-
GetProposal(id flow.Identifier) (*model.Proposal, bool)
31+
// GetBlock returns (BlockProposal, true) if the block with the specified
32+
// id was found and (nil, false) otherwise.
33+
GetBlock(blockID flow.Identifier) (*model.Block, bool)
2534

2635
// FinalizedView returns the largest view number where a finalized block is known
2736
FinalizedView() uint64
2837

2938
// FinalizedBlock returns the finalized block with the largest view number
3039
FinalizedBlock() *model.Block
3140

32-
// NewestView returns the largest view number of all proposals that were added to Forks.
33-
NewestView() uint64
34-
35-
// AddProposal adds the block proposal to Forks. This might cause an update of the finalized block
36-
// and pruning of older blocks.
37-
// Handles duplicated addition of blocks (at the potential cost of additional computation time).
38-
// PREREQUISITE:
39-
// Forks must be able to connect `proposal` to its latest finalized block
40-
// (without missing interim ancestors). Otherwise, an exception is raised.
41-
// Expected errors during normal operations:
42-
// * model.ByzantineThresholdExceededError - new block results in conflicting finalized blocks
43-
AddProposal(proposal *model.Proposal) error
41+
// FinalityProof returns the latest finalized block and a certified child from
42+
// the subsequent view, which proves finality.
43+
// CAUTION: method returns (nil, false), when Forks has not yet finalized any
44+
// blocks beyond the finalized root block it was initialized with.
45+
FinalityProof() (*FinalityProof, bool)
46+
47+
// AddProposal appends the given block to the tree of pending
48+
// blocks and updates the latest finalized block (if applicable). Unless the parent is
49+
// below the pruning threshold (latest finalized view), we require that the parent is
50+
// already stored in Forks. Calling this method with previously processed blocks
51+
// leaves the consensus state invariant (though, it will potentially cause some
52+
// duplicate processing).
53+
// Notes:
54+
// - Method `AddCertifiedBlock(..)` should be used preferably, if a QC certifying
55+
// `block` is already known. This is generally the case for the consensus follower.
56+
// Method `AddProposal` is intended for active consensus participants, which fully
57+
// validate blocks (incl. payload), i.e. QCs are processed as part of validated proposals.
58+
//
59+
// Possible error returns:
60+
// - model.MissingBlockError if the parent does not exist in the forest (but is above
61+
// the pruned view). From the perspective of Forks, this error is benign (no-op).
62+
// - model.InvalidBlockError if the block is invalid (see `Forks.EnsureBlockIsValidExtension`
63+
// for details). From the perspective of Forks, this error is benign (no-op). However, we
64+
// assume all blocks are fully verified, i.e. they should satisfy all consistency
65+
// requirements. Hence, this error is likely an indicator of a bug in the compliance layer.
66+
// - model.ByzantineThresholdExceededError if conflicting QCs or conflicting finalized
67+
// blocks have been detected (violating a foundational consensus guarantees). This
68+
// indicates that there are 1/3+ Byzantine nodes (weighted by stake) in the network,
69+
// breaking the safety guarantees of HotStuff (or there is a critical bug / data
70+
// corruption). Forks cannot recover from this exception.
71+
// - All other errors are potential symptoms of bugs or state corruption.
72+
AddProposal(proposal *model.Block) error
73+
74+
// AddCertifiedBlock appends the given certified block to the tree of pending
75+
// blocks and updates the latest finalized block (if finalization progressed).
76+
// Unless the parent is below the pruning threshold (latest finalized view), we
77+
// require that the parent is already stored in Forks. Calling this method with
78+
// previously processed blocks leaves the consensus state invariant (though,
79+
// it will potentially cause some duplicate processing).
80+
//
81+
// Possible error returns:
82+
// - model.MissingBlockError if the parent does not exist in the forest (but is above
83+
// the pruned view). From the perspective of Forks, this error is benign (no-op).
84+
// - model.InvalidBlockError if the block is invalid (see `Forks.EnsureBlockIsValidExtension`
85+
// for details). From the perspective of Forks, this error is benign (no-op). However, we
86+
// assume all blocks are fully verified, i.e. they should satisfy all consistency
87+
// requirements. Hence, this error is likely an indicator of a bug in the compliance layer.
88+
// - model.ByzantineThresholdExceededError if conflicting QCs or conflicting finalized
89+
// blocks have been detected (violating a foundational consensus guarantees). This
90+
// indicates that there are 1/3+ Byzantine nodes (weighted by stake) in the network,
91+
// breaking the safety guarantees of HotStuff (or there is a critical bug / data
92+
// corruption). Forks cannot recover from this exception.
93+
// - All other errors are potential symptoms of bugs or state corruption.
94+
AddCertifiedBlock(certifiedBlock *model.CertifiedBlock) error
4495
}

0 commit comments

Comments
 (0)