Skip to content
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

Merged
merged 33 commits into from
Jan 3, 2025
Merged

Basic epoch implementation - no failover #14

merged 33 commits into from
Jan 3, 2025

Conversation

yacovm
Copy link
Collaborator

@yacovm yacovm commented Dec 9, 2024

No description provided.

@yacovm yacovm force-pushed the epoch branch 2 times, most recently from 6cef669 to b54dd90 Compare December 9, 2024 23:07
encoding.go Outdated Show resolved Hide resolved
encoding.go Outdated
return nr.Signatures, signers, finalization, nil
}

func quorumRecord(signatures [][]byte, signers []NodeID, rawVote []byte, recordType uint16) Record {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: rename to newQuorumRecord?

Copy link
Collaborator

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)
Copy link
Collaborator

@samliok samliok Dec 10, 2024

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

Copy link
Collaborator Author

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 {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

epoch.go Show resolved Hide resolved
}

var fCert FinalizationCertificate
fCert.Finalization = finalization.Finalization
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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
Comment on lines 465 to 460
for _, signer := range fCert.AggregatedSignedVote.Signers {
signers = append(signers, signer)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
for _, signer := range fCert.AggregatedSignedVote.Signers {
signers = append(signers, signer)
}
signers = append(signers, fCert.AggregatedSignedVote.Signers...)

Copy link
Collaborator

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 {
Copy link
Collaborator

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,
Copy link
Collaborator

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)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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))

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Suggested change
return int(math.Ceil(float64((n + (n-1)/3 + 1) / 2.0)))
return int(math.Ceil(float64((n + (n-1)/3 + 1)) / 2.0))

Copy link
Collaborator Author

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

Copy link
Collaborator

@samliok samliok left a 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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Comment on lines 174 to 187
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
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Collaborator

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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)))
Copy link
Collaborator

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

Suggested change
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
Comment on lines 1046 to 1056
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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

Copy link
Collaborator Author

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.

@samliok samliok force-pushed the epoch branch 2 times, most recently from 8cfc101 to 73a37ea Compare December 17, 2024 17:43
epoch.go Outdated
return md
}

func (e *Epoch) doFinalized() {
Copy link
Collaborator

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

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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() {
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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

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.

@yacovm yacovm force-pushed the epoch branch 2 times, most recently from 9bd6ab7 to 07ab3d2 Compare December 24, 2024 21:32
Copy link
Collaborator

@StephenButtolph StephenButtolph left a 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

epoch.go Outdated Show resolved Hide resolved
metadata.go Outdated
Comment on lines 42 to 46
type Metadata struct {
ProtocolMetadata
// Digest returns a collision resistant short representation of the block's bytes
Digest Digest
}
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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])
Copy link
Collaborator

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.

Suggested change
return fmt.Sprintf("%x", []byte(d)[:digestFormatSize])
return fmt.Sprintf("%x...", []byte(d)[:digestFormatSize])

record/record.go Outdated Show resolved Hide resolved
}

// Guard against receiving messages from unknown nodes
_, known := e.eligibleNodeIDs[string(from)]
Copy link
Collaborator

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).

epoch.go Outdated Show resolved Hide resolved
Comment on lines +972 to +975
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++
}
Copy link
Collaborator

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)

Copy link
Collaborator Author

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 {
Copy link
Collaborator

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:

  1. The notarization is for a real block
  2. That we haven't voted for the dummy block yet

Copy link
Collaborator Author

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 {
Copy link
Collaborator

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)

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

yacovm and others added 8 commits December 27, 2024 16:14
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
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]>
@yacovm yacovm force-pushed the epoch branch 2 times, most recently from 0b6b612 to a3c59c3 Compare December 30, 2024 22:54
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]>
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]>
…ethod of Epoch

In order to make it more testable

Signed-off-by: Yacov Manevich <[email protected]>
Comment on lines 8 to +14
type Message struct {
BlockMessage BlockMessage
BlockMessage *BlockMessage
VoteMessage *Vote
Notarization *Notarization
Finalization *Finalization
FinalizationCertificate *FinalizationCertificate
}
Copy link
Collaborator

@StephenButtolph StephenButtolph Jan 2, 2025

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 includes Block which is an interface
  • Notarization and FinalizationCertificate include QuorumCertificate which is an interface.

Is it expected for the networking layer that integrates with the simplex code to handle the parsing of Blocks and QuorumCertificates?

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)

Copy link
Collaborator Author

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?

Copy link
Collaborator

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)

Copy link
Collaborator Author

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.

Copy link
Collaborator

@StephenButtolph StephenButtolph Jan 3, 2025

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
Copy link
Collaborator

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).

Copy link
Collaborator Author

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.

@yacovm yacovm merged commit 2df077b into main Jan 3, 2025
1 check passed
@yacovm yacovm deleted the epoch branch January 3, 2025 17:56
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.

3 participants