Skip to content

Don't restart non-validators that are behind - #475

Draft
samliok wants to merge 22 commits into
mainfrom
cleanup-instance
Draft

Don't restart non-validators that are behind#475
samliok wants to merge 22 commits into
mainfrom
cleanup-instance

Conversation

@samliok

@samliok samliok commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator
  • Checks if we have validated a higher epoch before transitioning to a validator
  • Also cleans up the instance code and removes the nodeRole struct

Comment thread instance.go
onEpochChange: func(epoch uint64, validators common.Nodes) error {
height := i.Config.PlatformChain.GetCurrentHeight()
vdrs, err := i.Config.PlatformChain.GetValidatorSet(height)
// set the communication to the highest validator set, since this node is a non-validator and may be behind.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this basically a poor man's caching of the highest validator set from the P-chain?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it uses the same mechanism as before, i just extracted it to a helper.

But yea, we should feed the non-validator the highest pchain validators. I think alternatively we could just pass in the platform chain interface into the non-validator. This could help if the non-validator was offline and the validator set diverged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 👍 if the non-validator was offline and the validator set has drifted away, it may still be able to replicate the P-chain and get the latest validator set.

Comment thread instance.go Outdated
switch epochChange.nodeRole {
case nonValidator:
switch {
case i.e != nil && i.nv != nil:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we removed the role but can we at least put some more meaningful code than i.e != nil and i.nv != nil?

Maybe:

amValidator := i.e != nil
amNonValidator := i.nv != nil

?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment thread 0_build.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this file belongs to the PR

Comment thread instance.go
default:
// The slot holds a stale epoch change: take it and keep the newer of the two.
select {
case pending := <-i.epochChanges:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can this happen?

Comment thread instance_test.go Outdated
require.ErrorContains(t, inst.Start(t.Context()), "instance already started")
}

// currentNonValidator returns the non-validator the instance is running, or nil if it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a weird thing to do. Can't we achieve the same thing in a different way?

Comment thread instance.go
case <-i.stopCh:
return
default:
// The slot holds a stale epoch change: take it and keep the newer of the two.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we can't just wait until the goroutine has picked up the epoch change from the channel?

This way, we will never move on to higher epochs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we have a suppeerrr annoying deadlock here if we don't do something along these lines.

Essentially the index from both nonvalidator and epoch run under their own lock. So for example, when we are indexing a sealing block as a non-validator the non-validator lock is being held. Then we try to send on this channel, but it will block until the consumer can process it.

But say we have been indexing sealing blocks very fast into storage(i.e in the tests). There may be a scenario where the consumer goroutine(listenForEpochChanges), is already processing an epoch change. So we have sent an epoch change, and the channel is now full.

Now imagine immediately after we index another sealing block. The channel is now full, so our non-validator blocks.

The goroutine that is processing an epoch change may call back into the non-validator and request the non-validator lock(ex. HighestValidatedEpoch).

This means we are deadlocked because the index goroutine can only let go of the lock if the epoch change channel is empty, but in order for the epoch change channel to get empty it needs to acquire the lock.

Comment thread instance_test.go Outdated
}

// The epoch change is acted on by another goroutine, so the node has to be given the
// chance to restart before concluding that it did not.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before concluding it did not what?

Comment thread instance_test.go Outdated

// sealingBlockAt builds the sealing block of epoch `epoch` at sequence `seq`
func sealingBlockAt(seq, epoch uint64, validators metadata.NodeBLSMappings) *ParsedBlock {
md := common.ProtocolMetadata{Epoch: epoch, Round: seq, Seq: seq}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the prev hash pointer? i guess this works because we replicate it and we don't check the prev pointer because we have a QC?

@samliok
samliok marked this pull request as draft August 6, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants