mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
consensus: DECOMPLECT single-validator finalize — remove selfVoter shortcut (one path, ava-aligned)
Owner mandate: decomplect the n=1 finalize, do it right, one path. Supersedes the async band-aid (v1.35.13) with the structural fix. The single-node selfVoter shortcut (integration.go) was the COMPLECTING element: for K==1 it called engine.ReceiveVote from inside RequestVotes while buildBlocksLocked held t.mu, self-deadlocking on the non-reentrant t.mu.RLock (the live n=1 freeze — node hangs at 'single-node mode: self-voting'), and delivered a redundant vote that could sit uncounted in handleVote's not-yet-tracked buffer. REMOVED entirely (field, wiring, RequestVotes branch). A K==1 chain now finalizes SOLELY through the inline build-path finalizer (buildBlocksLocked → buildSingleValidatorCertLocked → acceptWithCertCore) — one finalize path, matching avalanchego's 'a lone validator's own decision finalizes locally.' buildBlocksLocked skips RequestVotes for K==1 (no peers); RequestVotes returns early when the sample resolves to no peers (true no-op, not a query to nobody). Also (RED decentralization follow-up): gate reclampCommitteeLocked AND the buildSingleValidatorCertLocked count>1 synthesize-refusal on a RESOLVED set (GetFinalizedHeight set = ≥1 finalized block), so a fresh single-validator chain that transiently over-reports its validator count cannot wedge its own first block. PROOF through the REAL runtime path: TestBlue_SingleValidator_DecidesThroughFullRuntimePath now drives NewRuntime (real gossiperProposer) 0→1→2 and asserts VM.Accept + height advances — HANGS pre-decomplect (deadlock, 25s timeout), PASSES post. params_test / single_node_test converted to the decomplected invariant (K==1 polls nobody; K>1 polls network). Full engine/chain suite green incl -race. v1.35.14. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+31
-7
@@ -2820,6 +2820,17 @@ func (t *Transitive) reclampCommitteeLocked() {
|
||||
if t.liveValidatorCount == nil || t.presetK <= 0 {
|
||||
return
|
||||
}
|
||||
// RESOLVED-SET GATE (do not trust the sampler until the chain is actually running). A FRESH
|
||||
// single-validator chain can transiently over-report its validator count before the set is
|
||||
// resolved (the genesis/staking set not yet pruned to the active validator); reclamping K→N on
|
||||
// that read would skip the K==1 inline finalize and wedge the chain at block 0. Requiring at
|
||||
// least one FINALIZED block (GetFinalizedHeight set) means the chain has already produced +
|
||||
// finalized a block under its construction committee, so the live set read is now authoritative.
|
||||
// A genuinely single-validator chain (count==1) never reclamps regardless; a chain that
|
||||
// decentralizes has long since finalized blocks, so this never blocks the real 1→N transition.
|
||||
if _, set := t.consensus.GetFinalizedHeight(); !set {
|
||||
return
|
||||
}
|
||||
count := t.liveValidatorCount()
|
||||
if count < 1 {
|
||||
return // set not resolved/loaded for this network — keep the construction committee
|
||||
@@ -2859,9 +2870,16 @@ func (t *Transitive) buildSingleValidatorCertLocked(pending *PendingBlock, block
|
||||
// peers' votes arrive). count<1 (unwired sampler / --dev / a not-yet-resolved set) keeps the
|
||||
// genuine-single-validator + dev path (the n=1 fix) — a fresh chain that knows of no other
|
||||
// validator has no fork to create.
|
||||
// RESOLVED-SET GATE (mirrors reclampCommitteeLocked): only refuse to synthesize once the chain
|
||||
// has FINALIZED at least one block, so a fresh single-validator chain that transiently
|
||||
// over-reports its count cannot wedge its own first block by refusing the 1-of-1 cert. Once the
|
||||
// set is authoritative, a live count > 1 means this stuck-K==1 engine must NOT finalize
|
||||
// unilaterally (the 1→N fork) — return ZERO and let a real quorum finalize.
|
||||
if t.liveValidatorCount != nil {
|
||||
if count := t.liveValidatorCount(); count > 1 {
|
||||
return VerifiedQuorumCert{}
|
||||
if _, resolved := t.consensus.GetFinalizedHeight(); resolved {
|
||||
if count := t.liveValidatorCount(); count > 1 {
|
||||
return VerifiedQuorumCert{}
|
||||
}
|
||||
}
|
||||
}
|
||||
// K==1 FALLBACK — synthesize the 1-of-1 finality witness from the position. Both callers
|
||||
@@ -3297,12 +3315,18 @@ func (t *Transitive) buildBlocksLocked(ctx context.Context) error {
|
||||
ParentID: vmBlock.ParentID(),
|
||||
}
|
||||
t.proposer.Propose(ctx, proposal)
|
||||
voteReq := VoteRequest{
|
||||
BlockID: vmBlock.ID(),
|
||||
BlockData: vmBlock.Bytes(),
|
||||
Validators: nil,
|
||||
// Solicit peer votes ONLY on a multi-validator chain. A K==1 chain has no peers to
|
||||
// poll and finalizes SOLELY through the inline finalizer below — calling RequestVotes
|
||||
// here was the entry to the removed single-node self-vote shortcut that self-deadlocked
|
||||
// on t.mu. (Propose above is kept so non-validating FOLLOWERS still receive the block.)
|
||||
if t.consensus.K() > 1 {
|
||||
voteReq := VoteRequest{
|
||||
BlockID: vmBlock.ID(),
|
||||
BlockData: vmBlock.Bytes(),
|
||||
Validators: nil,
|
||||
}
|
||||
t.proposer.RequestVotes(ctx, voteReq)
|
||||
}
|
||||
t.proposer.RequestVotes(ctx, voteReq)
|
||||
}
|
||||
|
||||
// Self-finalize the just-built own block through the SOLE cert-gated
|
||||
|
||||
+14
-37
@@ -326,32 +326,14 @@ func NewRuntime(cfg NetworkConfig) *Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
// Wire the proposer (adapts Gossiper to BlockProposer interface).
|
||||
// In single-node mode (K=1, e.g. --dev), provide a self-voter callback
|
||||
// so the proposer can accept its own blocks without network round-trips.
|
||||
var selfVoter func(ids.ID)
|
||||
if params.K == 1 {
|
||||
selfVoter = func(blockID ids.ID) {
|
||||
// ASYNC — MUST NOT reenter the engine lock on the caller's goroutine. RequestVotes
|
||||
// invokes this selfVoter while buildBlocksLocked HOLDS t.mu (write), and
|
||||
// engine.ReceiveVote acquires t.mu.RLock; a SYNCHRONOUS call self-deadlocks —
|
||||
// sync.RWMutex is not reentrant, so the same goroutine blocks forever on RLock while
|
||||
// holding the write lock. That is the live single-validator freeze: the node logs
|
||||
// "single-node mode: self-voting" and then hangs there, so the inline K==1 finalize
|
||||
// (which runs AFTER RequestVotes) never executes and no block ever decides. Handing the
|
||||
// vote to a fresh goroutine lets buildBlocksLocked finish its inline finalize and release
|
||||
// t.mu; ReceiveVote then runs and delivers the (redundant, for the own-proposal path)
|
||||
// self-vote — and is the finality trigger for the rebuild / re-poll paths that have no
|
||||
// inline finalize. ReceiveVote is non-blocking (bounded channel + started check), so the
|
||||
// goroutine returns immediately (no leak).
|
||||
go engine.ReceiveVote(Vote{
|
||||
BlockID: blockID,
|
||||
NodeID: cfg.NodeID,
|
||||
Accept: true,
|
||||
SignedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Wire the proposer (adapts Gossiper to BlockProposer interface). There is NO single-node
|
||||
// self-voter shortcut: a K==1 chain has no peers to solicit, so it finalizes SOLELY through the
|
||||
// inline build-path finalizer (buildBlocksLocked → buildSingleValidatorCertLocked →
|
||||
// acceptWithCertCore) — one finalize path, matching avalanchego's "a lone validator's own
|
||||
// decision finalizes locally." The removed shortcut called engine.ReceiveVote from inside
|
||||
// RequestVotes while buildBlocksLocked held t.mu, self-deadlocking on the non-reentrant
|
||||
// t.mu.RLock (the live n=1 freeze), and delivered a redundant vote that could sit uncounted in
|
||||
// handleVote's not-yet-tracked buffer. Both hazards are gone with the path removed.
|
||||
engine.SetProposer(&gossiperProposer{
|
||||
gossiper: cfg.Gossiper,
|
||||
chainID: cfg.ChainID,
|
||||
@@ -360,7 +342,6 @@ func NewRuntime(cfg NetworkConfig) *Runtime {
|
||||
validators: cfg.Validators,
|
||||
nodeID: cfg.NodeID,
|
||||
k: params.K,
|
||||
selfVoter: selfVoter,
|
||||
})
|
||||
|
||||
// Set the VM for block building
|
||||
@@ -528,7 +509,6 @@ type gossiperProposer struct {
|
||||
validators ValidatorSampler // For k-peer sampling
|
||||
nodeID ids.NodeID // This node's ID (to exclude from samples)
|
||||
k int // Sample size from consensus params
|
||||
selfVoter func(ids.ID) // Callback for single-node self-voting (--dev mode)
|
||||
}
|
||||
|
||||
var _ BlockProposer = (*gossiperProposer)(nil)
|
||||
@@ -592,17 +572,14 @@ func (p *gossiperProposer) RequestVotes(ctx context.Context, req VoteRequest) er
|
||||
}
|
||||
validators = filtered
|
||||
|
||||
// Single-node mode: all sampled validators were self.
|
||||
// Deliver a self-vote directly — no network round-trip needed.
|
||||
if len(filtered) == 0 && p.k == 1 && p.selfVoter != nil {
|
||||
if p.logger != nil && !p.logger.IsZero() {
|
||||
p.logger.Info("single-node mode: self-voting for proposed block",
|
||||
log.Stringer("blockID", req.BlockID))
|
||||
}
|
||||
p.selfVoter(req.BlockID)
|
||||
// NO single-node self-vote shortcut: a K==1 chain finalizes via the inline build-path
|
||||
// finalizer (buildBlocksLocked), not by soliciting/self-delivering a vote here (see
|
||||
// NewRuntime). When the sample resolves to NO peers (single validator, or every sampled
|
||||
// node was self) there is nobody to poll — return without sending, so RequestVotes is a
|
||||
// true no-op rather than a query to an empty recipient set.
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.logger != nil && !p.logger.IsZero() {
|
||||
p.logger.Debug("sampled k validators for poll",
|
||||
log.Stringer("blockID", req.BlockID),
|
||||
|
||||
@@ -117,13 +117,10 @@ func TestConsensusConfigurations(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nodeID, sampler := makeValidators(tc.totalValidators)
|
||||
gossiper := &testGossiper{}
|
||||
selfVoted := false
|
||||
|
||||
var selfVoter func(ids.ID)
|
||||
if tc.k == 1 {
|
||||
selfVoter = func(_ ids.ID) { selfVoted = true }
|
||||
}
|
||||
|
||||
// DECOMPLECTED: there is no single-node self-voter. A K==1 proposer polls NOBODY (no
|
||||
// peers) and finalizes via the inline build-path finalizer; a K>1 proposer polls the
|
||||
// network. RequestVotes never self-votes.
|
||||
proposer := &gossiperProposer{
|
||||
gossiper: gossiper,
|
||||
chainID: ids.GenerateTestID(),
|
||||
@@ -131,7 +128,6 @@ func TestConsensusConfigurations(t *testing.T) {
|
||||
validators: sampler,
|
||||
nodeID: nodeID,
|
||||
k: tc.k,
|
||||
selfVoter: selfVoter,
|
||||
}
|
||||
|
||||
blockID := ids.GenerateTestID()
|
||||
@@ -143,12 +139,7 @@ func TestConsensusConfigurations(t *testing.T) {
|
||||
t.Fatalf("RequestVotes failed: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectSelfVote && !selfVoted {
|
||||
t.Error("expected self-vote but didn't get one")
|
||||
}
|
||||
if !tc.expectSelfVote && selfVoted {
|
||||
t.Error("got unexpected self-vote")
|
||||
}
|
||||
_ = tc.expectSelfVote // self-vote path removed; field retained as documentation only
|
||||
if tc.expectNetworkPoll && gossiper.pushQueries == 0 {
|
||||
t.Error("expected network poll but no PushQuery sent")
|
||||
}
|
||||
@@ -265,9 +256,8 @@ func TestSelfVoteOnlyForK1(t *testing.T) {
|
||||
t.Run("K="+string(rune('0'+k/10))+string(rune('0'+k%10)), func(t *testing.T) {
|
||||
nodeID, sampler := makeValidators(k)
|
||||
gossiper := &testGossiper{}
|
||||
selfVoted := false
|
||||
|
||||
// selfVoter should NOT be set for K>1
|
||||
// DECOMPLECTED: no self-voter exists. A K>1 proposer polls the network.
|
||||
proposer := &gossiperProposer{
|
||||
gossiper: gossiper,
|
||||
chainID: ids.GenerateTestID(),
|
||||
@@ -275,7 +265,6 @@ func TestSelfVoteOnlyForK1(t *testing.T) {
|
||||
validators: sampler,
|
||||
nodeID: nodeID,
|
||||
k: k,
|
||||
selfVoter: nil, // Correct: nil for K>1
|
||||
}
|
||||
|
||||
_ = proposer.RequestVotes(context.Background(), VoteRequest{
|
||||
@@ -283,9 +272,6 @@ func TestSelfVoteOnlyForK1(t *testing.T) {
|
||||
BlockData: []byte("block"),
|
||||
})
|
||||
|
||||
if selfVoted {
|
||||
t.Fatalf("K=%d should NOT self-vote", k)
|
||||
}
|
||||
if gossiper.pushQueries == 0 {
|
||||
t.Fatalf("K=%d should send network queries", k)
|
||||
}
|
||||
|
||||
@@ -35,59 +35,44 @@ func (g *testGossiper) SendPushQuery(_ ids.ID, _ ids.ID, _ []byte, _ []ids.NodeI
|
||||
func (g *testGossiper) SendPullQuery(_ ids.ID, _ ids.ID, _ ids.ID, _ []ids.NodeID) int { return 0 }
|
||||
func (g *testGossiper) SendVote(_ ids.ID, _ ids.NodeID, _ ids.ID) error { return nil }
|
||||
|
||||
// TestSingleNodeSelfVote verifies that in --dev mode (K=1, single validator),
|
||||
// the proposer delivers a self-vote instead of trying to poll 0 peers.
|
||||
// This is the standard path for local single-validator networks.
|
||||
func TestSingleNodeSelfVote(t *testing.T) {
|
||||
// TestSingleNode_PollsNobody verifies the DECOMPLECTED single-validator path: a K==1 proposer
|
||||
// polls NOBODY (no peers) and does NOT self-vote — there is no self-voter shortcut. Finality
|
||||
// comes solely from the inline build-path finalizer (proven in singlenode_decide_test.go).
|
||||
// RequestVotes returns cleanly and sends zero network queries.
|
||||
func TestSingleNode_PollsNobody(t *testing.T) {
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
blockID := ids.GenerateTestID()
|
||||
|
||||
gossiper := &testGossiper{}
|
||||
var selfVoted bool
|
||||
var votedBlockID ids.ID
|
||||
|
||||
proposer := &gossiperProposer{
|
||||
gossiper: gossiper,
|
||||
chainID: ids.GenerateTestID(),
|
||||
networkID: ids.GenerateTestID(),
|
||||
validators: &mockValidatorSampler{nodeID: nodeID},
|
||||
nodeID: nodeID,
|
||||
k: 1, // Single-node mode (--dev)
|
||||
selfVoter: func(id ids.ID) {
|
||||
selfVoted = true
|
||||
votedBlockID = id
|
||||
},
|
||||
k: 1, // single validator
|
||||
}
|
||||
|
||||
err := proposer.RequestVotes(context.Background(), VoteRequest{
|
||||
if err := proposer.RequestVotes(context.Background(), VoteRequest{
|
||||
BlockID: blockID,
|
||||
BlockData: []byte("block data"),
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
t.Fatalf("RequestVotes failed: %v", err)
|
||||
}
|
||||
|
||||
if !selfVoted {
|
||||
t.Fatal("single-node proposer did not self-vote")
|
||||
}
|
||||
if votedBlockID != blockID {
|
||||
t.Fatalf("self-voted for wrong block: got %s, want %s", votedBlockID, blockID)
|
||||
}
|
||||
if gossiper.pushQueries != 0 {
|
||||
t.Fatalf("single-node mode should not send push queries to network: got %d", gossiper.pushQueries)
|
||||
t.Fatalf("K==1 must poll nobody (no peers, no self-vote shortcut): got %d push queries", gossiper.pushQueries)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiNodeDoesNotSelfVote verifies that with K>1 (normal consensus),
|
||||
// the standard network polling path is used — no self-voting shortcut.
|
||||
func TestMultiNodeDoesNotSelfVote(t *testing.T) {
|
||||
// TestMultiNode_PollsNetwork verifies that with K>1 the proposer polls the network (no
|
||||
// self-voting shortcut ever existed for multi-validator, and none exists now).
|
||||
func TestMultiNode_PollsNetwork(t *testing.T) {
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
peerID := ids.GenerateTestNodeID()
|
||||
blockID := ids.GenerateTestID()
|
||||
|
||||
gossiper := &testGossiper{}
|
||||
selfVoted := false
|
||||
|
||||
proposer := &gossiperProposer{
|
||||
gossiper: gossiper,
|
||||
chainID: ids.GenerateTestID(),
|
||||
@@ -97,23 +82,16 @@ func TestMultiNodeDoesNotSelfVote(t *testing.T) {
|
||||
peers: []ids.NodeID{peerID},
|
||||
},
|
||||
nodeID: nodeID,
|
||||
k: 3, // Multi-node mode (normal 3+ validator consensus)
|
||||
selfVoter: func(_ ids.ID) {
|
||||
selfVoted = true
|
||||
},
|
||||
k: 3, // multi-validator
|
||||
}
|
||||
|
||||
err := proposer.RequestVotes(context.Background(), VoteRequest{
|
||||
if err := proposer.RequestVotes(context.Background(), VoteRequest{
|
||||
BlockID: blockID,
|
||||
BlockData: []byte("block data"),
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
t.Fatalf("RequestVotes failed: %v", err)
|
||||
}
|
||||
|
||||
if selfVoted {
|
||||
t.Fatal("multi-node mode should NOT self-vote — must use network consensus")
|
||||
}
|
||||
if gossiper.pushQueries != 1 {
|
||||
t.Fatalf("multi-node mode should send push query via network: got %d", gossiper.pushQueries)
|
||||
}
|
||||
@@ -134,19 +112,3 @@ func (m *multiValidatorSampler) Sample(_ ids.ID, k int) ([]ids.NodeID, error) {
|
||||
}
|
||||
|
||||
func (m *multiValidatorSampler) Count(_ ids.ID) int { return 1 + len(m.peers) }
|
||||
|
||||
// TestSelfVoterNilWhenKGreaterThan1 verifies the integration layer only
|
||||
// creates a selfVoter callback when K==1.
|
||||
func TestSelfVoterNilWhenKGreaterThan1(t *testing.T) {
|
||||
// K=1: selfVoter should be set
|
||||
proposerK1 := &gossiperProposer{k: 1, selfVoter: func(_ ids.ID) {}}
|
||||
if proposerK1.selfVoter == nil {
|
||||
t.Fatal("K=1 should have selfVoter set")
|
||||
}
|
||||
|
||||
// K>1: selfVoter should NOT trigger
|
||||
proposerK3 := &gossiperProposer{k: 3}
|
||||
if proposerK3.selfVoter != nil {
|
||||
t.Fatal("K>1 should not have selfVoter set")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,18 @@ func (s singleSelfSampler) Sample(ids.ID, int) ([]ids.NodeID, error) {
|
||||
func (s singleSelfSampler) Count(ids.ID) int { return 1 }
|
||||
|
||||
// TestBlue_SingleValidator_DecidesThroughFullRuntimePath drives the REAL runtime path a live
|
||||
// single-validator luxd uses — NewRuntime wires the gossiperProposer + selfVoter, so a build
|
||||
// runs Propose → RequestVotes → selfVoter → engine.ReceiveVote AND the inline build-loop finalize.
|
||||
// (The earlier tests used raw New with no proposer, so they never exercised the selfVoter path —
|
||||
// that is why they passed while the live chain froze.) With VM.Accept succeeding, the block MUST
|
||||
// DECIDE and the finalized height advance to 1.
|
||||
// single-validator luxd uses — NewRuntime wires the gossiperProposer, so a build runs Propose +
|
||||
// (for K>1) RequestVotes AND the inline build-loop finalize. There is NO self-voter shortcut: the
|
||||
// block finalizes SOLELY through the inline finalizer. (The earlier cert-fn-in-isolation tests
|
||||
// used raw New with no proposer and passed while the live chain froze on the removed selfVoter's
|
||||
// t.mu-reentrant deadlock.) With VM.Accept succeeding, the block MUST DECIDE and the finalized
|
||||
// height advance to 1.
|
||||
func TestBlue_SingleValidator_DecidesThroughFullRuntimePath(t *testing.T) {
|
||||
self := ids.GenerateTestNodeID()
|
||||
p := config.SingleValidatorParams() // K=1
|
||||
blk := &trackingMockBlock{id: ids.GenerateTestID(), parentID: ids.Empty, height: 1, timestamp: time.Now(), bytes: []byte("b1")}
|
||||
vm := &trackingMockVM{blocks: []*trackingMockBlock{blk}}
|
||||
blk1 := &trackingMockBlock{id: ids.GenerateTestID(), parentID: ids.Empty, height: 1, timestamp: time.Now(), bytes: []byte("b1")}
|
||||
blk2 := &trackingMockBlock{id: ids.GenerateTestID(), parentID: blk1.id, height: 2, timestamp: time.Now(), bytes: []byte("b2")}
|
||||
vm := &trackingMockVM{blocks: []*trackingMockBlock{blk1, blk2}}
|
||||
|
||||
rt := NewRuntime(NetworkConfig{
|
||||
ChainID: ids.GenerateTestID(),
|
||||
@@ -45,7 +47,7 @@ func TestBlue_SingleValidator_DecidesThroughFullRuntimePath(t *testing.T) {
|
||||
Gossiper: &recordingGossiper{}, // all sends return 0 (no peers) — the single-node condition
|
||||
VM: vm,
|
||||
Params: &p,
|
||||
VoteVerifier: rejectingVerifier{}, // wired like a value chain; rejects → my n=1 synthesize path
|
||||
VoteVerifier: rejectingVerifier{}, // wired like a value chain; rejects → the synthesize path
|
||||
VoteSigner: testAuth.signerFor(self),
|
||||
})
|
||||
ctx := context.Background()
|
||||
@@ -54,18 +56,29 @@ func TestBlue_SingleValidator_DecidesThroughFullRuntimePath(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(func() { _ = rt.Stop(context.Background()) })
|
||||
|
||||
// Drive the build through the FULL runtime path.
|
||||
// Drive TWO builds through the FULL runtime path — height must advance 0→1→2, each block
|
||||
// finalized (VM.Accept called once). Pre-fix this HANGS at the first build (selfVoter reentrant
|
||||
// deadlock, 25s test timeout); post-decomplect both blocks DECIDE.
|
||||
if err := rt.Notify(ctx, Message{Type: PendingTxs}); err != nil {
|
||||
t.Fatalf("notify: %v", err)
|
||||
t.Fatalf("notify(1): %v", err)
|
||||
}
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
|
||||
if got := blk.AcceptCalled(); got != 1 {
|
||||
t.Fatalf("FULL-RUNTIME single-validator STALL: the block did not DECIDE through the real "+
|
||||
"NewRuntime path (Propose→RequestVotes→selfVoter→ReceiveVote + inline finalize); AcceptCalled=%d", got)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
if got := blk1.AcceptCalled(); got != 1 {
|
||||
t.Fatalf("FULL-RUNTIME STALL at height 1: block did not DECIDE (VM.Accept AcceptCalled=%d)", got)
|
||||
}
|
||||
if h, ok := rt.consensus.GetFinalizedHeight(); !ok || h != 1 {
|
||||
t.Fatalf("consensus finalized height must be 1 after the single-validator block decides; got (%d,%v)", h, ok)
|
||||
t.Fatalf("finalized height must be 1 after block 1; got (%d,%v)", h, ok)
|
||||
}
|
||||
|
||||
if err := rt.Notify(ctx, Message{Type: PendingTxs}); err != nil {
|
||||
t.Fatalf("notify(2): %v", err)
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
if got := blk2.AcceptCalled(); got != 1 {
|
||||
t.Fatalf("FULL-RUNTIME STALL at height 2: block did not DECIDE (VM.Accept AcceptCalled=%d)", got)
|
||||
}
|
||||
if h, ok := rt.consensus.GetFinalizedHeight(); !ok || h != 2 {
|
||||
t.Fatalf("finalized height must advance to 2; got (%d,%v)", h, ok)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user