-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic epoch implementation - no failover #14
Conversation
6cef669
to
b54dd90
Compare
encoding.go
Outdated
return nr.Signatures, signers, finalization, nil | ||
} | ||
|
||
func quorumRecord(signatures [][]byte, signers []NodeID, rawVote []byte, recordType uint16) Record { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename to newQuorumRecord
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on second thought, the function seems to be doing multiple things. would it make sense to rename QuorumRecord
to QuorumPayload
? Since it's just the payload of a Record
type. Then we can split this method into newQuorumPayload
and have a re-usable newRecord(payload, type)
function.
encoding.go
Outdated
copy(buff, mdSizeBuff) | ||
copy(buff[4:], blockDataSizeBuff) | ||
copy(buff[8:], mdBytes) | ||
copy(buff[8+len(mdBytes):], blockData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think calling append()
rather than copy would clean up the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think you're right
|
||
expectedBuffSize := int(mdSizeBuff + blockDataSizeBuff) | ||
|
||
if len(buff) < expectedBuffSize { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't we expect them to be equal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the way we encode the record is - we encode the size of the metadata and the size of the block data and then we put the actual data.
It might be that the data is from some reason (due to a corruption, or whatever) too small.
If we don't have this check, we will get an array out of bounds panic, hence it's worth to check to be on the safe side.
} | ||
|
||
var fCert FinalizationCertificate | ||
fCert.Finalization = finalization.Finalization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could just do finalizations[0].Finalization
so we don't need to set finalization
above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then the lines are longer, it's used too many times. Better to allocate a stack variable for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i was referring to this initialization above finalization := finalizations[0]
. Fine with keeping it as is, but we only use the finalization
variable once
epoch.go
Outdated
for _, signer := range fCert.AggregatedSignedVote.Signers { | ||
signers = append(signers, signer) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for _, signer := range fCert.AggregatedSignedVote.Signers { | |
signers = append(signers, signer) | |
} | |
signers = append(signers, fCert.AggregatedSignedVote.Signers...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we should even be copying it, I think we could just do signers = fCert.AggregatedSignedVote.Signers
and avoid the allocation entirely.
epoch.go
Outdated
|
||
expectedDigest := e.BlockDigester.Digest(block) | ||
|
||
if md.Version != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we separate this into a constant?
Seq: expectedSeq, | ||
Epoch: e.Epoch, | ||
Prev: expectedPrevDigest, | ||
Version: 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
epoch.go
Outdated
func quorum(n int) int { | ||
// Obtained from the equation: | ||
// Quorum * 2 = N + F + 1 | ||
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) | |
return int(math.Ceil((float64(n) + float64(n-1)/3 + 1) / 2.0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The computation is actually on purpose as it is written. As per the comment - (n-1)/3
is actually F
and it's computed via a regular integer division. Setting this to a float will make it lose its semantic meaning as F
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in that case float() should be around the numerator
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) | |
return int(math.Ceil(float64((n + (n-1)/3 + 1)) / 2.0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lemme know if the new version is better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couple more comments/suggestions
epoch.go
Outdated
|
||
func (e *Epoch) loadLastBlockFromStorage() { | ||
lastBlock, retrieved := e.retrieveLastBlockFromStorage() | ||
e.lastBlock, e.noBlockCommittedYet = lastBlock, !retrieved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e.lastBlock
isn't used anywhere. should we delete it?
Also to clarify, if no block has been committed yet then we must be the first epoch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah good point, i ended up not using it after some code changes I made and forgot to remove it.
Also to clarify, if no block has been committed yet then we must be the first epoch?
Yes, of course. In order to change an epoch we need to ingest an epoch changing block.
But this is irrelevant to the epoch implementation, as it cannot be aware of epoch changes. The epoch will "die" once we have an epoch change processed. This will be added when we are done with implementing the epoch properly.
epoch.go
Outdated
func (e *Epoch) loadLastBlockFromStorage() { | ||
lastBlock, retrieved := e.retrieveLastBlockFromStorage() | ||
e.lastBlock, e.noBlockCommittedYet = lastBlock, !retrieved | ||
|
||
// Put the last block we committed in the rounds map. | ||
if retrieved { | ||
round := NewRound(lastBlock) | ||
e.rounds[round.num] = round | ||
} | ||
} | ||
|
||
func (e *Epoch) retrieveLastBlockFromStorage() (Block, bool) { | ||
var lastBlock Block | ||
var retrieved bool | ||
|
||
height := e.Storage.Height() | ||
if height > 0 { | ||
lastBlock, _, retrieved = e.Storage.Retrieve(height - 1) | ||
if !retrieved { | ||
e.Logger.Fatal("Failed retrieving last block from storage", zap.Uint64("seq", height-1)) | ||
} | ||
return lastBlock, true | ||
} | ||
return lastBlock, false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func (e *Epoch) loadLastBlockFromStorage() { | |
lastBlock, retrieved := e.retrieveLastBlockFromStorage() | |
e.lastBlock, e.noBlockCommittedYet = lastBlock, !retrieved | |
// Put the last block we committed in the rounds map. | |
if retrieved { | |
round := NewRound(lastBlock) | |
e.rounds[round.num] = round | |
} | |
} | |
func (e *Epoch) retrieveLastBlockFromStorage() (Block, bool) { | |
var lastBlock Block | |
var retrieved bool | |
height := e.Storage.Height() | |
if height > 0 { | |
lastBlock, _, retrieved = e.Storage.Retrieve(height - 1) | |
if !retrieved { | |
e.Logger.Fatal("Failed retrieving last block from storage", zap.Uint64("seq", height-1)) | |
} | |
return lastBlock, true | |
} | |
return lastBlock, false | |
} | |
func (e *Epoch) loadLastBlockFromStorage() { | |
lastBlock := e.retrieveLastBlockFromStorage() | |
e.lastBlock = lastBlock | |
// Put the last block we committed in the rounds map. | |
if lastBlock != nil { | |
round := NewRound(lastBlock) | |
e.rounds[round.num] = round | |
} | |
} | |
func (e *Epoch) retrieveLastBlockFromStorage() Block { | |
height := e.Storage.Height() | |
if height == 0 { | |
return nil | |
} | |
lastBlock, _, retrieved := e.Storage.Retrieve(height - 1) | |
if !retrieved { | |
e.Logger.Fatal("Failed retrieving last block from storage", zap.Uint64("seq", height-1)) | |
} | |
return lastBlock | |
} |
would it make sense to remove the variable noBlockCommittedYet
and clean up these two functions like so? This way we remove an extra runtime variable that could go out of sync and lead to errors
epoch.go
Outdated
|
||
height := e.Storage.Height() | ||
if height > 0 { | ||
lastBlock, _, retrieved = e.Storage.Retrieve(height - 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, i was confusing sequence numbers for round numbers(which could skip certain numbers in the case of an empty block)
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) | ||
} | ||
|
||
type messagesFromNode map[string]map[uint64]*messagesForRound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type messagesFromNode map[string]map[uint64]*messagesForRound | |
// messagesFromNode maps nodeIds to the messages it sent in a given round. | |
type messagesFromNode map[string]map[uint64]*messagesForRound |
epoch.go
Outdated
func quorum(n int) int { | ||
// Obtained from the equation: | ||
// Quorum * 2 = N + F + 1 | ||
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in that case float() should be around the numerator
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0))) | |
return int(math.Ceil(float64((n + (n-1)/3 + 1)) / 2.0)) |
epoch.go
Outdated
round, exists := e.rounds[md.Round] | ||
if !exists { | ||
round = NewRound(block) | ||
e.rounds[md.Round] = round | ||
// We might have receied votes and finalizations from future rounds before we received this block. | ||
// So load the messages into our round data structure now that we have created it. | ||
defer e.maybeLoadFutureMessages(md.Round) | ||
} else { | ||
// We have already received a block for this round in the past, refuse receiving an alternative block. | ||
// We do this because we may have already voted for a different block. | ||
// Refuse processing the block to not be coerced into voting for a different block. | ||
e.Logger.Warn("Already received a block for the round", zap.Stringer("NodeID", from), zap.Uint64("round", md.Round)) | ||
return false | ||
} | ||
return true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
round, exists := e.rounds[md.Round] | |
if !exists { | |
round = NewRound(block) | |
e.rounds[md.Round] = round | |
// We might have receied votes and finalizations from future rounds before we received this block. | |
// So load the messages into our round data structure now that we have created it. | |
defer e.maybeLoadFutureMessages(md.Round) | |
} else { | |
// We have already received a block for this round in the past, refuse receiving an alternative block. | |
// We do this because we may have already voted for a different block. | |
// Refuse processing the block to not be coerced into voting for a different block. | |
e.Logger.Warn("Already received a block for the round", zap.Stringer("NodeID", from), zap.Uint64("round", md.Round)) | |
return false | |
} | |
return true | |
if _, exists := e.rounds[md.Round]; exists { | |
// We have already received a block for this round in the past, refuse receiving an alternative block. | |
// We do this because we may have already voted for a different block. | |
// Refuse processing the block to not be coerced into voting for a different block. | |
e.Logger.Warn("Already received a block for the round", zap.Stringer("NodeID", from), zap.Uint64("round", md.Round)) | |
return false | |
} | |
e.rounds[md.Round] = NewRound(block) | |
// We might have receied votes and finalizations from future rounds before we received this block. | |
// So load the messages into our round data structure now that we have created it. | |
e.maybeLoadFutureMessages(md.Round) | |
return true |
I think this suggestion cleans up the logic a little bit, and keeps a consistent style with other parts of the code. i.e.. check for condition and return early. Also saves a couple lines, since we never actually use the original round variable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I usually prefer the style you suggested.
8cfc101
to
73a37ea
Compare
epoch.go
Outdated
return md | ||
} | ||
|
||
func (e *Epoch) doFinalized() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the functions named doFinalized
, do....
were initially a bit confusingly worded. For example doFinalized
seems like it should be performing a step in finalizing a block, however it's after the finalization is persisted. a more clear name could be startRound
.
same comment goes for methods such as doProposed
and doNotarized
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doX
means the previous thing we did was X
.
I'll think about restructuring this so it'll be less confusing.
return | ||
} | ||
|
||
// If we're not the leader, check if we have received a proposal earlier for this round |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the spec there is a delta that provides a certain timeframe in which nodes can receive messages. Would this be the right place to wait for messages within the Δ threshold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question.
I don't think so, because we wait here we can't process any other message. This mechanism should be called externally.
I envision we'll have some sort of external activator to the epoch that would do:
for {
select {
case <- timeout:
epch.handleTimeout()
case <- messages:
epoch.HandleMessage()
}
}
epoch.go
Outdated
e.phase = shutdown | ||
} | ||
|
||
func (e *Epoch) doPhase() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, epochs are just a series of rounds until a reconfiguration event
, so would it make sense to have these methods associated with a round? i.e epoch.doNotarized()
-> epoch.currentRound.doNotarized()
i think this has a benefit of making it easier to test the round
and epoch
components separately
epoch.go
Outdated
nextSeqToCommit := e.Storage.Height() | ||
if fCert.Finalization.Seq == nextSeqToCommit { | ||
block := e.rounds[fCert.Finalization.Round].block | ||
e.Storage.Index(fCert.Finalization.Seq, block, fCert) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm thinking we truncate the WAL after we index the storage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point, feel free to add this part here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, epochs are just a series of rounds until a
reconfiguration event
, so would it make sense to have these methods associated with a round? i.eepoch.doNotarized()
->epoch.currentRound.doNotarized()
i think this has a benefit of making it easier to test the
round
andepoch
components separately
The epoch implementation, is an implementation of a single epoch, so it is not aware just yet to a reconfiguration event. This should be added later. We should first have the epoch completed and solid before we expand into adding/removing nodes.
When we do doNotarized
we essentially broadcast a finalization and move to the next round.
We only have a single round that we notarize at any given point, we cannot notarize several rounds together.
Right now, the code allows us to jump to the next round if we either gather a notarization or enough votes, but at the moment we can only do this incrementally.
9bd6ab7
to
07ab3d2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A number of these questions may just be answered by "Yeah working on it.", but I just put things down as I observed them
metadata.go
Outdated
type Metadata struct { | ||
ProtocolMetadata | ||
// Digest returns a collision resistant short representation of the block's bytes | ||
Digest Digest | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of separating ProtocolMetadata
out of Metadata
? I don't see us using ProtocolMetadata
anywhere other than here.
Also, I feel like Metadata
is not a great name. I feel like any of:
BlockHeader
ProposalDescription
,BlockDescription
ProposalDesc
,BlockDesc
would make it more clear as to what it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The protocol metadata is likely to be encoded inside a block. Therefore, you cannot encode the digest of the block itself (without the universe collapsing into itself), but the digest of the block needs to be included in what we vote on.
That's why a vote is simply metadata, and the metadata is the protocol metadata + the digest of the block.
BlockHeader sounds good to me.
@@ -19,11 +20,13 @@ const ( | |||
metadataLen = metadataVersionLen + metadataDigestLen + metadataEpochLen + metadataRoundLen + metadataSeqLen + metadataPrevLen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going to define things like metadataPrevLen
and metadataDigestLen
, I think their types should be changed from []byte
to be [metadataPrevLen]byte
and [metadataDigestLen]byte
respectfully. It'll reduce the number of sanity checks we need to make and it'll improve the performance of serialization.
metadata.go
Outdated
type Digest []byte | ||
|
||
func (d Digest) String() string { | ||
return fmt.Sprintf("%x", []byte(d)[:digestFormatSize]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Seems like we should signature that there is more here.
return fmt.Sprintf("%x", []byte(d)[:digestFormatSize]) | |
return fmt.Sprintf("%x...", []byte(d)[:digestFormatSize]) |
} | ||
|
||
// Guard against receiving messages from unknown nodes | ||
_, known := e.eligibleNodeIDs[string(from)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
x.String()
is useful for pretty-printing a NodeID, but it is less efficient than string(x)
. As an example, x.String()
creates a string twice the size of string(x)
.
func (e *Epoch) increaseRound() { | ||
e.Logger.Info(fmt.Sprintf("Moving to a new round (%d --> %d", e.round, e.round+1), zap.Uint64("round", e.round+1)) | ||
e.round++ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be where we proposeBlock()
rather than in startRound()
? (not sure what startRound
would be called then)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to call startRound
when we start the entire Epoch
. I'm not sure what is the advantage of proposing the block here instead of startRound
, can you please elaborate?
e.round++ | ||
} | ||
|
||
func (e *Epoch) doNotarized() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this is probably still a TODO - but we'll need to verify that:
- The notarization is for a real block
- That we haven't voted for the dummy block yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD when dummy blocks would be introduced
return true | ||
} | ||
|
||
func (e *Epoch) getHighestRound() *Round { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we document what e.Round
is? I that that these would have expected these to be the ~same.
Alternatively it might make sense to include a "highest notarized block" value or something (as I think that'll become important once we add dummy block handling)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All exported fields are for initialization. It's essentially a poor man's configuration object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I documented the round
field, instead of the Round
one.
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
* cleanup * remove println * add lock * revert
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
… slices Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
This commit makes notarizations and finalizations not have two different signatures: - AggregatedSignedVote - SignaturesAndSigners But instead, it consolidates the two into a single abstract type called a "QuorumCertificate" (hereafter a "QC") A QC hides implementation details from Simplex, and is verifiable under a given message passed as input. Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
0b6b612
to
a3c59c3
Compare
In order to make it simple to implement replication, it's best to couple the block proposal and its corresponding vote produced by the block proposer. This way, a node different than the block proposer can prove that a certain block has been proposed by the leader of the round. Signed-off-by: Yacov Manevich <[email protected]>
… ahead Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Currently when locateBlock() fetches blocks from memory, it searches by sequence, but the rounds are sequenced by their round number, not by their sequence, which may be different. Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
…ethod of Epoch In order to make it more testable Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
…ltiNodeSimple Signed-off-by: Yacov Manevich <[email protected]>
Signed-off-by: Yacov Manevich <[email protected]>
type Message struct { | ||
BlockMessage BlockMessage | ||
BlockMessage *BlockMessage | ||
VoteMessage *Vote | ||
Notarization *Notarization | ||
Finalization *Finalization | ||
FinalizationCertificate *FinalizationCertificate | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was a bit surprised to see that these include non-concrete types (interfaces).
BlockMessage
includesBlock
which is an interfaceNotarization
andFinalizationCertificate
includeQuorumCertificate
which is an interface.
Is it expected for the networking layer that integrates with the simplex code to handle the parsing of Block
s and QuorumCertificate
s?
I wonder if it would be nicer to have the messages just be concrete types and allow the simplex code to parse these things (since it will need to parse them from the WAL anyways)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, however - The communication layer of Simplex and the application are the same layer. The advantage of having them as non-concrete types, is that if the application needs to do preliminary processing, it would anyway need to parse them in the application layer, so passing them as bytes to Simplex only to be parsed yet again seems like a waste to me.
I'm not 100% sure this is a good decision, but I felt like in avalanchego we do a lot of things (metrics, caching, etc.) outside of consensus, so it makes sense to parse the messages in the application layer because it is needed.
If this was a general consensus library to be used by an arbitrary application, then I would most probably have these messages as raw bytes.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify I wasn't trying to suggest changing HandleMessage(*Message, NodeID)
to HandleMessage([]byte, NodeID)
.
I was was wondering specifically if the Block
and QuorumCertificate
fields should be []byte
(similarly to how the raw individual signature is []byte
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I get that, but as I said - if the Block
field is bytes, then Simplex would need to parse it at least once to convert it to a Block
. But at the same time, I assume that avalanchego would want to parse the block bytes before passing it into Simplex, so that would mean we'd parse it twice, no? seems like a waste to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why avalanchego would want to parse the block before passing it into simplex. Not something we need to decide on right now though
} | ||
|
||
vote := message.BlockMessage.Vote | ||
from := vote.Signature.Signer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems dubious because we don't actually verify that this from
is valid until much later in the function.
I think the only case that this matters is with the handling of the messages for future rounds. Specifically, a virtuous node may send a valid block for a future round, and then after that a malicious node may send an invalid block, claiming to be the correct proposer. We will overwrite the proposal from the virtuous node and then only process the malicious node's block (which we will then drop).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you're right, good observation.
I addressed it here.
Signed-off-by: Yacov Manevich <[email protected]>
* simple flow * extra lines
No description provided.