Skip to content

Commit f345849

Browse files
authored
Try to collect an empty notarization after sending an empty vote (#94)
A node might be the last node among a quorum that times out. It therefore receives the empty votes before sending the empty vote itself, so it should check if it is possible to assemble an empty notarization right after sending the empty vote, because otherwise it might never get a trigger to check if an empty notarization can be assembled. Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
1 parent b158218 commit f345849

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

epoch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,11 @@ func (e *Epoch) triggerProposalWaitTimeExpired(round uint64) {
15871587
emptyVotes.votes[string(e.ID)] = &signedEV
15881588

15891589
e.Comm.Broadcast(&Message{EmptyVoteMessage: &signedEV})
1590+
1591+
if err := e.maybeAssembleEmptyNotarization(); err != nil {
1592+
e.Logger.Error("Failed assembling empty notarization", zap.Error(err))
1593+
e.haltedError = err
1594+
}
15901595
}
15911596

15921597
func (e *Epoch) monitorProgress(round uint64) {

epoch_failover_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,108 @@ import (
1515
"go.uber.org/zap/zapcore"
1616
)
1717

18+
func TestEpochLeaderFailoverReceivesEmptyVotesEarly(t *testing.T) {
19+
l := testutil.MakeLogger(t, 1)
20+
21+
bb := &testBlockBuilder{out: make(chan *testBlock, 1), blockShouldBeBuilt: make(chan struct{}, 1)}
22+
storage := newInMemStorage()
23+
24+
nodes := []NodeID{{1}, {2}, {3}, {4}}
25+
quorum := Quorum(len(nodes))
26+
27+
wal := newTestWAL(t)
28+
29+
start := time.Now()
30+
conf := EpochConfig{
31+
MaxProposalWait: DefaultMaxProposalWaitTime,
32+
StartTime: start,
33+
Logger: l,
34+
ID: nodes[0],
35+
Signer: &testSigner{},
36+
WAL: wal,
37+
Verifier: &testVerifier{},
38+
Storage: storage,
39+
Comm: noopComm(nodes),
40+
BlockBuilder: bb,
41+
SignatureAggregator: &testSignatureAggregator{},
42+
}
43+
44+
e, err := NewEpoch(conf)
45+
require.NoError(t, err)
46+
47+
require.NoError(t, e.Start())
48+
49+
// Run through 3 blocks, to make the block proposals be:
50+
// 1 --> 2 --> 3 --> X (node 4 doesn't propose a block)
51+
52+
// Then, don't do anything and wait for our node
53+
// to start complaining about a block not being notarized
54+
55+
for round := uint64(0); round < 3; round++ {
56+
notarizeAndFinalizeRound(t, nodes, round, round, e, bb, quorum, storage, false)
57+
}
58+
59+
lastBlock, _, ok := storage.Retrieve(storage.Height() - 1)
60+
require.True(t, ok)
61+
62+
prev := lastBlock.BlockHeader().Digest
63+
64+
emptyBlockMd := ProtocolMetadata{
65+
Round: 3,
66+
Seq: 2,
67+
Prev: prev,
68+
}
69+
70+
emptyVoteFrom1 := createEmptyVote(emptyBlockMd, nodes[1])
71+
emptyVoteFrom2 := createEmptyVote(emptyBlockMd, nodes[2])
72+
73+
e.HandleMessage(&Message{
74+
EmptyVoteMessage: emptyVoteFrom1,
75+
}, nodes[1])
76+
e.HandleMessage(&Message{
77+
EmptyVoteMessage: emptyVoteFrom2,
78+
}, nodes[2])
79+
80+
bb.blockShouldBeBuilt <- struct{}{}
81+
82+
waitForBlockProposerTimeout(t, e, start)
83+
84+
wal.lock.Lock()
85+
walContent, err := wal.ReadAll()
86+
require.NoError(t, err)
87+
wal.lock.Unlock()
88+
89+
rawEmptyVote, rawEmptyNotarization, rawProposal := walContent[len(walContent)-3], walContent[len(walContent)-2], walContent[len(walContent)-1]
90+
emptyVote, err := ParseEmptyVoteRecord(rawEmptyVote)
91+
require.NoError(t, err)
92+
require.Equal(t, createEmptyVote(emptyBlockMd, nodes[0]).Vote, emptyVote)
93+
94+
emptyNotarization, err := EmptyNotarizationFromRecord(rawEmptyNotarization, &testQCDeserializer{t: t})
95+
require.NoError(t, err)
96+
require.Equal(t, emptyVoteFrom1.Vote, emptyNotarization.Vote)
97+
require.Equal(t, uint64(3), emptyNotarization.Vote.Round)
98+
require.Equal(t, uint64(2), emptyNotarization.Vote.Seq)
99+
require.Equal(t, uint64(3), storage.Height())
100+
101+
header, _, err := ParseBlockRecord(rawProposal)
102+
require.NoError(t, err)
103+
require.Equal(t, uint64(4), header.Round)
104+
require.Equal(t, uint64(3), header.Seq)
105+
106+
// Ensure our node proposes block with sequence 3 for round 4
107+
block := <-bb.out
108+
109+
for i := 1; i <= quorum; i++ {
110+
injectTestFinalization(t, e, block, nodes[i])
111+
}
112+
113+
block2 := storage.waitForBlockCommit(3)
114+
require.Equal(t, block, block2)
115+
require.Equal(t, uint64(4), storage.Height())
116+
require.Equal(t, uint64(4), block2.BlockHeader().Round)
117+
require.Equal(t, uint64(3), block2.BlockHeader().Seq)
118+
}
119+
18120
func TestEpochLeaderFailover(t *testing.T) {
19121
l := testutil.MakeLogger(t, 1)
20122

0 commit comments

Comments
 (0)