Skip to content

Commit b0960c6

Browse files
authored
Rebroadcast finalizations for past notarized blocks when finalizing later blocks (#124)
Currently, if a node misses a finalization message or a finalization certificate message for a round, it will not try to re-broadcast the finalization message for that round even if it finalizes a later round. In our implementation of Simplex, we can only commit a block if we have collected a finalization certificate for the block, even if a finalization certificate has been collected for a later block. Also, our implementation of Simplex commits blocks in-order. It follows from the above that a quorum of correct nodes cannot commit a block if a finalization certificate hasn't been collected for an ancestor block, because an ancestor block cannot be committed due to missing the finalization certificate. This commit makes the node re-broadcast finalizations for ancestor blocks of blocks for which a finalization certificate has been collected, but cannot be committed because it needs to collect finalization certificates for ancestor blocks. Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
1 parent 29427b8 commit b0960c6

File tree

2 files changed

+180
-9
lines changed

2 files changed

+180
-9
lines changed

epoch.go

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,11 @@ func (e *Epoch) persistFinalizationCertificate(fCert FinalizationCertificate) er
856856

857857
// we receive a finalization certificate for a future round
858858
e.Logger.Debug("Received a finalization certificate for a future sequence", zap.Uint64("seq", fCert.Finalization.Seq), zap.Uint64("nextSeqToCommit", nextSeqToCommit))
859+
860+
if err := e.rebroadcastPastFinalizations(); err != nil {
861+
return err
862+
}
863+
859864
e.replicationState.collectFutureFinalizationCertificates(&fCert, e.round, nextSeqToCommit)
860865
}
861866

@@ -875,6 +880,45 @@ func (e *Epoch) persistFinalizationCertificate(fCert FinalizationCertificate) er
875880
return nil
876881
}
877882

883+
func (e *Epoch) rebroadcastPastFinalizations() error {
884+
r := e.round
885+
886+
for {
887+
if r == 0 {
888+
return nil
889+
}
890+
r--
891+
round, exists := e.rounds[r]
892+
if !exists {
893+
return nil
894+
}
895+
896+
// Already collected a finalization certificate
897+
if round.fCert != nil {
898+
continue
899+
}
900+
901+
// Has notarized this round?
902+
if round.notarization == nil {
903+
continue
904+
}
905+
906+
var finalizationMessage *Message
907+
// Try to re-use finalization we created if possible, else create it.
908+
if finalization, exists := round.finalizations[string(e.ID)]; exists {
909+
finalizationMessage = &Message{Finalization: finalization}
910+
} else {
911+
_, msg, err := e.constructFinalizationMessage(round.notarization.Vote.BlockHeader)
912+
if err != nil {
913+
return err
914+
}
915+
finalizationMessage = msg
916+
}
917+
e.Logger.Debug("Rebroadcasting finalization", zap.Uint64("round", r))
918+
e.Comm.Broadcast(finalizationMessage)
919+
}
920+
}
921+
878922
func (e *Epoch) indexFinalizationCertificates(startRound uint64) {
879923
r := startRound
880924
round, exists := e.rounds[r]
@@ -1913,13 +1957,26 @@ func (e *Epoch) doNotarized(r uint64) error {
19131957

19141958
md := block.BlockHeader()
19151959

1960+
finalization, finalizationMsg, err := e.constructFinalizationMessage(md)
1961+
if err != nil {
1962+
return err
1963+
}
1964+
e.Comm.Broadcast(finalizationMsg)
1965+
1966+
err1 := e.startRound()
1967+
err2 := e.handleFinalizationMessage(&finalization, e.ID)
1968+
1969+
return errors.Join(err1, err2)
1970+
}
1971+
1972+
func (e *Epoch) constructFinalizationMessage(md BlockHeader) (Finalization, *Message, error) {
19161973
f := ToBeSignedFinalization{BlockHeader: md}
19171974
signature, err := f.Sign(e.Signer)
19181975
if err != nil {
1919-
return fmt.Errorf("failed signing vote %w", err)
1976+
return Finalization{}, nil, fmt.Errorf("failed signing vote %w", err)
19201977
}
19211978

1922-
sf := Finalization{
1979+
finalization := Finalization{
19231980
Signature: Signature{
19241981
Signer: e.ID,
19251982
Value: signature,
@@ -1930,14 +1987,9 @@ func (e *Epoch) doNotarized(r uint64) error {
19301987
}
19311988

19321989
finalizationMsg := &Message{
1933-
Finalization: &sf,
1990+
Finalization: &finalization,
19341991
}
1935-
e.Comm.Broadcast(finalizationMsg)
1936-
1937-
err1 := e.startRound()
1938-
err2 := e.handleFinalizationMessage(&sf, e.ID)
1939-
1940-
return errors.Join(err1, err2)
1992+
return finalization, finalizationMsg, nil
19411993
}
19421994

19431995
// stores a notarization in the epoch's memory.

epoch_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,125 @@ func TestEpochConsecutiveProposalsDoNotGetVerified(t *testing.T) {
179179
}
180180
}
181181

182+
func TestEpochNotarizeTwiceThenFinalize(t *testing.T) {
183+
l := testutil.MakeLogger(t, 1)
184+
bb := &testBlockBuilder{out: make(chan *testBlock, 1)}
185+
storage := newInMemStorage()
186+
187+
wal := newTestWAL(t)
188+
189+
nodes := []NodeID{{1}, {2}, {3}, {4}}
190+
191+
recordedMessages := make(chan *Message, 100)
192+
comm := &recordingComm{Communication: noopComm(nodes), BroadcastMessages: recordedMessages}
193+
194+
conf := EpochConfig{
195+
MaxProposalWait: DefaultMaxProposalWaitTime,
196+
Logger: l,
197+
ID: nodes[0],
198+
Signer: &testSigner{},
199+
WAL: wal,
200+
Verifier: &testVerifier{},
201+
Storage: storage,
202+
Comm: comm,
203+
BlockBuilder: bb,
204+
SignatureAggregator: &testSignatureAggregator{},
205+
}
206+
207+
e, err := NewEpoch(conf)
208+
require.NoError(t, err)
209+
210+
require.NoError(t, e.Start())
211+
212+
// Round 0
213+
block0 := <-bb.out
214+
215+
injectTestVote(t, e, block0, nodes[1])
216+
injectTestVote(t, e, block0, nodes[2])
217+
wal.assertNotarization(0)
218+
219+
// Round 1
220+
md := e.Metadata()
221+
_, ok := bb.BuildBlock(context.Background(), md)
222+
require.True(t, ok)
223+
block1 := <-bb.out
224+
225+
vote, err := newTestVote(block1, nodes[1])
226+
require.NoError(t, err)
227+
err = e.HandleMessage(&Message{
228+
BlockMessage: &BlockMessage{
229+
Vote: *vote,
230+
Block: block1,
231+
},
232+
}, nodes[1])
233+
require.NoError(t, err)
234+
235+
injectTestVote(t, e, block1, nodes[2])
236+
237+
wal.assertNotarization(1)
238+
239+
// Round 2
240+
md = e.Metadata()
241+
_, ok = bb.BuildBlock(context.Background(), md)
242+
require.True(t, ok)
243+
block2 := <-bb.out
244+
245+
vote, err = newTestVote(block2, nodes[2])
246+
require.NoError(t, err)
247+
err = e.HandleMessage(&Message{
248+
BlockMessage: &BlockMessage{
249+
Vote: *vote,
250+
Block: block2,
251+
},
252+
}, nodes[2])
253+
require.NoError(t, err)
254+
255+
injectTestVote(t, e, block2, nodes[1])
256+
257+
wal.assertNotarization(2)
258+
259+
// drain the recorded messages
260+
for len(recordedMessages) > 0 {
261+
<-recordedMessages
262+
}
263+
264+
blocks := []*testBlock{block0, block1}
265+
266+
var wg sync.WaitGroup
267+
wg.Add(1)
268+
269+
finish := make(chan struct{})
270+
// Once the node sends a finalization message, send it finalization messages as a response
271+
go func() {
272+
defer wg.Done()
273+
for {
274+
select {
275+
case <-finish:
276+
return
277+
case msg := <-recordedMessages:
278+
if msg.Finalization != nil {
279+
index := msg.Finalization.Finalization.Round
280+
if index > 1 {
281+
continue
282+
}
283+
injectTestFinalization(t, e, blocks[int(index)], nodes[1])
284+
injectTestFinalization(t, e, blocks[int(index)], nodes[2])
285+
}
286+
}
287+
}
288+
}()
289+
290+
injectTestFinalization(t, e, block2, nodes[1])
291+
injectTestFinalization(t, e, block2, nodes[2])
292+
293+
storage.waitForBlockCommit(0)
294+
storage.waitForBlockCommit(1)
295+
storage.waitForBlockCommit(2)
296+
297+
close(finish)
298+
wg.Wait()
299+
}
300+
182301
func TestEpochFinalizeThenNotarize(t *testing.T) {
183302
l := testutil.MakeLogger(t, 1)
184303
bb := &testBlockBuilder{out: make(chan *testBlock, 1)}

0 commit comments

Comments
 (0)