Conversation
4aa113e to
ef4c05f
Compare
msm/fake_node_test.go
Outdated
| } | ||
| require.Greater(t, node.Epoch(), uint64(1)) | ||
|
|
||
| return |
There was a problem hiding this comment.
oops, artifact from debugging 🤦♂️
Fixed.
| type outerBlock struct { | ||
| finalization *simplex.Finalization | ||
| block metadata.StateMachineBlock | ||
| } | ||
|
|
||
| type blockStore map[uint64]*outerBlock |
There was a problem hiding this comment.
can we store helper structs like this in their own files? alongside functions like newStateMachine()?
it's hard to review since im bouncing around these large files trying to separate/review the helpers from the tests
| func (bs blockStore) clone() blockStore { | ||
| newStore := make(blockStore) | ||
| for k, v := range bs { | ||
| newStore[k] = v | ||
| } | ||
| return newStore | ||
| } |
There was a problem hiding this comment.
| func (bs blockStore) clone() blockStore { | |
| newStore := make(blockStore) | |
| for k, v := range bs { | |
| newStore[k] = v | |
| } | |
| return newStore | |
| } | |
| func (bs blockStore) clone() blockStore { | |
| newStore := make(blockStore) | |
| maps.Copy(newStore, bs) | |
| return newStore | |
| } |
| return sm.wrapBlock(parentBlock, childBlock, simplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil | ||
| } | ||
|
|
||
| func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, simplexEpochInfo SimplexEpochInfo, newApprovals *approvals, pChainHeight uint64) (*StateMachineBlock, error) { |
| }) | ||
| } | ||
|
|
||
| func (nbms NodeBLSMappings) ForEach(selector func(int, NodeBLSMapping)) { |
There was a problem hiding this comment.
why can't we just do
for i, nbm := range NodeBLSMappings {
}| // GetPChainHeight returns the latest known P-chain height. | ||
| GetPChainHeight func() uint64 | ||
| // GetUpgrades returns the current upgrade configuration. | ||
| GetUpgrades func() UpgradeConfig |
There was a problem hiding this comment.
im confused by this function. The only time we set it is in a test, and it is set to a noop. If it is not necessary now, can we remove it from this pr?
There was a problem hiding this comment.
It's needed by ICM epochs
| return | ||
| } | ||
| if selector(i, nbm) { | ||
| total, err = safeAdd(total, nbm.Weight) |
There was a problem hiding this comment.
why not just return the error right away?
this function feels so ai generated 😅 so much unnecessary complexity for a simple for loop + sum counter. Unless i am missing something?
|
|
||
| type ValidatorSetApprovals []ValidatorSetApproval | ||
|
|
||
| func (vsa ValidatorSetApprovals) ForEach(f func(int, ValidatorSetApproval)) { |
| } | ||
| }) | ||
| return result | ||
| } No newline at end of file |
There was a problem hiding this comment.
needs a new line at the end
| func (nbms NodeBLSMappings) Equal(other NodeBLSMappings) bool { | ||
| if len(nbms) != len(other) { | ||
| return false | ||
| } | ||
|
|
||
| nbmsClone := nbms.Clone() | ||
| otherClone := other.Clone() | ||
|
|
||
| slices.SortFunc(nbmsClone, func(a, b NodeBLSMapping) int { | ||
| return slices.Compare(a.NodeID[:], b.NodeID[:]) | ||
| }) | ||
|
|
||
| slices.SortFunc(otherClone, func(a, b NodeBLSMapping) int { | ||
| return slices.Compare(a.NodeID[:], b.NodeID[:]) | ||
| }) | ||
|
|
||
| for i := range nbmsClone { | ||
| if !nbmsClone[i].Equals(&otherClone[i]) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } |
There was a problem hiding this comment.
| func (nbms NodeBLSMappings) Equal(other NodeBLSMappings) bool { | |
| if len(nbms) != len(other) { | |
| return false | |
| } | |
| nbmsClone := nbms.Clone() | |
| otherClone := other.Clone() | |
| slices.SortFunc(nbmsClone, func(a, b NodeBLSMapping) int { | |
| return slices.Compare(a.NodeID[:], b.NodeID[:]) | |
| }) | |
| slices.SortFunc(otherClone, func(a, b NodeBLSMapping) int { | |
| return slices.Compare(a.NodeID[:], b.NodeID[:]) | |
| }) | |
| for i := range nbmsClone { | |
| if !nbmsClone[i].Equals(&otherClone[i]) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func (nbms NodeBLSMappings) Equal(other NodeBLSMappings) bool { | |
| if len(nbms) != len(other) { | |
| return false | |
| } | |
| sortByNodeID := func(a, b NodeBLSMapping) int { | |
| return slices.Compare(a.NodeID[:], b.NodeID[:]) | |
| } | |
| nbmsClone := nbms.Clone() | |
| otherClone := other.Clone() | |
| slices.SortFunc(nbmsClone, sortByNodeID) | |
| slices.SortFunc(otherClone, sortByNodeID) | |
| return slices.EqualFunc(nbmsClone, otherClone, func(a, b NodeBLSMapping) bool { | |
| return a.Equals(&b) | |
| }) | |
| } |
also why don't we just keep these sets sorted? potentially we could require GetValidatorSet to return a sorted array, or create a wrapper like GetSortedValidatorSet which would save us a bunch of slices.SortFunc() calls as equal can be called multiple times
There was a problem hiding this comment.
also we could just store the NodeBLSMappings as a map and then assert equality in linear time rather than sorting both functions and then iterating(O(nlogn) * 2 + n) ?
| canotoData canotoData_ValidatorSetApproval | ||
| } | ||
|
|
||
| type ValidatorSetApprovals []ValidatorSetApproval |
There was a problem hiding this comment.
why is this an array and not a map with the nodeIDs being keys? seems like UniqueByNodeID would benefit from this.
There was a problem hiding this comment.
It's not unique. A node may send several approvals, for different payloads.
That's why we filter them:
newApprovals = newApprovals.Filter(func(i int, approval ValidatorSetApproval) bool {
// Pick only approvals that agree with our candidate auxiliary info digest and P-Chain height
return approval.PChainHeight == pChainHeight && approval.AuxInfoSeqDigest == candidateAuxInfoDigest
})
| - The genesis block does not contain the Simplex epoch information, and it is implicitly set to be the zero values of the fields above. | ||
|
|
||
|
|
||
| ### Auxiliary information encoding |
There was a problem hiding this comment.
alongside separating out the ICM stuff from this PR, I think we should also remove the auxillary information to reduce the diff of this PR. thoughts?
| func (fn *fakeNode) tryFinalizeNextBlock() { | ||
| nextIndex := len(fn.finalizedBlocks) | ||
|
|
||
| if fn.isNextBlockTelock() { | ||
| return | ||
| } | ||
|
|
||
| block := fn.notarizedBlocks[nextIndex] |
There was a problem hiding this comment.
it seems like we can some index out of bounds exceptions/nil panics if notarizedBlocks & finalizedBlocks are out of sync. For example this test panics
func TestTryFinalize(t *testing.T) {
fn := newFakeNode(t)
fn.tryFinalizeNextBlock()
}There was a problem hiding this comment.
is there any benefit to having the block in two places? can we have something like
type blockState struct {
block metadata.StateMachineBlock
finalized bool
}and then remove the two arrays and just have a singular array in the node state?
| Prev [32]byte | ||
| } | ||
|
|
||
| type fakeNode struct { |
There was a problem hiding this comment.
im not a fan of the name fakeNode, it is too generic and non-descriptive as it doesn't really tell me much about what the node actually does, i.e. the part being faked.
we already have BasicNode, LongRunningNode, ControlledNode. Maybe MultiEpochNode?
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestFakeNode(t *testing.T) { |
There was a problem hiding this comment.
this test was initially hard to follow. TestFakeNode makes it seem like we are trying to test the fake node implementation, even though we don't really care about testing the fake node implementation. Maybe TestStateMachineEpochTransition is better?
| notarizedBlocks []metadata.StateMachineBlock | ||
| finalizedBlocks []metadata.StateMachineBlock |
There was a problem hiding this comment.
should we consider consolidating these into a single data structure?
| sm.maybeInit() | ||
|
|
||
| // The zero sequence number is reserved for the genesis block, which should never be built. | ||
| if simplexMetadata.Seq == 0 { |
There was a problem hiding this comment.
should we add a test for this case?
| return block.InnerBlock.Verify(ctx) | ||
| } | ||
|
|
||
| func (sm *StateMachine) identifyCurrentState(prevBlockSimplexEpochInfo SimplexEpochInfo) (state, error) { |
There was a problem hiding this comment.
| func (sm *StateMachine) identifyCurrentState(prevBlockSimplexEpochInfo SimplexEpochInfo) (state, error) { | |
| func epochState(epochInfo SimplexEpochInfo) (state, error) { |
i dont think this function should be associated with StateMachine.
| return block.InnerBlock.Verify(ctx) | ||
| } | ||
|
|
||
| func (sm *StateMachine) identifyCurrentState(prevBlockSimplexEpochInfo SimplexEpochInfo) (state, error) { |
There was a problem hiding this comment.
I think this suggestion is even better than the previous. Also we don't need to return error regardless
| func (sm *StateMachine) identifyCurrentState(prevBlockSimplexEpochInfo SimplexEpochInfo) (state, error) { | |
| func (i *SimplexEpochInfo) CurrentState() state { |
we should also probably write tests for this function
| } | ||
|
|
||
| func computePrevVMBlockSeq(parentBlock StateMachineBlock, prevBlockSeq uint64) uint64 { | ||
| // Either our parent block has no inner block, in which case we just inherit its previous VM block sequence, |
There was a problem hiding this comment.
| // Either our parent block has no inner block, in which case we just inherit its previous VM block sequence, | |
| // Either our parent block has no inner block, in which case we just inherit its previous VM block sequence. |
| case stateFirstSimplexBlock: | ||
| return sm.buildBlockZero(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes) | ||
| case stateBuildBlockNormalOp: | ||
| return sm.buildBlockNormalOp(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) | ||
| case stateBuildCollectingApprovals: | ||
| return sm.buildBlockCollectingApprovals(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) | ||
| case stateBuildBlockEpochSealed: | ||
| return sm.buildBlockEpochSealed(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) |
There was a problem hiding this comment.
if we pass in the simplexMetadata as simplex.Metadata instead of bytes, we can remove prevBlockSeq from all the function headers and just use simplexMetadata.Seq - 1. Then when we need the bytes, we should just call simplexMetadata.Bytes()
This commit adds an implementation of the Metadata State Machine (MSM). It intercepts block building and verification from the Simplex instance, and performs or verifies metadata state transitions according to the logic defined in the README.md. Contains: - MSM block building and verification logic - Epoch transition (sealing blocks, Telocks, approval collection) - Block encoding with canoto - Unit and integration tests (fake node test and full epoch lifecycle test) Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
Signed-off-by: Yacov Manevich <yacov.manevich@avalabs.org>
No description provided.