Skip to content

Commit d462be6

Browse files
authored
Merge pull request #7915 from multiversx/contended-blocks-finality
contended blocks finality
2 parents cd57dd1 + 4dc95fb commit d462be6

15 files changed

Lines changed: 776 additions & 15 deletions

common/common.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,5 +648,11 @@ func IsContendedHeader(header data.HeaderHandler, parentHeader data.HeaderHandle
648648
return false
649649
}
650650

651-
return header.GetRound() > parentHeader.GetRound()+1
651+
return IsContendedRound(header.GetRound(), parentHeader.GetRound())
652+
}
653+
654+
// IsContendedRound returns true if rounds were skipped between a parent at parentRound and its
655+
// child at round
656+
func IsContendedRound(round uint64, parentRound uint64) bool {
657+
return round > parentRound+1
652658
}

common/common_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,3 +1388,11 @@ func TestIsContendedHeader(t *testing.T) {
13881388
require.True(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 3}, parent))
13891389
require.True(t, common.IsContendedHeader(&testscommon.HeaderHandlerStub{RoundField: 10}, parent))
13901390
}
1391+
1392+
func TestIsContendedRound(t *testing.T) {
1393+
t.Parallel()
1394+
1395+
require.False(t, common.IsContendedRound(2, 1))
1396+
require.True(t, common.IsContendedRound(3, 1))
1397+
require.True(t, common.IsContendedRound(10, 1))
1398+
}

process/block/baseProcess.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ type baseProcessor struct {
140140

141141
processDataTriesOnCommitEpoch bool
142142
lastRestartNonce uint64
143+
lastSignaledFinalNonce uint64
143144
pruningDelay uint32
144145
processedMiniBlocksTracker process.ProcessedMiniBlocksTracker
145146
receiptsRepository receiptsRepository

process/block/metablock.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,11 @@ func (mp *metaProcessor) updateState(metaBlock data.MetaHeaderHandler, metaBlock
16971697

16981698
// for header v3, trie prnning is triggered in async mode from headers executor
16991699

1700+
if mp.enableEpochsHandler.IsFlagEnabledInEpoch(common.SupernovaFlag, metaBlock.GetEpoch()) {
1701+
mp.signalNewlyFinalBlocks(metaBlock, metaBlockHash, rootHash, prevMetaBlock)
1702+
return
1703+
}
1704+
17001705
outportFinalizedHeaderHash := metaBlockHash
17011706
if !common.IsFlagEnabledAfterEpochsStartBlock(metaBlock, mp.enableEpochsHandler, common.AndromedaFlag) {
17021707
outportFinalizedHeaderHash = metaBlock.GetPrevHash()
@@ -1706,6 +1711,46 @@ func (mp *metaProcessor) updateState(metaBlock data.MetaHeaderHandler, metaBlock
17061711
mp.blockChain.SetFinalBlockInfo(metaBlock.GetNonce(), metaBlockHash, rootHash)
17071712
}
17081713

1714+
// signalNewlyFinalBlocks emits the external finality signals for the blocks finalized since the
1715+
// previous commit; with deferred finality the committed header itself may not be final yet
1716+
func (mp *metaProcessor) signalNewlyFinalBlocks(
1717+
metaBlock data.MetaHeaderHandler,
1718+
metaBlockHash []byte,
1719+
rootHash []byte,
1720+
prevMetaBlock data.MetaHeaderHandler,
1721+
) {
1722+
finalNonce := mp.forkDetector.GetHighestFinalBlockNonce()
1723+
if finalNonce <= mp.lastSignaledFinalNonce {
1724+
return
1725+
}
1726+
1727+
// finality is deferred by at most one block on meta (settle-on-child), so the previous block
1728+
// is the only one that can become final together with the committed one
1729+
isPrevNewlyFinal := finalNonce >= prevMetaBlock.GetNonce() &&
1730+
prevMetaBlock.GetNonce() > mp.lastSignaledFinalNonce
1731+
if isPrevNewlyFinal {
1732+
mp.setFinalizedHeaderHashInIndexer(metaBlock.GetPrevHash())
1733+
}
1734+
1735+
if finalNonce >= metaBlock.GetNonce() {
1736+
mp.setFinalizedHeaderHashInIndexer(metaBlockHash)
1737+
mp.blockChain.SetFinalBlockInfo(metaBlock.GetNonce(), metaBlockHash, rootHash)
1738+
mp.lastSignaledFinalNonce = metaBlock.GetNonce()
1739+
return
1740+
}
1741+
1742+
if finalNonce == prevMetaBlock.GetNonce() {
1743+
prevRootHash, _, err := mp.getRootHashAndValidatorRootHash(prevMetaBlock)
1744+
if err != nil {
1745+
log.Warn("signalNewlyFinalBlocks: cannot get root hash of the newly final block", "error", err.Error())
1746+
} else {
1747+
mp.blockChain.SetFinalBlockInfo(prevMetaBlock.GetNonce(), metaBlock.GetPrevHash(), prevRootHash)
1748+
}
1749+
}
1750+
1751+
mp.lastSignaledFinalNonce = finalNonce
1752+
}
1753+
17091754
func (mp *metaProcessor) pruneTrieHeaderV3(
17101755
metaBlock data.HeaderHandler,
17111756
) {

process/block/metablock_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5870,3 +5870,94 @@ func TestMetaProcessor_CheckShardHeadersValidityContendedGate(t *testing.T) {
58705870
assert.Nil(t, err)
58715871
})
58725872
}
5873+
5874+
func TestMetaProcessor_UpdateStateSignalsNewlyFinalBlocksUnderSupernova(t *testing.T) {
5875+
t.Parallel()
5876+
5877+
hash4, hash5, hash6, hash7, hash8 := []byte("hash4"), []byte("hash5"), []byte("hash6"), []byte("hash7"), []byte("hash8")
5878+
newMetaBlock := func(nonce uint64, round uint64, prevHash []byte) *block.MetaBlock {
5879+
return &block.MetaBlock{
5880+
Nonce: nonce,
5881+
Round: round,
5882+
PrevHash: prevHash,
5883+
RootHash: append([]byte("rootHash"), byte(nonce)),
5884+
ValidatorStatsRootHash: append([]byte("validatorRootHash"), byte(nonce)),
5885+
}
5886+
}
5887+
hdr4 := newMetaBlock(4, 4, []byte("hash3"))
5888+
hdr5 := newMetaBlock(5, 5, hash4)
5889+
contendedHdr6 := newMetaBlock(6, 8, hash5)
5890+
contendedHdr7 := newMetaBlock(7, 11, hash6)
5891+
cleanHdr8 := newMetaBlock(8, 12, hash7)
5892+
headersByHash := map[string]data.HeaderHandler{
5893+
string(hash4): hdr4,
5894+
string(hash5): hdr5,
5895+
string(hash6): contendedHdr6,
5896+
string(hash7): contendedHdr7,
5897+
}
5898+
5899+
coreComponents, dataComponents, bootstrapComponents, statusComponents := createMockComponentHolders()
5900+
coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{
5901+
IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool {
5902+
return flag == common.AndromedaFlag || flag == common.SupernovaFlag
5903+
},
5904+
}
5905+
5906+
dataPool := initDataPool()
5907+
dataPool.HeadersCalled = func() dataRetriever.HeadersPool {
5908+
return &mock.HeadersCacherStub{
5909+
GetHeaderByHashCalled: func(hash []byte) (data.HeaderHandler, error) {
5910+
header, ok := headersByHash[string(hash)]
5911+
if !ok {
5912+
return nil, errors.New("header not found")
5913+
}
5914+
return header, nil
5915+
},
5916+
}
5917+
}
5918+
dataComponents.DataPool = dataPool
5919+
5920+
setFinalBlockInfos := make([]uint64, 0)
5921+
dataComponents.BlockChain = &testscommon.ChainHandlerStub{
5922+
GetGenesisHeaderCalled: func() data.HeaderHandler {
5923+
return &block.Header{Nonce: 0}
5924+
},
5925+
SetFinalBlockInfoCalled: func(nonce uint64, headerHash []byte, rootHash []byte) {
5926+
setFinalBlockInfos = append(setFinalBlockInfos, nonce)
5927+
},
5928+
}
5929+
5930+
outportCapture := &finalizedBlocksCapture{OutportStub: &outport.OutportStub{}}
5931+
statusComponents.Outport = outportCapture
5932+
5933+
finalNonce := uint64(5)
5934+
arguments := createMockMetaArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents)
5935+
arguments.ForkDetector = &mock.ForkDetectorMock{
5936+
GetHighestFinalBlockNonceCalled: func() uint64 {
5937+
return finalNonce
5938+
},
5939+
}
5940+
mp, _ := processBlock.NewMetaProcessor(arguments)
5941+
5942+
// clean committed block is final and signaled at once
5943+
mp.UpdateState(hdr5, hash5)
5944+
require.Equal(t, [][]byte{hash4, hash5}, outportCapture.finalizedHashes)
5945+
require.Equal(t, []uint64{5}, setFinalBlockInfos)
5946+
5947+
// contended committed block is not signaled while unsettled
5948+
mp.UpdateState(contendedHdr6, hash6)
5949+
require.Equal(t, [][]byte{hash4, hash5}, outportCapture.finalizedHashes)
5950+
require.Equal(t, []uint64{5}, setFinalBlockInfos)
5951+
5952+
// the committed child settles its parent: the parent is signaled, the contended child is not
5953+
finalNonce = 6
5954+
mp.UpdateState(contendedHdr7, hash7)
5955+
require.Equal(t, [][]byte{hash4, hash5, hash6}, outportCapture.finalizedHashes)
5956+
require.Equal(t, []uint64{5, 6}, setFinalBlockInfos)
5957+
5958+
// clean child settles its parent and is instantly final: both signaled exactly once
5959+
finalNonce = 8
5960+
mp.UpdateState(cleanHdr8, hash8)
5961+
require.Equal(t, [][]byte{hash4, hash5, hash6, hash7, hash8}, outportCapture.finalizedHashes)
5962+
require.Equal(t, []uint64{5, 6, 8}, setFinalBlockInfos)
5963+
}

process/block/shardblock.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,12 +1302,81 @@ func (sp *shardProcessor) updateState(headers []data.HeaderHandler, currentHeade
13021302
return
13031303
}
13041304

1305+
if sp.enableEpochsHandler.IsFlagEnabledInEpoch(common.SupernovaFlag, currentHeader.GetEpoch()) {
1306+
sp.signalNewlyFinalBlocks(currentHeader, currentHeaderHash)
1307+
return
1308+
}
1309+
13051310
sp.setFinalizedHeaderHashInIndexer(currentHeaderHash)
13061311

13071312
scheduledHeaderRootHash, _ := sp.scheduledTxsExecutionHandler.GetScheduledRootHashForHeader(currentHeaderHash)
13081313
sp.setFinalBlockInfo(currentHeader, currentHeaderHash, scheduledHeaderRootHash)
13091314
}
13101315

1316+
// signalNewlyFinalBlocks emits the external finality signals for the blocks finalized since the
1317+
// previous commit; with deferred finality the committed header itself may not be final yet
1318+
func (sp *shardProcessor) signalNewlyFinalBlocks(currentHeader data.HeaderHandler, currentHeaderHash []byte) {
1319+
finalNonce := sp.forkDetector.GetHighestFinalBlockNonce()
1320+
finalHash := sp.forkDetector.GetHighestFinalBlockHash()
1321+
if len(finalHash) == 0 || finalNonce <= sp.lastSignaledFinalNonce {
1322+
return
1323+
}
1324+
1325+
newlyFinalHashes := sp.getNewlyFinalHashes(finalNonce, finalHash, currentHeader, currentHeaderHash)
1326+
for i := len(newlyFinalHashes) - 1; i >= 0; i-- {
1327+
sp.setFinalizedHeaderHashInIndexer(newlyFinalHashes[i])
1328+
}
1329+
sp.lastSignaledFinalNonce = finalNonce
1330+
1331+
if finalNonce != currentHeader.GetNonce() {
1332+
return
1333+
}
1334+
1335+
scheduledHeaderRootHash, _ := sp.scheduledTxsExecutionHandler.GetScheduledRootHashForHeader(currentHeaderHash)
1336+
sp.setFinalBlockInfo(currentHeader, currentHeaderHash, scheduledHeaderRootHash)
1337+
}
1338+
1339+
// getNewlyFinalHashes walks the prev-hash chain from the final block back to the last signaled
1340+
// nonce and returns the hashes in descending nonce order
1341+
func (sp *shardProcessor) getNewlyFinalHashes(
1342+
finalNonce uint64,
1343+
finalHash []byte,
1344+
currentHeader data.HeaderHandler,
1345+
currentHeaderHash []byte,
1346+
) [][]byte {
1347+
hashes := [][]byte{finalHash}
1348+
isFirstSignal := sp.lastSignaledFinalNonce == 0
1349+
if isFirstSignal || finalNonce == sp.lastSignaledFinalNonce+1 {
1350+
return hashes
1351+
}
1352+
1353+
header := currentHeader
1354+
if !bytes.Equal(finalHash, currentHeaderHash) {
1355+
var err error
1356+
header, err = process.GetShardHeader(finalHash, sp.dataPool.Headers(), sp.marshalizer, sp.store)
1357+
if err != nil {
1358+
log.Warn("signalNewlyFinalBlocks: cannot load final header, signaling only its hash",
1359+
"final nonce", finalNonce, "error", err.Error())
1360+
return hashes
1361+
}
1362+
}
1363+
1364+
for nonce := finalNonce - 1; nonce > sp.lastSignaledFinalNonce; nonce-- {
1365+
hash := header.GetPrevHash()
1366+
var err error
1367+
header, err = process.GetShardHeader(hash, sp.dataPool.Headers(), sp.marshalizer, sp.store)
1368+
if err != nil {
1369+
log.Warn("signalNewlyFinalBlocks: cannot load newly final header, skipping older signals",
1370+
"nonce", nonce, "error", err.Error())
1371+
break
1372+
}
1373+
1374+
hashes = append(hashes, hash)
1375+
}
1376+
1377+
return hashes
1378+
}
1379+
13111380
func (sp *shardProcessor) pruneTrieHeaderV3(
13121381
header data.HeaderHandler,
13131382
) {

process/block/shardblock_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8234,3 +8234,70 @@ func TestShardProcessor_CheckMetaHeadersValidityAndFinalityContendedGate(t *test
82348234
assert.Nil(t, err)
82358235
})
82368236
}
8237+
8238+
type finalizedBlocksCapture struct {
8239+
*outport.OutportStub
8240+
finalizedHashes [][]byte
8241+
}
8242+
8243+
func (capture *finalizedBlocksCapture) FinalizedBlock(finalizedBlock *outportcore.FinalizedBlock) {
8244+
capture.finalizedHashes = append(capture.finalizedHashes, finalizedBlock.HeaderHash)
8245+
}
8246+
8247+
func TestShardProcessor_UpdateStateSignalsNewlyFinalBlocksUnderSupernova(t *testing.T) {
8248+
t.Parallel()
8249+
8250+
coreComponents, dataComponents, bootstrapComponents, statusComponents := createComponentHolderMocks()
8251+
coreComponents.EnableEpochsHandlerField = &enableEpochsHandlerMock.EnableEpochsHandlerStub{
8252+
IsFlagEnabledInEpochCalled: func(flag core.EnableEpochFlag, epoch uint32) bool {
8253+
return flag == common.AndromedaFlag || flag == common.SupernovaFlag
8254+
},
8255+
}
8256+
poolsHolder := dataRetrieverMock.NewPoolsHolderMock()
8257+
dataComponents.DataPool = poolsHolder
8258+
outportCapture := &finalizedBlocksCapture{OutportStub: &outport.OutportStub{}}
8259+
statusComponents.Outport = outportCapture
8260+
8261+
hash5, hash6, hash7, hash8 := []byte("hash5"), []byte("hash6"), []byte("hash7"), []byte("hash8")
8262+
hdr5 := &block.Header{Nonce: 5, Round: 5}
8263+
contendedHdr6 := &block.Header{Nonce: 6, Round: 8, PrevHash: hash5}
8264+
cleanHdr7 := &block.Header{Nonce: 7, Round: 9, PrevHash: hash6}
8265+
cleanHdr8 := &block.Header{Nonce: 8, Round: 10, PrevHash: hash7}
8266+
poolsHolder.Headers().AddHeader(hash6, contendedHdr6)
8267+
poolsHolder.Headers().AddHeader(hash7, cleanHdr7)
8268+
8269+
finalNonce, finalHash := uint64(5), hash5
8270+
8271+
arguments := CreateMockArguments(coreComponents, dataComponents, bootstrapComponents, statusComponents)
8272+
arguments.ForkDetector = &mock.ForkDetectorMock{
8273+
GetHighestFinalBlockNonceCalled: func() uint64 {
8274+
return finalNonce
8275+
},
8276+
GetHighestFinalBlockHashCalled: func() []byte {
8277+
return finalHash
8278+
},
8279+
}
8280+
sp, _ := blproc.NewShardProcessor(arguments)
8281+
8282+
// clean committed block is final and signaled at once
8283+
sp.UpdateStateStorage(nil, hdr5, hash5)
8284+
require.Equal(t, [][]byte{hash5}, outportCapture.finalizedHashes)
8285+
finalInfoNonce, finalInfoHash, _ := dataComponents.BlockChain.GetFinalBlockInfo()
8286+
require.Equal(t, uint64(5), finalInfoNonce)
8287+
require.Equal(t, hash5, finalInfoHash)
8288+
8289+
// contended committed block and its descendant are not signaled while unsettled
8290+
sp.UpdateStateStorage(nil, contendedHdr6, hash6)
8291+
sp.UpdateStateStorage(nil, cleanHdr7, hash7)
8292+
require.Equal(t, [][]byte{hash5}, outportCapture.finalizedHashes)
8293+
finalInfoNonce, _, _ = dataComponents.BlockChain.GetFinalBlockInfo()
8294+
require.Equal(t, uint64(5), finalInfoNonce)
8295+
8296+
// settlement advances the final checkpoint: each newly final block is signaled exactly once
8297+
finalNonce, finalHash = 8, hash8
8298+
sp.UpdateStateStorage(nil, cleanHdr8, hash8)
8299+
require.Equal(t, [][]byte{hash5, hash6, hash7, hash8}, outportCapture.finalizedHashes)
8300+
finalInfoNonce, finalInfoHash, _ = dataComponents.BlockChain.GetFinalBlockInfo()
8301+
require.Equal(t, uint64(8), finalInfoNonce)
8302+
require.Equal(t, hash8, finalInfoHash)
8303+
}

process/sync/baseForkDetector.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type headerInfo struct {
1818
nonce uint64
1919
round uint64
2020
hash []byte
21+
prevHash []byte
2122
state process.BlockHeaderState
2223
hasProof bool
2324
}
@@ -412,6 +413,43 @@ func (bfd *baseForkDetector) setFinalCheckpoint(finalCheckpoint *checkpointInfo)
412413
bfd.mutFork.Unlock()
413414
}
414415

416+
// advanceFinalCheckpoint sets the final checkpoint only forward, so concurrent computations
417+
// cannot regress an already finalized nonce
418+
func (bfd *baseForkDetector) advanceFinalCheckpoint(finalCheckpoint *checkpointInfo) {
419+
bfd.mutFork.Lock()
420+
if finalCheckpoint.nonce > bfd.fork.finalCheckpoint.nonce {
421+
bfd.fork.finalCheckpoint = finalCheckpoint
422+
}
423+
bfd.mutFork.Unlock()
424+
}
425+
426+
func (bfd *baseForkDetector) isSupernovaForHeader(header data.HeaderHandler) bool {
427+
return bfd.enableEpochsHandler.IsFlagEnabledInEpoch(common.SupernovaFlag, header.GetEpoch())
428+
}
429+
430+
func isParentCheckpoint(checkpoint *checkpointInfo, header data.HeaderHandler) bool {
431+
if checkpoint.nonce+1 != header.GetNonce() {
432+
return false
433+
}
434+
435+
return len(checkpoint.hash) == 0 || bytes.Equal(checkpoint.hash, header.GetPrevHash())
436+
}
437+
438+
// canInstantlyFinalize returns false for a contended header or one whose parent is not final yet;
439+
// such headers finalize later, on settlement (R-INTRA)
440+
func (bfd *baseForkDetector) canInstantlyFinalize(header data.HeaderHandler) bool {
441+
if !bfd.isSupernovaForHeader(header) {
442+
return true
443+
}
444+
445+
finalCheckpoint := bfd.finalCheckpoint()
446+
if !isParentCheckpoint(finalCheckpoint, header) {
447+
return false
448+
}
449+
450+
return !common.IsContendedRound(header.GetRound(), finalCheckpoint.round)
451+
}
452+
415453
// RestoreToGenesis sets class variables to theirs initial values
416454
func (bfd *baseForkDetector) RestoreToGenesis() {
417455
bfd.mutHeaders.Lock()
@@ -935,6 +973,7 @@ func (bfd *baseForkDetector) processReceivedBlock(
935973
nonce: header.GetNonce(),
936974
round: header.GetRound(),
937975
hash: headerHash,
976+
prevHash: header.GetPrevHash(),
938977
state: state,
939978
hasProof: hasProof,
940979
}

0 commit comments

Comments
 (0)