Conversation
6be0d9c to
23d9c67
Compare
9c76fa7 to
9501164
Compare
9501164 to
a1bb893
Compare
9d1886a to
b4bfaba
Compare
b4bfaba to
4b6b2f5
Compare
a1a3fb9 to
b507b64
Compare
|
Looks like this test demonstrates we have a liveness problem in case we miss the first broadcast we send via |
|
In the issue #530 I wrote:
The below test demonstrates that that's not what we do: |
| return nil | ||
| } | ||
|
|
||
| n.Logger.Info("Bootstrapped, received a threshold of sealing block info for an epoch", zap.Stringer("Info", qr.Block.SealingBlockInfo())) |
There was a problem hiding this comment.
We should only consider ourselves as bootstrapped when we have replicated and committed all blocks from the last block in the ledger to the last known tip.
There was a problem hiding this comment.
i think we can consider ourselves bootstrapped once we have validated the hash chain of sealing blocks, then we can start as normal syncing all the blocks in between
There was a problem hiding this comment.
as to your test, i made a few non-validator tests to ensure we do the backwards hash validation first
TestNonValidator_BootstrapIgnoresSealingBlockOffChain && TestNonValidator_BootstrapWalksHashChain
b04bddb to
c2d3997
Compare
yacovm
left a comment
There was a problem hiding this comment.
These are the comments I have so far, I'm not nearly done with the review.
| } | ||
|
|
||
| // Equal returns whether both hold the same nodes, ignoring order. | ||
| func (nws Nodes) Equal(other Nodes) bool { |
| } | ||
|
|
||
| // HasTasks reports whether any task is still outstanding. | ||
| func (t *TimeoutHandler[T]) HasTasks() bool { |
There was a problem hiding this comment.
Maybe call it something shorter like Empty() and just use ! Empty() ?
| parent, _, err := nonValidatorNode.storage.GetBlock(1) | ||
| require.NoError(t, err) | ||
|
|
||
| // The non-validator drops every message until it bootstraps. One peer reporting the block |
There was a problem hiding this comment.
I don't understand this statement.
If it drops every message how does it bootstrap? 🤡
There was a problem hiding this comment.
ya i meant to say drops every message that doesn't help it bootstrap.
| // and it is in the validator set | ||
| TransitionToValidator func(epoch uint64, validators common.Nodes) | ||
|
|
||
| // Bootstrapped is set once every epoch from our tip up to the one a threshold of the latest |
There was a problem hiding this comment.
Why would we call that bootstrapped? Bootstrapped should just mean that we finished bootstrapping, exactly like we do in snowman. Which is that we have replicated all blocks we know are missing from the latest discovered tip down to the tip before bootstrapping.
| return nil | ||
| } | ||
|
|
||
| // No sealing block is missing, so every epoch from our tip to the highest is validated. |
There was a problem hiding this comment.
but we should still replicate all blocks in the last epoch that we know about before we declare that we have finished bootstrapping.
There was a problem hiding this comment.
I think this comment is still relevant
| } | ||
|
|
||
| comm := newCommunication(i.Config.Sender, i.Config.Broadcaster, mappings.Nodes()) | ||
| comm := newCommunication(i.Config.Sender, i.Config.Broadcaster, latestValidatorSet.Nodes()) |
There was a problem hiding this comment.
unrelated to this PR, but... what updates the comm's validator set once we move through epochs after we bootstrap?
There was a problem hiding this comment.
yea we need to change the non-validator comm to not hardcode its Validators() method. everytime it calls Validators the pchain should be queried or something
|
|
||
| // We have indexed the latest validator set, therefore we can skip bootstrapping and start as a validator. | ||
| // Note: this may not be the latest epoch, but a future PR will eventually notice we are behind and transition properly. | ||
| if latestIndexedEpochValidators.Equal(latestValidatorSet.Nodes()) && latestValidatorSet.Nodes().Contains(i.Config.ID) { |
There was a problem hiding this comment.
what if we're a validator and behind (old epoch) but somehow the latest on-chain validator set is equal to the P-chain validator set?
We won't know we're behind and will reject all messages because they will have a higher epoch.
Is #578 supposed to solve this?
| } | ||
|
|
||
| if !n.Bootstrapped && msg.ReplicationResponse == nil { | ||
| n.Logger.Debug("Dropping message received while bootstrapping, we only accept replication responses", zap.Any("Message", msg), zap.Stringer("From", from)) |
There was a problem hiding this comment.
we assume here that the message can only have a single field inside of it.
But it can have at least a single field, not up to a single field.
Shouldn't we re-order the switch a few lines below to have msg.ReplicationResponse != nil: first?
There was a problem hiding this comment.
If we're bootstrapping but replication response is not nil, we can still process block messages. Can we re-order so we have msg.ReplicationResponse != nil: first?
| for _, md := range e { | ||
| if bytes.Equal(md.prevSealingBlockHash[:], digest[:]) { | ||
| // We have validated the next epoch, and the next epoch has a backward pointer to this one | ||
| return true |
There was a problem hiding this comment.
what if the block is already indexed?
There was a problem hiding this comment.
We initialize epochs with the latest indexed sealing block so we should be good.
There was a problem hiding this comment.
switch {
case n.epochs.canValidate(block):
// The sealing block in the backwards hash chain
n.validateSealingBlock(qr, from)
case n.highestEpochCollector.collectedSealingBlockInfo(sealingInfo, bh, from):
n.Logger.Info("A threshold of validators reported a sealing block", zap.Uint64("Seq", bh.Seq), zap.Stringer("Info", sealingInfo))
n.sealingBlockTimeouts.RemoveTask(startBroadcastTask)
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
default:
return nil
}
If we get a block we've already indexed we may think we can validate it and then we will get into validateSealingBlock even though we don't need to.
We can just add a check here:
switch {
case n.epochs.canValidate(block):
// The sealing block in the backwards hash chain
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
case n.highestEpochCollector.collectedSealingBlockInfo(sealingInfo, bh, from):
n.Logger.Info("A threshold of validators reported a sealing block", zap.Uint64("Seq", bh.Seq), zap.Stringer("Info", sealingInfo))
n.sealingBlockTimeouts.RemoveTask(startBroadcastTask)
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
default:
return nil
}
no?
| bh := block.BlockHeader() | ||
| sealingInfo := block.SealingBlockInfo() | ||
|
|
||
| if sealingInfo == nil { |
There was a problem hiding this comment.
what if we have the epoch in the ledger?
There was a problem hiding this comment.
so i think we still need this in the case for these scenarios
TestNonValidatorBootstrapLatestKnownEpoch and TestNonValidatorBootstrapRequestsSealingBlock
There was a problem hiding this comment.
I am not sure i understand. If we have the epoch in the ledger why do we need to fetch it again?
d7defc8 to
8fa6844
Compare
a189c1f to
2a76d39
Compare
2a76d39 to
a02c097
Compare
| } | ||
| nv.sealingBlockTimeouts = common.NewTimeoutHandler(config.Logger, "sealing block replication", config.StartTime, simplex.DefaultReplicationRequestTimeout, nv.requestMissingSealingBlocks) | ||
| if !config.Bootstrapped { | ||
| nv.sealingBlockTimeouts.AddTask(startBroadcastTask) |
There was a problem hiding this comment.
nv.sealingBlockTimeouts.AddTask(0) - I don't understand what we're trying to do here.
| // The finalization has not been verified yet. Storing tells the replicator a valid sequence exists | ||
| // and its validity is checked when the round is processed. | ||
| func (n *NonValidator) validateSealingBlock(qr *common.QuorumRound, from common.NodeID) { | ||
| n.maybeValidateNextEpoch(qr.Block, from) |
There was a problem hiding this comment.
It looks like we maybe validate and then anyway store the QR?
but maybeValidateNextEpoch can return early in many cases. Is that intentional?
I guess the purpose of validateSealingBlock is to kickstart validation of the previous sealing block? If so, is this the right name for the method?
| epochResponses[string(from)] = digest | ||
|
|
||
| // check if we have a threshold of responses | ||
| counts := make(map[common.Digest]uint64) |
There was a problem hiding this comment.
Not really relevant to this PR, but - if validators changes between two invocations of this function, then we have a problem.
Consider two invocations collectedSealingBlockInfo() in t1 and collectedSealingBlockInfo() in t2, and the first one sampled validators = [v1, v2, v3, v4] and the second once sampled v2, v3, v4, v5]'.
The first one got a vote from v1 and the second one got a vote from v2 but v1 is not in the second.
We reached the threshold f+1 but with an illegal count.
| if !n.isIndexed(bh.Seq) { | ||
| n.validateSealingBlock(qr, from) | ||
| } | ||
| default: |
There was a problem hiding this comment.
if we have a default case that returns nil then how do we proceed below?
We don't, which means this is some kind of catch-all case in which we don't want to proceed.
Can we add some comments to each if case that explain better what we're trying to do?
| // No sealing block is missing, so every epoch from our tip to the highest is validated. | ||
| if !n.sealingBlockTimeouts.Empty() { | ||
| n.finishBootstrap() | ||
| // If the highest epoch is already indexed, nothing more gets indexed to trigger the transition. |
There was a problem hiding this comment.
Don't we need to replicate the blocks between the sealing blocks?
| return nil | ||
| } | ||
|
|
||
| // No sealing block is missing, so every epoch from our tip to the highest is validated. |
There was a problem hiding this comment.
I think this comment is still relevant
| } | ||
|
|
||
| if !n.Bootstrapped && msg.ReplicationResponse == nil { | ||
| n.Logger.Debug("Dropping message received while bootstrapping, we only accept replication responses", zap.Any("Message", msg), zap.Stringer("From", from)) |
There was a problem hiding this comment.
If we're bootstrapping but replication response is not nil, we can still process block messages. Can we re-order so we have msg.ReplicationResponse != nil: first?
| bh := block.BlockHeader() | ||
| sealingInfo := block.SealingBlockInfo() | ||
|
|
||
| if sealingInfo == nil { |
There was a problem hiding this comment.
I am not sure i understand. If we have the epoch in the ledger why do we need to fetch it again?
| t.lock.Lock() | ||
| defer t.lock.Unlock() | ||
|
|
||
| return len(t.tasks) > 0 |
There was a problem hiding this comment.
Shouldn't this be == instead of > ?
| for _, md := range e { | ||
| if bytes.Equal(md.prevSealingBlockHash[:], digest[:]) { | ||
| // We have validated the next epoch, and the next epoch has a backward pointer to this one | ||
| return true |
There was a problem hiding this comment.
switch {
case n.epochs.canValidate(block):
// The sealing block in the backwards hash chain
n.validateSealingBlock(qr, from)
case n.highestEpochCollector.collectedSealingBlockInfo(sealingInfo, bh, from):
n.Logger.Info("A threshold of validators reported a sealing block", zap.Uint64("Seq", bh.Seq), zap.Stringer("Info", sealingInfo))
n.sealingBlockTimeouts.RemoveTask(startBroadcastTask)
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
default:
return nil
}
If we get a block we've already indexed we may think we can validate it and then we will get into validateSealingBlock even though we don't need to.
We can just add a check here:
switch {
case n.epochs.canValidate(block):
// The sealing block in the backwards hash chain
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
case n.highestEpochCollector.collectedSealingBlockInfo(sealingInfo, bh, from):
n.Logger.Info("A threshold of validators reported a sealing block", zap.Uint64("Seq", bh.Seq), zap.Stringer("Info", sealingInfo))
n.sealingBlockTimeouts.RemoveTask(startBroadcastTask)
if !n.isIndexed(bh.Seq) {
n.validateSealingBlock(qr, from)
}
default:
return nil
}
no?
Addresses #530 but for non-validator and validators.
Nodes will first need to complete bootstrapping before being able to index/verify blocks.