From a206d941c3ce1070f3d02819b62bbee8f94db88d Mon Sep 17 00:00:00 2001 From: Adrian Dobrita Date: Wed, 22 Jul 2026 16:06:09 +0300 Subject: [PATCH] meta finality view and settlement checker integration --- factory/consensus/consensusComponents.go | 11 ++ .../sync/basicSync/supernovaReconcile_test.go | 81 ++++++++++-- integrationTests/testSyncNode.go | 10 ++ process/errors.go | 3 + process/sync/argBootstrapper.go | 1 + process/sync/baseSync.go | 116 +++++++++------- process/sync/baseSync_test.go | 104 ++++++++++++++- process/sync/interface.go | 6 + process/sync/metablock.go | 4 + process/sync/settlementChecker.go | 51 +++++++ process/sync/settlementChecker_test.go | 124 ++++++++++++++++++ process/sync/shardblock.go | 8 ++ process/sync/shardblock_test.go | 1 + testscommon/metaFinalityViewStub.go | 42 ++++++ 14 files changed, 504 insertions(+), 58 deletions(-) create mode 100644 process/sync/settlementChecker.go create mode 100644 process/sync/settlementChecker_test.go create mode 100644 testscommon/metaFinalityViewStub.go diff --git a/factory/consensus/consensusComponents.go b/factory/consensus/consensusComponents.go index df66a253b52..b8d98b1994e 100644 --- a/factory/consensus/consensusComponents.go +++ b/factory/consensus/consensusComponents.go @@ -28,6 +28,7 @@ import ( "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-go/process/sync" "github.com/multiversx/mx-chain-go/process/sync/storageBootstrap" + "github.com/multiversx/mx-chain-go/process/track" "github.com/multiversx/mx-chain-go/sharding" nodesCoord "github.com/multiversx/mx-chain-go/sharding/nodesCoordinator" "github.com/multiversx/mx-chain-go/state/syncer" @@ -535,8 +536,18 @@ func (ccf *consensusComponentsFactory) createShardBootstrapper() (process.Bootst ProcessConfigsHandler: ccf.coreComponents.ProcessConfigsHandler(), } + dataPool := ccf.dataComponents.Datapool() + metaFinalityView, err := track.NewMetaFinalityView(track.ArgsMetaFinalityView{ + HeadersPool: dataPool.Headers(), + ProofsPool: dataPool.Proofs(), + }) + if err != nil { + return nil, err + } + argsShardBootstrapper := sync.ArgShardBootstrapper{ ArgBaseBootstrapper: argsBaseBootstrapper, + MetaFinalityView: metaFinalityView, } return sync.NewShardBootstrap(argsShardBootstrapper) diff --git a/integrationTests/sync/basicSync/supernovaReconcile_test.go b/integrationTests/sync/basicSync/supernovaReconcile_test.go index a4278496fab..aedf18f5387 100644 --- a/integrationTests/sync/basicSync/supernovaReconcile_test.go +++ b/integrationTests/sync/basicSync/supernovaReconcile_test.go @@ -10,8 +10,30 @@ import ( "github.com/stretchr/testify/require" "github.com/multiversx/mx-chain-go/integrationTests" + "github.com/multiversx/mx-chain-go/process" ) +// mirrors process/block.metaArbitrationWindowRounds, the R-RESOLVE discovery window +const metaArbitrationWindowRounds = 3 + +func requireNotarizes(t *testing.T, metaHeader data.HeaderHandler, shardID uint32, hashes ...[]byte) { + metaHandler, ok := metaHeader.(data.MetaHeaderHandler) + require.True(t, ok) + + for _, shardInfo := range process.GetShardHeadersReferencedByMeta(metaHandler) { + if shardInfo.GetShardID() != shardID { + continue + } + for _, hash := range hashes { + if string(shardInfo.GetHeaderHash()) == string(hash) { + return + } + } + } + + require.Fail(t, "meta block does not notarize the winning branch") +} + // backstop scenario: island 1 instantly finalizes clean sibling A; island 2, // blind to A, commits contended sibling B AND extends it with a proofed child C. // When B and C reach island 1, its nodes hold a FINALIZED block that objectively @@ -55,12 +77,14 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T RoundsConfig: &roundsConfig, }) - island1 := []*integrationTests.TestProcessorNode{pA, obsA, metaNode} - island2 := []*integrationTests.TestProcessorNode{pB, obsB} + // meta sits on the winning side: it never learns A, so it arbitrates B and its notarization is + // the authority verdict delivered to the stranded island + island1 := []*integrationTests.TestProcessorNode{pA, obsA} + island2 := []*integrationTests.TestProcessorNode{pB, obsB, metaNode} allNodes := []*integrationTests.TestProcessorNode{pA, obsA, metaNode, pB, obsB} - integrationTests.ConnectNodes([]integrationTests.Connectable{pA, obsA, metaNode}) - integrationTests.ConnectNodes([]integrationTests.Connectable{pB, obsB}) + integrationTests.ConnectNodes([]integrationTests.Connectable{pA, obsA}) + integrationTests.ConnectNodes([]integrationTests.Connectable{pB, obsB, metaNode}) defer func() { for _, n := range allNodes { @@ -100,14 +124,22 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T integrationTests.UpdateRound(allNodes, round) nonce++ - // common prefix, rounds 1..4 + // common prefix, rounds 1..4: the shard proposes on island 1, meta notarizes it on island 2, + // and each side is fed the other's block numPrefixBlocks := 4 for i := 0; i < numPrefixBlocks; i++ { - integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA, metaNode}, round, nonce) + integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA}, round, nonce) time.Sleep(integrationTests.SyncDelay) for _, target := range island2 { mirrorCurrentBlock(target, pA) + } + time.Sleep(integrationTests.SyncDelay) + + integrationTests.ProposeBlockWithProof(island2, []*integrationTests.TestProcessorNode{metaNode}, round, nonce) + time.Sleep(integrationTests.SyncDelay) + + for _, target := range island1 { mirrorCurrentBlock(target, metaNode) } time.Sleep(integrationTests.SyncDelay) @@ -117,6 +149,9 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T nonce++ } + // meta built its own chain over the prefix and stops here; the shard nonce moves on without it + nextMetaNonce := nonce + // round 5: island 1 commits and instantly finalizes the clean sibling A integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA}, round, nonce) time.Sleep(integrationTests.SyncDelay) @@ -159,8 +194,33 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T round = integrationTests.IncrementAndPrintRound(round) integrationTests.UpdateRound(allNodes, round) - // deliver the winning branch to island 1: its final head A is now a proven loser + // B is contended, so meta may only arbitrate it once the discovery window has elapsed + for i := 0; i < metaArbitrationWindowRounds; i++ { + round = integrationTests.IncrementAndPrintRound(round) + integrationTests.UpdateRound(allNodes, round) + time.Sleep(integrationTests.SyncDelay) + } + + // meta arbitrates B, then extends itself so the notarizing block is settled and thus held final + integrationTests.ProposeBlockWithProof(island2, []*integrationTests.TestProcessorNode{metaNode}, round, nextMetaNonce) + time.Sleep(integrationTests.SyncDelay) + notarizingMeta, notarizingMetaHash, notarizingMetaProof := grabCurrentBlock(metaNode) + requireNotarizes(t, notarizingMeta, shardId, hashB, hashC) + + round = integrationTests.IncrementAndPrintRound(round) + integrationTests.UpdateRound(allNodes, round) + integrationTests.ProposeBlockWithProof(island2, []*integrationTests.TestProcessorNode{metaNode}, round, nextMetaNonce+1) + time.Sleep(integrationTests.SyncDelay) + settlingMeta, settlingMetaHash, settlingMetaProof := grabCurrentBlock(metaNode) + require.Equal(t, string(notarizingMetaHash), string(settlingMeta.GetPrevHash())) + + round = integrationTests.IncrementAndPrintRound(round) + integrationTests.UpdateRound(allNodes, round) + + // deliver the winning branch AND the authority verdict on it to island 1 for _, n := range []*integrationTests.TestProcessorNode{pA, obsA} { + injectBlock(n, notarizingMeta, notarizingMetaHash, notarizingMetaProof) + injectBlock(n, settlingMeta, settlingMetaHash, settlingMetaProof) injectBlock(n, headerB, hashB, proofB) injectBlock(n, headerC, hashC, proofC) } @@ -183,6 +243,8 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T // the forced rollback clears pools above the rollback nonce; in production the // majority network answers the re-requests -- modeled by re-injecting each round for _, n := range []*integrationTests.TestProcessorNode{pA, obsA} { + injectBlock(n, notarizingMeta, notarizingMetaHash, notarizingMetaProof) + injectBlock(n, settlingMeta, settlingMetaHash, settlingMetaProof) injectBlock(n, headerB, hashB, proofB) injectBlock(n, headerC, hashC, proofC) } @@ -199,8 +261,9 @@ func TestSupernovaSync_ReconcileBackstop_FinalizedMinorityConverges(t *testing.T assert.Equal(t, uint64(4), n.ForkDetector.GetHighestFinalBlockNonce()) } - // island 2 was never on the losing block and is untouched - for _, n := range island2 { + // island 2 was never on the losing block and is untouched (the meta node runs its own chain, + // so only the shard nodes are compared here) + for _, n := range []*integrationTests.TestProcessorNode{pB, obsB} { assert.Equal(t, string(hashC), string(n.BlockChain.GetCurrentBlockHeaderHash())) assert.Equal(t, uint64(4), n.ForkDetector.GetHighestFinalBlockNonce()) } diff --git a/integrationTests/testSyncNode.go b/integrationTests/testSyncNode.go index fad1100c61f..92ff10b84fc 100644 --- a/integrationTests/testSyncNode.go +++ b/integrationTests/testSyncNode.go @@ -29,6 +29,7 @@ import ( "github.com/multiversx/mx-chain-go/process/block" "github.com/multiversx/mx-chain-go/process/block/bootstrapStorage" "github.com/multiversx/mx-chain-go/process/sync" + "github.com/multiversx/mx-chain-go/process/track" "github.com/multiversx/mx-chain-go/state" stateDisabled "github.com/multiversx/mx-chain-go/state/disabled" "github.com/multiversx/mx-chain-go/testscommon" @@ -366,8 +367,17 @@ func (tpn *TestProcessorNode) createShardBootstrapper() (TestBootstrapper, error ProcessConfigsHandler: tpn.ProcessConfigsHandler, } + metaFinalityView, err := track.NewMetaFinalityView(track.ArgsMetaFinalityView{ + HeadersPool: tpn.DataPool.Headers(), + ProofsPool: tpn.DataPool.Proofs(), + }) + if err != nil { + return nil, err + } + argsShardBootstrapper := sync.ArgShardBootstrapper{ ArgBaseBootstrapper: argsBaseBootstrapper, + MetaFinalityView: metaFinalityView, } bootstrap, err := sync.NewShardBootstrap(argsShardBootstrapper) diff --git a/process/errors.go b/process/errors.go index 2aef2176c5c..aee467bbae6 100644 --- a/process/errors.go +++ b/process/errors.go @@ -254,6 +254,9 @@ var ErrNilMetaBlocksPool = errors.New("nil meta blocks pool") // ErrNilProofsPool signals that a nil proofs pool was used var ErrNilProofsPool = errors.New("nil proofs pool") +// ErrNilMetaFinalityView signals that a nil meta finality view was used +var ErrNilMetaFinalityView = errors.New("nil meta finality view") + // ErrNilTxProcessor signals that a nil transactions processor was used var ErrNilTxProcessor = errors.New("nil transactions processor") diff --git a/process/sync/argBootstrapper.go b/process/sync/argBootstrapper.go index 25f475b9a22..0a2a1642b6f 100644 --- a/process/sync/argBootstrapper.go +++ b/process/sync/argBootstrapper.go @@ -60,6 +60,7 @@ type ArgBaseBootstrapper struct { // new instances of shard bootstrapper type ArgShardBootstrapper struct { ArgBaseBootstrapper + MetaFinalityView process.MetaFinalityView } // ArgMetaBootstrapper holds all dependencies required by the bootstrap data factory in order to create diff --git a/process/sync/baseSync.go b/process/sync/baseSync.go index e3d2a7d0323..0d70d94caa6 100644 --- a/process/sync/baseSync.go +++ b/process/sync/baseSync.go @@ -81,9 +81,10 @@ type nonceRecoveryInfo struct { } type reconcileEvidence struct { - nonce uint64 - localHash []byte - competitorHash []byte + nonce uint64 + localHash []byte + competitorHash []byte + lastEvaluatedRound int64 } type baseBootstrap struct { @@ -106,6 +107,7 @@ type baseBootstrap struct { shardCoordinator sharding.Coordinator accounts state.AccountsAdapter blockBootstrapper blockBootstrapper + settlementChecker settlementChecker blackListHandler process.TimeCacher enableEpochsHandler common.EnableEpochsHandler enableRoundsHandler common.EnableRoundsHandler @@ -1049,6 +1051,14 @@ func (boot *baseBootstrap) prepareForSyncAtBoostrapIfNeeded() error { func (boot *baseBootstrap) syncBlock() error { boot.computeNodeState() + + // evaluated before the synchronized gate: the authority may settle a childless competitor, and + // that leaves the node reading as synchronized while its final head is already dead + if boot.tryReconcileEquivocation() { + boot.invalidateNodeState() + return nil + } + nodeState := boot.GetNodeState() if nodeState != common.NsNotSynchronized { @@ -1061,15 +1071,7 @@ func (boot *baseBootstrap) syncBlock() error { return nil } - defer func() { - boot.mutNodeState.Lock() - boot.isNodeStateCalculated = false - boot.mutNodeState.Unlock() - }() - - if boot.tryReconcileEquivocation() { - return nil - } + defer boot.invalidateNodeState() if boot.forkInfo.IsDetected { boot.statusHandler.Increment(common.MetricNumTimesInForkChoice) @@ -2346,7 +2348,12 @@ func (boot *baseBootstrap) onEquivocationEvidence(headerProof data.HeaderProofHa } boot.mutReconcile.Lock() - boot.pendingReconcile = &reconcileEvidence{nonce: nonce, localHash: localHash, competitorHash: competitorHash} + boot.pendingReconcile = &reconcileEvidence{ + nonce: nonce, + localHash: localHash, + competitorHash: competitorHash, + lastEvaluatedRound: -1, + } boot.mutReconcile.Unlock() log.Warn("equivocation proof observed at the final head nonce, reconcile evidence recorded", @@ -2372,38 +2379,33 @@ func pickCompetitorHash(localHash []byte, headerProof data.HeaderProofHandler, c return nil } -// tryReconcileEquivocation overrides the final gate and forces the switch when the final head is -// childless and the equivocation competitor has a proofed child; never past a settled descendant +// tryReconcileEquivocation overrides the final gate and forces the switch when the settlement +// authority settled the equivocation competitor and not the local block func (boot *baseBootstrap) tryReconcileEquivocation() bool { - boot.mutReconcile.Lock() - evidence := boot.pendingReconcile - boot.mutReconcile.Unlock() - + evidence, shouldEvaluate := boot.reconcileEvidenceToEvaluate() if evidence == nil { return false } - currentHeader := boot.chainHandler.GetCurrentBlockHeader() - currentHash := boot.chainHandler.GetCurrentBlockHeaderHash() - stillApplies := !check.IfNil(currentHeader) && - currentHeader.GetNonce() == evidence.nonce && - bytes.Equal(currentHash, evidence.localHash) && - evidence.nonce == boot.forkDetector.GetHighestFinalBlockNonce() - if !stillApplies { + if !boot.reconcileEvidenceStillApplies(evidence) { boot.clearReconcileEvidence(evidence) return false } - if boot.hasProofedChild(evidence.nonce+1, evidence.localHash) { + if !shouldEvaluate { + return false + } + + if boot.settlementChecker.isSettled(evidence.nonce, evidence.localHash) { boot.clearReconcileEvidence(evidence) return false } selfID := boot.shardCoordinator.SelfId() isCompetitorSettled := boot.proofs.HasProof(selfID, evidence.competitorHash) && - boot.hasProofedChild(evidence.nonce+1, evidence.competitorHash) + boot.settlementChecker.isSettled(evidence.nonce, evidence.competitorHash) if !isCompetitorSettled { - // the settling child may still arrive; keep the evidence armed for the next iteration + // the authority's verdict may still arrive; keep the evidence armed for the next round return false } @@ -2422,31 +2424,49 @@ func (boot *baseBootstrap) tryReconcileEquivocation() bool { return true } -func (boot *baseBootstrap) clearReconcileEvidence(evidence *reconcileEvidence) { +// the settlement checks walk the pools, so the authority is consulted at most once per round +func (boot *baseBootstrap) reconcileEvidenceToEvaluate() (*reconcileEvidence, bool) { boot.mutReconcile.Lock() - if boot.pendingReconcile == evidence { - boot.pendingReconcile = nil - } - boot.mutReconcile.Unlock() -} + defer boot.mutReconcile.Unlock() -func (boot *baseBootstrap) hasProofedChild(nonce uint64, parentHash []byte) bool { - selfID := boot.shardCoordinator.SelfId() - headers, hashes, err := boot.headers.GetHeadersByNonceAndShardId(nonce, selfID) - if err != nil { - return false + evidence := boot.pendingReconcile + if evidence == nil { + return nil, false } - for i, header := range headers { - if check.IfNil(header) || !bytes.Equal(header.GetPrevHash(), parentHash) { - continue - } - if boot.proofs.HasProof(selfID, hashes[i]) { - return true - } + currentRound := boot.roundHandler.Index() + shouldEvaluate := evidence.lastEvaluatedRound != currentRound + if shouldEvaluate { + evidence.lastEvaluatedRound = currentRound } - return false + return evidence, shouldEvaluate +} + +// invalidateNodeState forces the next iteration to recompute the fork info, so a roll back armed +// here is picked up without waiting for the round to change +func (boot *baseBootstrap) invalidateNodeState() { + boot.mutNodeState.Lock() + boot.isNodeStateCalculated = false + boot.mutNodeState.Unlock() +} + +func (boot *baseBootstrap) reconcileEvidenceStillApplies(evidence *reconcileEvidence) bool { + currentHeader := boot.chainHandler.GetCurrentBlockHeader() + currentHash := boot.chainHandler.GetCurrentBlockHeaderHash() + + return !check.IfNil(currentHeader) && + currentHeader.GetNonce() == evidence.nonce && + bytes.Equal(currentHash, evidence.localHash) && + evidence.nonce == boot.forkDetector.GetHighestFinalBlockNonce() +} + +func (boot *baseBootstrap) clearReconcileEvidence(evidence *reconcileEvidence) { + boot.mutReconcile.Lock() + if boot.pendingReconcile == evidence { + boot.pendingReconcile = nil + } + boot.mutReconcile.Unlock() } func (boot *baseBootstrap) isForcedRollBackOneBlock() bool { diff --git a/process/sync/baseSync_test.go b/process/sync/baseSync_test.go index 10804229fcc..acc5a8d1fd2 100644 --- a/process/sync/baseSync_test.go +++ b/process/sync/baseSync_test.go @@ -986,11 +986,13 @@ func TestBaseBootstrap_ReconcileEquivocation(t *testing.T) { blacklisted []string } - buildBootstrapper := func(childrenOf []byte, calls *reconcileCalls) *baseBootstrap { + buildBootstrapperWithChecker := func(childrenOf []byte, calls *reconcileCalls, checker settlementChecker, roundHandler *mock.RoundHandlerMock) *baseBootstrap { childHash := []byte("childHash") child := &block.HeaderV3{Nonce: finalNonce + 1, Round: 13, PrevHash: childrenOf} return &baseBootstrap{ + settlementChecker: checker, + roundHandler: roundHandler, chainHandler: &testscommon.ChainHandlerStub{ GetCurrentBlockHeaderCalled: func() data.HeaderHandler { return localHead @@ -1034,6 +1036,29 @@ func TestBaseBootstrap_ReconcileEquivocation(t *testing.T) { } } + settlesOnly := func(hashes ...[]byte) *settlementCheckerStub { + return &settlementCheckerStub{ + isSettledCalled: func(_ uint64, headerHash []byte) bool { + for _, hash := range hashes { + if bytes.Equal(hash, headerHash) { + return true + } + } + return false + }, + } + } + + buildBootstrapper := func(childrenOf []byte, calls *reconcileCalls) *baseBootstrap { + checker := &settlementCheckerStub{ + isSettledCalled: func(nonce uint64, headerHash []byte) bool { + return len(childrenOf) > 0 && bytes.Equal(childrenOf, headerHash) + }, + } + + return buildBootstrapperWithChecker(childrenOf, calls, checker, &mock.RoundHandlerMock{}) + } + t.Run("fires when the final head is childless and the competitor has a proofed child", func(t *testing.T) { t.Parallel() @@ -1086,6 +1111,83 @@ func TestBaseBootstrap_ReconcileEquivocation(t *testing.T) { require.Nil(t, boot.pendingReconcile) require.False(t, boot.tryReconcileEquivocation()) }) + + // spot 2: under per-round R0 a stranded loser can hold a proofed child of its own, so that child + // must no longer protect it from the authority's verdict + t.Run("switches away from a local block that has its own proofed child", func(t *testing.T) { + t.Parallel() + + calls := &reconcileCalls{} + boot := buildBootstrapperWithChecker(localHash, calls, settlesOnly(competitorHash), &mock.RoundHandlerMock{}) + + boot.onEquivocationEvidence(competitorProof, nil) + require.True(t, boot.tryReconcileEquivocation()) + require.Equal(t, finalNonce, calls.reconciledNonce) + require.Equal(t, []string{string(localHash)}, calls.blacklisted) + }) + + // the authority's verdict on the local hash beats any competitor evidence + t.Run("never switches when the authority settled the local block", func(t *testing.T) { + t.Parallel() + + calls := &reconcileCalls{} + boot := buildBootstrapperWithChecker(competitorHash, calls, settlesOnly(localHash, competitorHash), &mock.RoundHandlerMock{}) + + boot.onEquivocationEvidence(competitorProof, nil) + require.False(t, boot.tryReconcileEquivocation()) + require.Equal(t, uint64(0), calls.reconciledNonce) + require.Empty(t, calls.blacklisted) + require.Nil(t, boot.pendingReconcile) + }) + + // the R-RESOLVE outcome: meta arbitrates the lowest-round sibling, which has no child at all + t.Run("switches onto a childless competitor the authority notarized", func(t *testing.T) { + t.Parallel() + + calls := &reconcileCalls{} + boot := buildBootstrapperWithChecker(nil, calls, settlesOnly(competitorHash), &mock.RoundHandlerMock{}) + + boot.onEquivocationEvidence(competitorProof, nil) + require.True(t, boot.tryReconcileEquivocation()) + require.Equal(t, finalNonce, calls.reconciledNonce) + }) + + t.Run("evaluates the authority at most once per round", func(t *testing.T) { + t.Parallel() + + calls := &reconcileCalls{} + checker := settlesOnly() + roundHandler := &mock.RoundHandlerMock{RoundIndex: 7} + boot := buildBootstrapperWithChecker(nil, calls, checker, roundHandler) + + boot.onEquivocationEvidence(competitorProof, nil) + + require.False(t, boot.tryReconcileEquivocation()) + callsInFirstRound := checker.numCalls + require.NotZero(t, callsInFirstRound) + + require.False(t, boot.tryReconcileEquivocation()) + require.False(t, boot.tryReconcileEquivocation()) + require.Equal(t, callsInFirstRound, checker.numCalls) + + roundHandler.RoundIndex = 8 + require.False(t, boot.tryReconcileEquivocation()) + require.Greater(t, checker.numCalls, callsInFirstRound) + }) +} + +type settlementCheckerStub struct { + isSettledCalled func(nonce uint64, headerHash []byte) bool + numCalls int +} + +func (stub *settlementCheckerStub) isSettled(nonce uint64, headerHash []byte) bool { + stub.numCalls++ + if stub.isSettledCalled != nil { + return stub.isSettledCalled(nonce, headerHash) + } + + return false } func TestBaseBootstrap_SelectNonBlackListedHash(t *testing.T) { diff --git a/process/sync/interface.go b/process/sync/interface.go index 268ce7e4207..0f843cc0981 100644 --- a/process/sync/interface.go +++ b/process/sync/interface.go @@ -26,6 +26,12 @@ type syncStarter interface { SyncBlock(ctx context.Context) error } +// settlementChecker answers whether a block at the reconcile nonce is settled, per the settlement +// authority of the chain the node belongs to +type settlementChecker interface { + isSettled(nonce uint64, headerHash []byte) bool +} + // forkDetector is the interface needed by base fork detector to deal with shards and meta nodes type forkDetector interface { computeFinalCheckpoint() diff --git a/process/sync/metablock.go b/process/sync/metablock.go index 1623b5e36c3..9196311e360 100644 --- a/process/sync/metablock.go +++ b/process/sync/metablock.go @@ -117,6 +117,10 @@ func NewMetaBootstrap(arguments ArgMetaBootstrapper) (*MetaBootstrap, error) { base.blockBootstrapper = &boot base.syncStarter = &boot + base.settlementChecker = &metaSettlementChecker{ + headers: arguments.PoolsHolder.Headers(), + proofs: arguments.PoolsHolder.Proofs(), + } base.requestMiniBlocks = boot.requestMiniBlocksFromHeaderWithNonceIfMissing // placed in struct fields for performance reasons diff --git a/process/sync/settlementChecker.go b/process/sync/settlementChecker.go new file mode 100644 index 00000000000..1d31da5df29 --- /dev/null +++ b/process/sync/settlementChecker.go @@ -0,0 +1,51 @@ +package sync + +import ( + "bytes" + + "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-core-go/core/check" + + "github.com/multiversx/mx-chain-go/dataRetriever" + "github.com/multiversx/mx-chain-go/process" +) + +// shardSettlementChecker settles a shard block on the verdict of the settlement authority: a meta +// block the node holds final notarized it, or one of its descendants +type shardSettlementChecker struct { + metaFinalityView process.MetaFinalityView + selfShardID uint32 +} + +func (checker *shardSettlementChecker) isSettled(nonce uint64, headerHash []byte) bool { + return checker.metaFinalityView.IsIncludedInHeldFinalMetaBlock(checker.selfShardID, headerHash, nonce) +} + +// metaSettlementChecker settles a meta block on the depth-1 settle-on-child rule; meta has no +// external authority to defer to +type metaSettlementChecker struct { + headers dataRetriever.HeadersPool + proofs dataRetriever.ProofsPool +} + +func (checker *metaSettlementChecker) isSettled(nonce uint64, headerHash []byte) bool { + return checker.hasProofedChild(nonce+1, headerHash) +} + +func (checker *metaSettlementChecker) hasProofedChild(nonce uint64, parentHash []byte) bool { + headers, hashes, err := checker.headers.GetHeadersByNonceAndShardId(nonce, core.MetachainShardId) + if err != nil { + return false + } + + for i, header := range headers { + if check.IfNil(header) || !bytes.Equal(header.GetPrevHash(), parentHash) { + continue + } + if checker.proofs.HasProof(core.MetachainShardId, hashes[i]) { + return true + } + } + + return false +} diff --git a/process/sync/settlementChecker_test.go b/process/sync/settlementChecker_test.go new file mode 100644 index 00000000000..da971dadd15 --- /dev/null +++ b/process/sync/settlementChecker_test.go @@ -0,0 +1,124 @@ +package sync + +import ( + "errors" + "testing" + + "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-core-go/data" + "github.com/multiversx/mx-chain-core-go/data/block" + "github.com/stretchr/testify/require" + + "github.com/multiversx/mx-chain-go/testscommon" + testscommonDataRetriever "github.com/multiversx/mx-chain-go/testscommon/dataRetriever" + "github.com/multiversx/mx-chain-go/testscommon/pool" +) + +func TestShardSettlementChecker_IsSettled(t *testing.T) { + t.Parallel() + + nonce := uint64(10) + headerHash := []byte("headerHash") + + t.Run("defers to the meta finality view with the self shard id", func(t *testing.T) { + t.Parallel() + + var gotShardID uint32 + var gotHash []byte + var gotNonce uint64 + checker := &shardSettlementChecker{ + selfShardID: 3, + metaFinalityView: &testscommon.MetaFinalityViewStub{ + IsIncludedInHeldFinalMetaBlockCalled: func(shardID uint32, hash []byte, hdrNonce uint64) bool { + gotShardID, gotHash, gotNonce = shardID, hash, hdrNonce + return true + }, + }, + } + + require.True(t, checker.isSettled(nonce, headerHash)) + require.Equal(t, uint32(3), gotShardID) + require.Equal(t, headerHash, gotHash) + require.Equal(t, nonce, gotNonce) + }) + + t.Run("a proofed shard child alone does not settle", func(t *testing.T) { + t.Parallel() + + checker := &shardSettlementChecker{ + selfShardID: 0, + metaFinalityView: &testscommon.MetaFinalityViewStub{}, + } + + require.False(t, checker.isSettled(nonce, headerHash)) + }) +} + +func TestMetaSettlementChecker_IsSettled(t *testing.T) { + t.Parallel() + + nonce := uint64(10) + parentHash := []byte("parentHash") + childHash := []byte("childHash") + + newChecker := func(child data.HeaderHandler, proofedHashes ...[]byte) *metaSettlementChecker { + return &metaSettlementChecker{ + headers: &pool.HeadersPoolStub{ + GetHeaderByNonceAndShardIdCalled: func(hdrNonce uint64, shardID uint32) ([]data.HeaderHandler, [][]byte, error) { + if hdrNonce != nonce+1 || shardID != core.MetachainShardId { + return nil, nil, errors.New("no headers at nonce") + } + return []data.HeaderHandler{child}, [][]byte{childHash}, nil + }, + }, + proofs: &testscommonDataRetriever.ProofsPoolMock{ + HasProofCalled: func(_ uint32, hash []byte) bool { + for _, proofed := range proofedHashes { + if string(proofed) == string(hash) { + return true + } + } + return false + }, + }, + } + } + + t.Run("a proofed child settles the meta header", func(t *testing.T) { + t.Parallel() + + child := &block.MetaBlock{Nonce: nonce + 1, PrevHash: parentHash} + checker := newChecker(child, childHash) + + require.True(t, checker.isSettled(nonce, parentHash)) + }) + + t.Run("an unproofed child does not settle", func(t *testing.T) { + t.Parallel() + + child := &block.MetaBlock{Nonce: nonce + 1, PrevHash: parentHash} + checker := newChecker(child) + + require.False(t, checker.isSettled(nonce, parentHash)) + }) + + t.Run("a proofed child of a sibling does not settle", func(t *testing.T) { + t.Parallel() + + child := &block.MetaBlock{Nonce: nonce + 1, PrevHash: []byte("siblingHash")} + checker := newChecker(child, childHash) + + require.False(t, checker.isSettled(nonce, parentHash)) + }) + + t.Run("no child known", func(t *testing.T) { + t.Parallel() + + checker := &metaSettlementChecker{ + headers: &pool.HeadersPoolStub{}, + proofs: &testscommonDataRetriever.ProofsPoolMock{}, + } + + require.False(t, checker.isSettled(nonce, parentHash)) + }) +} diff --git a/process/sync/shardblock.go b/process/sync/shardblock.go index 6fe62307530..6a05a38dce9 100644 --- a/process/sync/shardblock.go +++ b/process/sync/shardblock.go @@ -9,6 +9,7 @@ import ( "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/data" "github.com/multiversx/mx-chain-core-go/data/block" + "github.com/multiversx/mx-chain-go/dataRetriever" "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-go/storage" @@ -33,6 +34,9 @@ func NewShardBootstrap(arguments ArgShardBootstrapper) (*ShardBootstrap, error) if check.IfNil(arguments.PoolsHolder.MiniBlocks()) { return nil, process.ErrNilTxBlockBody } + if check.IfNil(arguments.MetaFinalityView) { + return nil, process.ErrNilMetaFinalityView + } err := checkBaseBootstrapParameters(arguments.ArgBaseBootstrapper) if err != nil { @@ -87,6 +91,10 @@ func NewShardBootstrap(arguments ArgShardBootstrapper) (*ShardBootstrap, error) base.blockBootstrapper = &boot base.syncStarter = &boot + base.settlementChecker = &shardSettlementChecker{ + metaFinalityView: arguments.MetaFinalityView, + selfShardID: arguments.ShardCoordinator.SelfId(), + } base.requestMiniBlocks = boot.requestMiniBlocksFromHeaderWithNonceIfMissing // placed in struct fields for performance reasons diff --git a/process/sync/shardblock_test.go b/process/sync/shardblock_test.go index b1229e07e76..76fb0fbd0eb 100644 --- a/process/sync/shardblock_test.go +++ b/process/sync/shardblock_test.go @@ -388,6 +388,7 @@ func CreateShardBootstrapMockArguments() sync.ArgShardBootstrapper { argsShardBootstrapper := sync.ArgShardBootstrapper{ ArgBaseBootstrapper: argsBaseBootstrapper, + MetaFinalityView: &testscommon.MetaFinalityViewStub{}, } return argsShardBootstrapper diff --git a/testscommon/metaFinalityViewStub.go b/testscommon/metaFinalityViewStub.go new file mode 100644 index 00000000000..3c569cf2d92 --- /dev/null +++ b/testscommon/metaFinalityViewStub.go @@ -0,0 +1,42 @@ +package testscommon + +import "github.com/multiversx/mx-chain-core-go/data" + +// MetaFinalityViewStub - +type MetaFinalityViewStub struct { + IsMetaHeaderHeldFinalCalled func(header data.HeaderHandler, headerHash []byte) bool + IsIncludedInHeldFinalMetaBlockCalled func(shardID uint32, headerHash []byte, nonce uint64) bool + HasHeldFinalCompetitorAtNonceCalled func(metaHeader data.HeaderHandler, metaHash []byte) bool +} + +// IsMetaHeaderHeldFinal - +func (stub *MetaFinalityViewStub) IsMetaHeaderHeldFinal(header data.HeaderHandler, headerHash []byte) bool { + if stub.IsMetaHeaderHeldFinalCalled != nil { + return stub.IsMetaHeaderHeldFinalCalled(header, headerHash) + } + + return false +} + +// IsIncludedInHeldFinalMetaBlock - +func (stub *MetaFinalityViewStub) IsIncludedInHeldFinalMetaBlock(shardID uint32, headerHash []byte, nonce uint64) bool { + if stub.IsIncludedInHeldFinalMetaBlockCalled != nil { + return stub.IsIncludedInHeldFinalMetaBlockCalled(shardID, headerHash, nonce) + } + + return false +} + +// HasHeldFinalCompetitorAtNonce - +func (stub *MetaFinalityViewStub) HasHeldFinalCompetitorAtNonce(metaHeader data.HeaderHandler, metaHash []byte) bool { + if stub.HasHeldFinalCompetitorAtNonceCalled != nil { + return stub.HasHeldFinalCompetitorAtNonceCalled(metaHeader, metaHash) + } + + return false +} + +// IsInterfaceNil - +func (stub *MetaFinalityViewStub) IsInterfaceNil() bool { + return stub == nil +}