diff --git a/go.mod b/go.mod index a1145162..da791ef9 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,20 @@ module github.com/ava-labs/simplex -go 1.23.0 +go 1.25.0 require ( - github.com/stretchr/testify v1.9.0 + github.com/StephenButtolph/canoto v0.19.0 + github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.26.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.10.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 56d9d664..bf0e562e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,25 @@ +github.com/StephenButtolph/canoto v0.19.0 h1:a28jijQ4gyaSW920h69nX1E/o9WilHlx6L9PVuLkB/Y= +github.com/StephenButtolph/canoto v0.19.0/go.mod h1:01RsiQp1gnV1eJ6LwygP6buPCLUoAz7jKadQSB0FI0o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= +github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thepudds/fzgen v0.4.3 h1:srUP/34BulQaEwPP/uHZkdjUcUjIzL7Jkf4CBVryiP8= +github.com/thepudds/fzgen v0.4.3/go.mod h1:BhhwtRhzgvLWAjjcHDJ9pEiLD2Z9hrVIFjBCHJ//zJ4= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= diff --git a/msm/README.md b/msm/README.md index 898891f0..25a81d65 100644 --- a/msm/README.md +++ b/msm/README.md @@ -86,7 +86,7 @@ message SimplexEpochInfo { - The validator set of the epoch numbered `epoch_number` is derived from `p_chain_reference_height`. - The `prev_sealing_block_hash` is the hash of the sealing block of the previous epoch, and it is used to efficiently validate the sealing block of the previous epoch. - If there is no previous epoch (i.e., the current epoch is the first ever epoch), then it is nil. + If there is no previous epoch (i.e., the current epoch is the first ever epoch), then is equal to the hash of the first Simplex block. - The `next_p_chain_reference_height` is the P-chain height of the next epoch, otherwise it is set to `0`. @@ -378,35 +378,30 @@ ____________________ | ```proto -message SimplexBlock { +message OuterBlock { bytes inner_block = 1; // The inner block built by the VM, opaque to Simplex. - OuterBlock outer_block = 2; // The outer block that wraps the inner block without the inner block. - bytes protocol_metadata = 3 // The Simplex protocol metadata, set by the Simplex consensus protocol. + StateMachineMetadata metadata = 2; // The the metadata of the block. } ``` -where `OuterBlock` is a protobuf message that contains the ICM epoch information, the Simplex epoch information, and auxiliary information: +where `StateMachineMetadata` is a protobuf message that contains the ICM epoch information, the Simplex epoch information, and auxiliary information: ```proto -message OuterBlock { +message StateMachineMetadata { ICMEpochInfo icm_epoch_info = 1; // The ICM epoch information. SimplexEpochInfo simplex_epoch_info = 2; // The Simplex epoch information. - AuxiliaryInfo auxiliary_info = 3; // The auxiliary information. + bytes protocol_metadata = 3; // The Simplex protocol metadata, set by the Simplex consensus protocol. + bytes blacklist = 4; // The blacklist of the Simplex protocol. + AuxiliaryInfo auxiliary_info = 5; // The auxiliary information. + uint64 p_chain_height = 6; // The P-chain height sampled when building the block. + uint64 timestamp = 7; // The timestamp of the block, set by the block builder. } ``` The digest of the simplex block is computed as follows: -Let $h_i$ be the hash of the inner block. -The digest of the Simplex block is the hash of the following encoding: - -```proto -message HashPreImage { - bytes h_i = 1; // The inner block hash - OuterBlock outer_block = 2; - bytes protocol_metadata = 3; -} -``` +Let $h_i$ be the hash of the inner block and $h_m$ be the hash of the metadata. +The digest of the Simplex block is the hash of the following encoding: `h_i || h_m` where `||` denotes concatenation. This way of hashing the block allows any holder of a finalization certificate for the block to authenticate the block while hiding the content of the inner block. @@ -426,6 +421,7 @@ The Simplex epoch information is a canoto encoded message with the following sch message NodeBLSMapping { bytes node_id = 1; // The nodeID bytes bls_key = 2; // The BLS key of the node + uint64 weight = 3; // The weight of the node in the validator set, used for quorum calculations. } message BlockValidationDescriptor { @@ -453,5 +449,6 @@ message SimplexEpochInfo { uint64 prev_vm_block_seq = 5; // The sequence of the previous VM block BlockValidationDescriptor block_validation_descriptor = 6; // Describes how to validate the blocks of the next epoch NextEpochApprovals next_epoch_approvals = 7; // The epoch change approvals of the next epoch by at least n-f nodes. + uint64 sealing_block_seq = 8; // The sequence number of the sealing block of the current epoch. } ``` diff --git a/msm/block_type.go b/msm/block_type.go new file mode 100644 index 00000000..4d55d533 --- /dev/null +++ b/msm/block_type.go @@ -0,0 +1,85 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import "fmt" + +// BlockType classifies a Simplex block by the role it plays in its epoch: +// a normal block in the middle of an epoch, a Telock (a pre-sealing block +// that carries no inner VM block), a sealing block that finalizes the +// current epoch, or the first block of a new epoch. +type BlockType uint8 + +const ( + BlockTypeNormal BlockType = iota + 1 + BlockTypeTelock + BlockTypeSealing + BlockTypeNewEpoch +) + +func (t BlockType) String() string { + switch t { + case BlockTypeNormal: + return "Normal" + case BlockTypeTelock: + return "Telock" + case BlockTypeSealing: + return "Sealing" + case BlockTypeNewEpoch: + return "NewEpoch" + default: + return fmt.Sprintf("UnknownBlockType(%d)", t) + } +} + +// IdentifyBlockType classifies a proposed block relative to its parent by +// inspecting the epoch information and the parent's sequence number. +func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachineMetadata, prevSeq uint64) BlockType { + simplexEpochInfo := nextBlockMD.SimplexEpochInfo + prevSimplexEpochInfo := prevBlockMD.SimplexEpochInfo + + // Only sealing blocks carry block validation descriptors + if nextBlockMD.SimplexEpochInfo.BlockValidationDescriptor != nil { + return BlockTypeSealing + } + + // This block could be in the edges of an epoch, either at the end or at the beginning. + + // If the new block comes after a sealing block, it could be a Telock or the first block of the next epoch. + // [ Sealing Block ] <-- [ New Block ] + if prevSimplexEpochInfo.BlockValidationDescriptor != nil { + // The zero-epoch block has BlockValidationDescriptor but epoch number 1 and next P-chain reference height of 0, + // so the block following it is a normal block, not a Telock. + if prevSimplexEpochInfo.EpochNumber == 1 && prevSimplexEpochInfo.NextPChainReferenceHeight == 0 { + return BlockTypeNormal + } + + if simplexEpochInfo.EpochNumber == prevSeq { + // If the epoch number of the new block is the same as the previous block's sequence number, + // it means we have just transitioned to a new epoch as the previous block was a sealing block. + return BlockTypeNewEpoch + } + + // Otherwise, we haven't transitioned to a new epoch yet, so this block has to be a Telock, + // as after a sealing block we either have a Telock or the first block of the new epoch, + // and we have already ruled out the first block of the new epoch in the previous condition. + return BlockTypeTelock + } + + // Else, if the previous block has a sealing block sequence and is in the same epoch as this block, + // then this block has to be a Telock, as the sealing block sequence indicates that the sealing block has been created. + // [ Sealing Block ] <-- [ Prev block ] <-- [ New Block ] + if simplexEpochInfo.EpochNumber == prevSimplexEpochInfo.EpochNumber && prevSimplexEpochInfo.SealingBlockSeq != 0 { + return BlockTypeTelock + } + + // This block is the first block of its epoch if the epoch number is the sealing block sequence of the previous epoch + if simplexEpochInfo.EpochNumber == prevSimplexEpochInfo.SealingBlockSeq { + return BlockTypeNewEpoch + } + + // Otherwise, we do not fall into any of these cases, so it's a block in the middle of the epoch, + // not in the edges. + return BlockTypeNormal +} diff --git a/msm/build_decision.go b/msm/build_decision.go new file mode 100644 index 00000000..d962a75f --- /dev/null +++ b/msm/build_decision.go @@ -0,0 +1,191 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "sync" + "time" + + "go.uber.org/zap" +) + +// blockBuildingDecision represents the decision of whether we should build a block at the current time, +// and if so, whether we should also transition to a new epoch along the way. +type blockBuildingDecision int8 + +const ( + blockBuildingDecisionUndefined blockBuildingDecision = iota + blockBuildingDecisionBuildBlock // We should build a block, and we don't need to transition to a new epoch. + blockBuildingDecisionTransitionEpoch // We should transition to a new epoch immediately, but we don't need to build a block. + blockBuildingDecisionBuildBlockAndTransitionEpoch // We should build a block and transition to a new epoch along the way. + blockBuildingDecisionContextCanceled +) + +func (bbd blockBuildingDecision) String() string { + switch bbd { + case blockBuildingDecisionUndefined: + return "undefined" + case blockBuildingDecisionBuildBlock: + return "build block" + case blockBuildingDecisionTransitionEpoch: + return "transition epoch" + case blockBuildingDecisionBuildBlockAndTransitionEpoch: + return "build block and transition epoch" + case blockBuildingDecisionContextCanceled: + return "context canceled" + default: + return "unknown" + } +} + +// PChainProgressListener listens for changes in the P-chain height. +type PChainProgressListener interface { + // WaitForProgress should block until either the context is cancelled, or the P-chain height has increased from the provided pChainHeight. + WaitForProgress(ctx context.Context, pChainHeight uint64) error +} + +type blockBuildingDecider struct { + logger Logger + maxBlockBuildingWaitTime time.Duration + pChainlistener PChainProgressListener + waitForPendingBlock func(ctx context.Context) + shouldTransitionEpoch func(pChainHeight uint64) (bool, error) + getPChainHeight func() uint64 +} + +// newBlockBuildingDecider wires a blockBuildingDecider around a StateMachine +// and the parent block it is extending. A fresh decider is created per call +// because shouldTransitionEpoch closes over the parent block. +func newBlockBuildingDecider(sm *StateMachine, parentBlock StateMachineBlock) blockBuildingDecider { + return blockBuildingDecider{ + logger: sm.Logger, + maxBlockBuildingWaitTime: sm.MaxBlockBuildingWaitTime, + pChainlistener: sm.PChainProgressListener, + getPChainHeight: sm.GetPChainHeight, + waitForPendingBlock: sm.BlockBuilder.WaitForPendingBlock, + shouldTransitionEpoch: func(pChainHeight uint64) (bool, error) { + // The given pChainHeight was sampled by the caller of shouldTransitionEpoch(). + // We compare between the current validator set, defined by the P-chain reference height in the parent block, + // and the new validator set defined by the given pChainHeight. + // If they are different, then we should transition to a new epoch. + currentValidatorSet, err := sm.GetValidatorSet(parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight) + if err != nil { + return false, err + } + + newValidatorSet, err := sm.GetValidatorSet(pChainHeight) + if err != nil { + return false, err + } + + if !currentValidatorSet.Equal(newValidatorSet) { + return true, nil + } + return false, nil + }, + } +} + +// shouldBuildBlock determines whether we should build a block at the current time, +// based on the current P-chain height and whether we should transition to a new epoch. +// It returns a blockBuildingDecision, the current P-chain height sampled at the time of deciding, +// and an error if the decision cannot be made. +// The P-chain height is returned because sampling the P-chain height afterwards might be inconsistent with the decision that was made. +func (bbd *blockBuildingDecider) shouldBuildBlock( + ctx context.Context, +) (blockBuildingDecision, uint64, error) { + for { + pChainHeight := bbd.getPChainHeight() + + shouldTransitionEpoch, err := bbd.shouldTransitionEpoch(pChainHeight) + if err != nil { + return blockBuildingDecisionUndefined, 0, err + } + + if shouldTransitionEpoch { + // If we should transition to a new epoch, maybe we can also build a block along the way. + return bbd.maybeBuildBlockWithEpochTransition(ctx), pChainHeight, nil + } + + // Else, we don't need to transition to a new epoch, but maybe we should build a block. + // We wait for either the P-chain height to change, or for a block to be ready to be built. + bbd.waitForPChainChangeOrPendingBlock(ctx, pChainHeight) + + // If the context was cancelled in the meantime, abandon evaluation. + if bbd.wasContextCanceled(ctx) { + return blockBuildingDecisionContextCanceled, 0, nil + } + + // If we've reached here, either the P-chain height has changed, or a block is ready to be built. + + // If the P-chain height changed, re-evaluate again whether we should transition to a new epoch, + // or continue waiting to build a block. + if bbd.getPChainHeight() != pChainHeight { + continue + } + + // Else, we have reached here because a block is ready to be built, and the P-chain height has not changed, + // which means we should build a block. + + return blockBuildingDecisionBuildBlock, pChainHeight, nil + } +} + +// waitForPChainChangeOrPendingBlock waits until either the given P-chain height changes from the provided pChainHeight, +// or a block is ready to be built. +func (bbd *blockBuildingDecider) waitForPChainChangeOrPendingBlock(ctx context.Context, pChainHeight uint64) { + pChainAwareContext, cancel := context.WithCancel(ctx) + + var wg sync.WaitGroup + wg.Add(1) + + defer wg.Wait() + defer cancel() + + go func() { + defer wg.Done() + err := bbd.pChainlistener.WaitForProgress(pChainAwareContext, pChainHeight) + if err != nil && pChainAwareContext.Err() == nil { + bbd.logger.Warn("error while waiting for P-chain progress", zap.Error(err)) + } + cancel() + }() + + bbd.waitForPendingBlock(pChainAwareContext) +} + +// maybeBuildBlockWithEpochTransition decides if we should build a block while transitioning to a new epoch. +// It waits up to a limited amount of time (bbd.maxBlockBuildingWaitTime) for a block to be ready to be built, +// and if no block is ready by then, it returns the decision to transition epoch without building a block. +// Otherwise, it returns the decision to build a block and transition epoch along the way. +func (bbd *blockBuildingDecider) maybeBuildBlockWithEpochTransition(ctx context.Context) blockBuildingDecision { + impatientContext, cancel := context.WithTimeout(ctx, bbd.maxBlockBuildingWaitTime) + defer cancel() + + // We should transition to a new epoch, so we wait some time just in case we can also build a block along the way. + // waitForPendingBlock will return in case a block is ready to be built, or when the context times out. + bbd.waitForPendingBlock(impatientContext) + + if impatientContext.Err() != nil { + // Check if we have returned because the parent context was cancelled + if bbd.wasContextCanceled(ctx) { + return blockBuildingDecisionContextCanceled + } + // We have returned from waitForPendingBlock because the context has timed out, which means we don't need to build a block. + return blockBuildingDecisionTransitionEpoch + } + + // Block is ready to be built + return blockBuildingDecisionBuildBlockAndTransitionEpoch +} + +func (bbd *blockBuildingDecider) wasContextCanceled(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} diff --git a/msm/build_decision_test.go b/msm/build_decision_test.go new file mode 100644 index 00000000..d6dc8e8e --- /dev/null +++ b/msm/build_decision_test.go @@ -0,0 +1,203 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type fakePChainListener struct { + onListen func(ctx context.Context, pChainHeight uint64) +} + +func (f *fakePChainListener) WaitForProgress(ctx context.Context, pChainHeight uint64) error { + f.onListen(ctx, pChainHeight) + return nil // We don't do anything with the error but log it, so it's fine to always return nil here. +} + +func TestShouldBuildBlock_VMSignalsBlock(t *testing.T) { + // No epoch transition needed, VM signals a block is ready. + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: time.Second, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, _ uint64) { + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) {}, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, + getPChainHeight: func() uint64 { return 100 }, + } + + result, pChainHeight, err := bbd.shouldBuildBlock(t.Context()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionBuildBlock, result) + require.Equal(t, uint64(100), pChainHeight) +} + +func TestShouldBuildBlock_ContextCanceled(t *testing.T) { + // No epoch transition, parent context is cancelled while waiting. + ctx, cancel := context.WithCancel(t.Context()) + + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: time.Second, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, _ uint64) { + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) { + cancel() + <-ctx.Done() + }, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, + getPChainHeight: func() uint64 { return 100 }, + } + + result, _, err := bbd.shouldBuildBlock(ctx) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionContextCanceled, result) +} + +func TestShouldBuildBlock_PChainHeightChangeTriggersEpochTransition(t *testing.T) { + // First iteration: no epoch transition, P-chain listener fires (height changes). + // Second iteration: shouldTransitionEpoch returns true, VM doesn't signal a block before timeout. + var pChainHeight atomic.Uint64 + pChainHeight.Store(100) + + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: 10 * time.Millisecond, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, height uint64) { + // On the first call, simulate a P-chain height change. + if height == 100 { + pChainHeight.Store(200) + return + } + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) { + <-ctx.Done() + }, + shouldTransitionEpoch: func(height uint64) (bool, error) { + return height == 200, nil + }, + getPChainHeight: func() uint64 { return pChainHeight.Load() }, + } + + result, resultPChainHeight, err := bbd.shouldBuildBlock(t.Context()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionTransitionEpoch, result) + require.Equal(t, uint64(200), resultPChainHeight) +} + +func TestShouldBuildBlock_PChainHeightChangeButNoEpochTransition(t *testing.T) { + // P-chain height changes on first iteration but shouldTransitionEpoch stays false. + // On second iteration, VM signals a block. + var pChainHeight atomic.Uint64 + pChainHeight.Store(100) + + var iteration atomic.Int32 + + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: time.Second, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, height uint64) { + if height == 100 { + pChainHeight.Store(200) + return + } + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) { + // First iteration: block on context (P-chain listener will cancel it). + // Second iteration: return immediately (VM has a block). + if iteration.Add(1) == 1 { + <-ctx.Done() + return + } + }, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, + getPChainHeight: func() uint64 { return pChainHeight.Load() }, + } + + result, resultPChainHeight, err := bbd.shouldBuildBlock(t.Context()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionBuildBlock, result) + require.Equal(t, uint64(200), resultPChainHeight) +} + +func TestShouldBuildBlock_EpochTransitionWithVMBlock(t *testing.T) { + // Epoch transition needed, but VM signals a block before the timeout. + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: time.Second, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, _ uint64) { + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) {}, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, + getPChainHeight: func() uint64 { return 100 }, + } + + result, pChainHeight, err := bbd.shouldBuildBlock(t.Context()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionBuildBlockAndTransitionEpoch, result) + require.Equal(t, uint64(100), pChainHeight) +} + +func TestShouldBuildBlock_EpochTransitionWithoutVMBlock(t *testing.T) { + // Epoch transition needed, VM doesn't signal a block before timeout. + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: 10 * time.Millisecond, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, _ uint64) { + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) { + <-ctx.Done() + }, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, + getPChainHeight: func() uint64 { return 100 }, + } + + result, pChainHeight, err := bbd.shouldBuildBlock(t.Context()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionTransitionEpoch, result) + require.Equal(t, uint64(100), pChainHeight) +} + +func TestShouldBuildBlock_EpochTransitionContextCanceled(t *testing.T) { + // Epoch transition needed, but parent context is cancelled during the wait. + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + bbd := &blockBuildingDecider{ + maxBlockBuildingWaitTime: time.Second, + pChainlistener: &fakePChainListener{ + onListen: func(ctx context.Context, _ uint64) { + <-ctx.Done() + }, + }, + waitForPendingBlock: func(ctx context.Context) { + cancel() + <-ctx.Done() + }, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, + getPChainHeight: func() uint64 { return 100 }, + } + + result, _, err := bbd.shouldBuildBlock(ctx) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionContextCanceled, result) +} diff --git a/msm/encoding.canoto.go b/msm/encoding.canoto.go new file mode 100644 index 00000000..9dcb9541 --- /dev/null +++ b/msm/encoding.canoto.go @@ -0,0 +1,2373 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.19.0 +// source: encoding.go + +package metadata + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that the generated code is compatible with the library version. +const ( + _ uint = canoto.VersionCompatibility - 1 + _ uint = 1 - canoto.VersionCompatibility +) + +// Ensure that unused imports do not error +var _ = io.ErrUnexpectedEOF + +const ( + canotoNumber_OuterBlock__InnerBlock = 1 + canotoNumber_OuterBlock__Metadata = 2 + + canotoTag_OuterBlock__InnerBlock = "\x0a" // canoto.Tag(canotoNumber_OuterBlock__InnerBlock, canoto.Len) + canotoTag_OuterBlock__Metadata = "\x12" // canoto.Tag(canotoNumber_OuterBlock__Metadata, canoto.Len) +) + +type canotoData_OuterBlock struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*OuterBlock) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[OuterBlock]()) + var zero OuterBlock + s := &canoto.Spec{ + Name: "OuterBlock", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_OuterBlock__InnerBlock, + Name: "InnerBlock", + OneOf: "", + TypeBytes: true, + }, + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.Metadata), + /*FieldNumber: */ canotoNumber_OuterBlock__Metadata, + /*Name: */ "Metadata", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *OuterBlock) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *OuterBlock) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = OuterBlock{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_OuterBlock__InnerBlock: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.InnerBlock); err != nil { + return err + } + if len(c.InnerBlock) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_OuterBlock__Metadata: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + if err := (&c.Metadata).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *OuterBlock) ValidCanoto() bool { + if !(&c.Metadata).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *OuterBlock) CalculateCanotoCache() { + var size uint64 + if len(c.InnerBlock) != 0 { + size += uint64(len(canotoTag_OuterBlock__InnerBlock)) + canoto.SizeBytes(c.InnerBlock) + } + (&c.Metadata).CalculateCanotoCache() + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_OuterBlock__Metadata)) + canoto.SizeUint(fieldSize) + fieldSize + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *OuterBlock) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *OuterBlock) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *OuterBlock) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if len(c.InnerBlock) != 0 { + canoto.Append(&w, canotoTag_OuterBlock__InnerBlock) + canoto.AppendBytes(&w, c.InnerBlock) + } + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_OuterBlock__Metadata) + canoto.AppendUint(&w, fieldSize) + w = (&c.Metadata).MarshalCanotoInto(w) + } + return w +} + +const ( + canotoNumber_StateMachineMetadata__SimplexEpochInfo = 1 + canotoNumber_StateMachineMetadata__SimplexProtocolMetadata = 2 + canotoNumber_StateMachineMetadata__SimplexBlacklist = 3 + canotoNumber_StateMachineMetadata__PChainHeight = 4 + canotoNumber_StateMachineMetadata__Timestamp = 5 + canotoNumber_StateMachineMetadata__AuxiliaryInfo = 6 + canotoNumber_StateMachineMetadata__ICMEpochInfo = 7 + + canotoTag_StateMachineMetadata__SimplexEpochInfo = "\x0a" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexEpochInfo, canoto.Len) + canotoTag_StateMachineMetadata__SimplexProtocolMetadata = "\x12" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexProtocolMetadata, canoto.Len) + canotoTag_StateMachineMetadata__SimplexBlacklist = "\x1a" // canoto.Tag(canotoNumber_StateMachineMetadata__SimplexBlacklist, canoto.Len) + canotoTag_StateMachineMetadata__PChainHeight = "\x20" // canoto.Tag(canotoNumber_StateMachineMetadata__PChainHeight, canoto.Varint) + canotoTag_StateMachineMetadata__Timestamp = "\x28" // canoto.Tag(canotoNumber_StateMachineMetadata__Timestamp, canoto.Varint) + canotoTag_StateMachineMetadata__AuxiliaryInfo = "\x32" // canoto.Tag(canotoNumber_StateMachineMetadata__AuxiliaryInfo, canoto.Len) + canotoTag_StateMachineMetadata__ICMEpochInfo = "\x3a" // canoto.Tag(canotoNumber_StateMachineMetadata__ICMEpochInfo, canoto.Len) +) + +type canotoData_StateMachineMetadata struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*StateMachineMetadata) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[StateMachineMetadata]()) + var zero StateMachineMetadata + s := &canoto.Spec{ + Name: "StateMachineMetadata", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.SimplexEpochInfo), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__SimplexEpochInfo, + /*Name: */ "SimplexEpochInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + { + FieldNumber: canotoNumber_StateMachineMetadata__SimplexProtocolMetadata, + Name: "SimplexProtocolMetadata", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canotoNumber_StateMachineMetadata__SimplexBlacklist, + Name: "SimplexBlacklist", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canotoNumber_StateMachineMetadata__PChainHeight, + Name: "PChainHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainHeight), + }, + { + FieldNumber: canotoNumber_StateMachineMetadata__Timestamp, + Name: "Timestamp", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Timestamp), + }, + canoto.FieldTypeFromField( + /*type inference:*/ (zero.AuxiliaryInfo), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__AuxiliaryInfo, + /*Name: */ "AuxiliaryInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ true, + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.ICMEpochInfo), + /*FieldNumber: */ canotoNumber_StateMachineMetadata__ICMEpochInfo, + /*Name: */ "ICMEpochInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *StateMachineMetadata) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *StateMachineMetadata) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = StateMachineMetadata{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_StateMachineMetadata__SimplexEpochInfo: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + if err := (&c.SimplexEpochInfo).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case canotoNumber_StateMachineMetadata__SimplexProtocolMetadata: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.SimplexProtocolMetadata); err != nil { + return err + } + if len(c.SimplexProtocolMetadata) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_StateMachineMetadata__SimplexBlacklist: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.SimplexBlacklist); err != nil { + return err + } + if len(c.SimplexBlacklist) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_StateMachineMetadata__PChainHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PChainHeight); err != nil { + return err + } + if canoto.IsZero(c.PChainHeight) { + return canoto.ErrZeroValue + } + case canotoNumber_StateMachineMetadata__Timestamp: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Timestamp); err != nil { + return err + } + if canoto.IsZero(c.Timestamp) { + return canoto.ErrZeroValue + } + case canotoNumber_StateMachineMetadata__AuxiliaryInfo: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + c.AuxiliaryInfo = canoto.MakePointer(c.AuxiliaryInfo) + if err := (c.AuxiliaryInfo).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case canotoNumber_StateMachineMetadata__ICMEpochInfo: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + if err := (&c.ICMEpochInfo).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *StateMachineMetadata) ValidCanoto() bool { + if !(&c.SimplexEpochInfo).ValidCanoto() { + return false + } + if c.AuxiliaryInfo != nil && !(c.AuxiliaryInfo).ValidCanoto() { + return false + } + if !(&c.ICMEpochInfo).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *StateMachineMetadata) CalculateCanotoCache() { + var size uint64 + (&c.SimplexEpochInfo).CalculateCanotoCache() + if fieldSize := (&c.SimplexEpochInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__SimplexEpochInfo)) + canoto.SizeUint(fieldSize) + fieldSize + } + if len(c.SimplexProtocolMetadata) != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__SimplexProtocolMetadata)) + canoto.SizeBytes(c.SimplexProtocolMetadata) + } + if len(c.SimplexBlacklist) != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__SimplexBlacklist)) + canoto.SizeBytes(c.SimplexBlacklist) + } + if !canoto.IsZero(c.PChainHeight) { + size += uint64(len(canotoTag_StateMachineMetadata__PChainHeight)) + canoto.SizeUint(c.PChainHeight) + } + if !canoto.IsZero(c.Timestamp) { + size += uint64(len(canotoTag_StateMachineMetadata__Timestamp)) + canoto.SizeUint(c.Timestamp) + } + if c.AuxiliaryInfo != nil { + (c.AuxiliaryInfo).CalculateCanotoCache() + fieldSize := (c.AuxiliaryInfo).CachedCanotoSize() + size += uint64(len(canotoTag_StateMachineMetadata__AuxiliaryInfo)) + canoto.SizeUint(fieldSize) + fieldSize + } + (&c.ICMEpochInfo).CalculateCanotoCache() + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_StateMachineMetadata__ICMEpochInfo)) + canoto.SizeUint(fieldSize) + fieldSize + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *StateMachineMetadata) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *StateMachineMetadata) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *StateMachineMetadata) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if fieldSize := (&c.SimplexEpochInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_StateMachineMetadata__SimplexEpochInfo) + canoto.AppendUint(&w, fieldSize) + w = (&c.SimplexEpochInfo).MarshalCanotoInto(w) + } + if len(c.SimplexProtocolMetadata) != 0 { + canoto.Append(&w, canotoTag_StateMachineMetadata__SimplexProtocolMetadata) + canoto.AppendBytes(&w, c.SimplexProtocolMetadata) + } + if len(c.SimplexBlacklist) != 0 { + canoto.Append(&w, canotoTag_StateMachineMetadata__SimplexBlacklist) + canoto.AppendBytes(&w, c.SimplexBlacklist) + } + if !canoto.IsZero(c.PChainHeight) { + canoto.Append(&w, canotoTag_StateMachineMetadata__PChainHeight) + canoto.AppendUint(&w, c.PChainHeight) + } + if !canoto.IsZero(c.Timestamp) { + canoto.Append(&w, canotoTag_StateMachineMetadata__Timestamp) + canoto.AppendUint(&w, c.Timestamp) + } + if c.AuxiliaryInfo != nil { + fieldSize := (c.AuxiliaryInfo).CachedCanotoSize() + canoto.Append(&w, canotoTag_StateMachineMetadata__AuxiliaryInfo) + canoto.AppendUint(&w, fieldSize) + w = (c.AuxiliaryInfo).MarshalCanotoInto(w) + } + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_StateMachineMetadata__ICMEpochInfo) + canoto.AppendUint(&w, fieldSize) + w = (&c.ICMEpochInfo).MarshalCanotoInto(w) + } + return w +} + +const ( + canotoNumber_ICMEpochInfo__EpochStartTime = 1 + canotoNumber_ICMEpochInfo__EpochNumber = 2 + canotoNumber_ICMEpochInfo__PChainEpochHeight = 3 + + canotoTag_ICMEpochInfo__EpochStartTime = "\x08" // canoto.Tag(canotoNumber_ICMEpochInfo__EpochStartTime, canoto.Varint) + canotoTag_ICMEpochInfo__EpochNumber = "\x10" // canoto.Tag(canotoNumber_ICMEpochInfo__EpochNumber, canoto.Varint) + canotoTag_ICMEpochInfo__PChainEpochHeight = "\x18" // canoto.Tag(canotoNumber_ICMEpochInfo__PChainEpochHeight, canoto.Varint) +) + +type canotoData_ICMEpochInfo struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ICMEpochInfo) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero ICMEpochInfo + s := &canoto.Spec{ + Name: "ICMEpochInfo", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_ICMEpochInfo__EpochStartTime, + Name: "EpochStartTime", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochStartTime), + }, + { + FieldNumber: canotoNumber_ICMEpochInfo__EpochNumber, + Name: "EpochNumber", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochNumber), + }, + { + FieldNumber: canotoNumber_ICMEpochInfo__PChainEpochHeight, + Name: "PChainEpochHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainEpochHeight), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ICMEpochInfo) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *ICMEpochInfo) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ICMEpochInfo{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_ICMEpochInfo__EpochStartTime: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.EpochStartTime); err != nil { + return err + } + if canoto.IsZero(c.EpochStartTime) { + return canoto.ErrZeroValue + } + case canotoNumber_ICMEpochInfo__EpochNumber: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.EpochNumber); err != nil { + return err + } + if canoto.IsZero(c.EpochNumber) { + return canoto.ErrZeroValue + } + case canotoNumber_ICMEpochInfo__PChainEpochHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PChainEpochHeight); err != nil { + return err + } + if canoto.IsZero(c.PChainEpochHeight) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *ICMEpochInfo) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.EpochStartTime) { + size += uint64(len(canotoTag_ICMEpochInfo__EpochStartTime)) + canoto.SizeUint(c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + size += uint64(len(canotoTag_ICMEpochInfo__EpochNumber)) + canoto.SizeUint(c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + size += uint64(len(canotoTag_ICMEpochInfo__PChainEpochHeight)) + canoto.SizeUint(c.PChainEpochHeight) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *ICMEpochInfo) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ICMEpochInfo) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.EpochStartTime) { + canoto.Append(&w, canotoTag_ICMEpochInfo__EpochStartTime) + canoto.AppendUint(&w, c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + canoto.Append(&w, canotoTag_ICMEpochInfo__EpochNumber) + canoto.AppendUint(&w, c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + canoto.Append(&w, canotoTag_ICMEpochInfo__PChainEpochHeight) + canoto.AppendUint(&w, c.PChainEpochHeight) + } + return w +} + +const ( + canotoNumber_AuxiliaryInfo__Info = 1 + canotoNumber_AuxiliaryInfo__PrevAuxInfoSeq = 2 + canotoNumber_AuxiliaryInfo__ApplicationID = 3 + + canotoTag_AuxiliaryInfo__Info = "\x0a" // canoto.Tag(canotoNumber_AuxiliaryInfo__Info, canoto.Len) + canotoTag_AuxiliaryInfo__PrevAuxInfoSeq = "\x10" // canoto.Tag(canotoNumber_AuxiliaryInfo__PrevAuxInfoSeq, canoto.Varint) + canotoTag_AuxiliaryInfo__ApplicationID = "\x18" // canoto.Tag(canotoNumber_AuxiliaryInfo__ApplicationID, canoto.Varint) +) + +type canotoData_AuxiliaryInfo struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*AuxiliaryInfo) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero AuxiliaryInfo + s := &canoto.Spec{ + Name: "AuxiliaryInfo", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_AuxiliaryInfo__Info, + Name: "Info", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canotoNumber_AuxiliaryInfo__PrevAuxInfoSeq, + Name: "PrevAuxInfoSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PrevAuxInfoSeq), + }, + { + FieldNumber: canotoNumber_AuxiliaryInfo__ApplicationID, + Name: "ApplicationID", + OneOf: "", + TypeUint: canoto.SizeOf(zero.ApplicationID), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *AuxiliaryInfo) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *AuxiliaryInfo) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = AuxiliaryInfo{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_AuxiliaryInfo__Info: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.Info); err != nil { + return err + } + if len(c.Info) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_AuxiliaryInfo__PrevAuxInfoSeq: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PrevAuxInfoSeq); err != nil { + return err + } + if canoto.IsZero(c.PrevAuxInfoSeq) { + return canoto.ErrZeroValue + } + case canotoNumber_AuxiliaryInfo__ApplicationID: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.ApplicationID); err != nil { + return err + } + if canoto.IsZero(c.ApplicationID) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *AuxiliaryInfo) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *AuxiliaryInfo) CalculateCanotoCache() { + var size uint64 + if len(c.Info) != 0 { + size += uint64(len(canotoTag_AuxiliaryInfo__Info)) + canoto.SizeBytes(c.Info) + } + if !canoto.IsZero(c.PrevAuxInfoSeq) { + size += uint64(len(canotoTag_AuxiliaryInfo__PrevAuxInfoSeq)) + canoto.SizeUint(c.PrevAuxInfoSeq) + } + if !canoto.IsZero(c.ApplicationID) { + size += uint64(len(canotoTag_AuxiliaryInfo__ApplicationID)) + canoto.SizeUint(c.ApplicationID) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *AuxiliaryInfo) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *AuxiliaryInfo) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *AuxiliaryInfo) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if len(c.Info) != 0 { + canoto.Append(&w, canotoTag_AuxiliaryInfo__Info) + canoto.AppendBytes(&w, c.Info) + } + if !canoto.IsZero(c.PrevAuxInfoSeq) { + canoto.Append(&w, canotoTag_AuxiliaryInfo__PrevAuxInfoSeq) + canoto.AppendUint(&w, c.PrevAuxInfoSeq) + } + if !canoto.IsZero(c.ApplicationID) { + canoto.Append(&w, canotoTag_AuxiliaryInfo__ApplicationID) + canoto.AppendUint(&w, c.ApplicationID) + } + return w +} + +const ( + canotoNumber_SimplexEpochInfo__PChainReferenceHeight = 1 + canotoNumber_SimplexEpochInfo__EpochNumber = 2 + canotoNumber_SimplexEpochInfo__PrevSealingBlockHash = 3 + canotoNumber_SimplexEpochInfo__NextPChainReferenceHeight = 4 + canotoNumber_SimplexEpochInfo__PrevVMBlockSeq = 5 + canotoNumber_SimplexEpochInfo__BlockValidationDescriptor = 6 + canotoNumber_SimplexEpochInfo__NextEpochApprovals = 7 + canotoNumber_SimplexEpochInfo__SealingBlockSeq = 8 + + canotoTag_SimplexEpochInfo__PChainReferenceHeight = "\x08" // canoto.Tag(canotoNumber_SimplexEpochInfo__PChainReferenceHeight, canoto.Varint) + canotoTag_SimplexEpochInfo__EpochNumber = "\x10" // canoto.Tag(canotoNumber_SimplexEpochInfo__EpochNumber, canoto.Varint) + canotoTag_SimplexEpochInfo__PrevSealingBlockHash = "\x1a" // canoto.Tag(canotoNumber_SimplexEpochInfo__PrevSealingBlockHash, canoto.Len) + canotoTag_SimplexEpochInfo__NextPChainReferenceHeight = "\x20" // canoto.Tag(canotoNumber_SimplexEpochInfo__NextPChainReferenceHeight, canoto.Varint) + canotoTag_SimplexEpochInfo__PrevVMBlockSeq = "\x28" // canoto.Tag(canotoNumber_SimplexEpochInfo__PrevVMBlockSeq, canoto.Varint) + canotoTag_SimplexEpochInfo__BlockValidationDescriptor = "\x32" // canoto.Tag(canotoNumber_SimplexEpochInfo__BlockValidationDescriptor, canoto.Len) + canotoTag_SimplexEpochInfo__NextEpochApprovals = "\x3a" // canoto.Tag(canotoNumber_SimplexEpochInfo__NextEpochApprovals, canoto.Len) + canotoTag_SimplexEpochInfo__SealingBlockSeq = "\x40" // canoto.Tag(canotoNumber_SimplexEpochInfo__SealingBlockSeq, canoto.Varint) +) + +type canotoData_SimplexEpochInfo struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*SimplexEpochInfo) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[SimplexEpochInfo]()) + var zero SimplexEpochInfo + s := &canoto.Spec{ + Name: "SimplexEpochInfo", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_SimplexEpochInfo__PChainReferenceHeight, + Name: "PChainReferenceHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainReferenceHeight), + }, + { + FieldNumber: canotoNumber_SimplexEpochInfo__EpochNumber, + Name: "EpochNumber", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochNumber), + }, + { + FieldNumber: canotoNumber_SimplexEpochInfo__PrevSealingBlockHash, + Name: "PrevSealingBlockHash", + OneOf: "", + TypeFixedBytes: uint64(len(zero.PrevSealingBlockHash)), + }, + { + FieldNumber: canotoNumber_SimplexEpochInfo__NextPChainReferenceHeight, + Name: "NextPChainReferenceHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NextPChainReferenceHeight), + }, + { + FieldNumber: canotoNumber_SimplexEpochInfo__PrevVMBlockSeq, + Name: "PrevVMBlockSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PrevVMBlockSeq), + }, + canoto.FieldTypeFromField( + /*type inference:*/ (zero.BlockValidationDescriptor), + /*FieldNumber: */ canotoNumber_SimplexEpochInfo__BlockValidationDescriptor, + /*Name: */ "BlockValidationDescriptor", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ true, + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (zero.NextEpochApprovals), + /*FieldNumber: */ canotoNumber_SimplexEpochInfo__NextEpochApprovals, + /*Name: */ "NextEpochApprovals", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ true, + /*types: */ types, + ), + { + FieldNumber: canotoNumber_SimplexEpochInfo__SealingBlockSeq, + Name: "SealingBlockSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.SealingBlockSeq), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *SimplexEpochInfo) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *SimplexEpochInfo) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = SimplexEpochInfo{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_SimplexEpochInfo__PChainReferenceHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PChainReferenceHeight); err != nil { + return err + } + if canoto.IsZero(c.PChainReferenceHeight) { + return canoto.ErrZeroValue + } + case canotoNumber_SimplexEpochInfo__EpochNumber: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.EpochNumber); err != nil { + return err + } + if canoto.IsZero(c.EpochNumber) { + return canoto.ErrZeroValue + } + case canotoNumber_SimplexEpochInfo__PrevSealingBlockHash: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.PrevSealingBlockHash) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.PrevSealingBlockHash)[:], r.B) + if canoto.IsZero(c.PrevSealingBlockHash) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case canotoNumber_SimplexEpochInfo__NextPChainReferenceHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NextPChainReferenceHeight); err != nil { + return err + } + if canoto.IsZero(c.NextPChainReferenceHeight) { + return canoto.ErrZeroValue + } + case canotoNumber_SimplexEpochInfo__PrevVMBlockSeq: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PrevVMBlockSeq); err != nil { + return err + } + if canoto.IsZero(c.PrevVMBlockSeq) { + return canoto.ErrZeroValue + } + case canotoNumber_SimplexEpochInfo__BlockValidationDescriptor: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + c.BlockValidationDescriptor = canoto.MakePointer(c.BlockValidationDescriptor) + if err := (c.BlockValidationDescriptor).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case canotoNumber_SimplexEpochInfo__NextEpochApprovals: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + c.NextEpochApprovals = canoto.MakePointer(c.NextEpochApprovals) + if err := (c.NextEpochApprovals).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case canotoNumber_SimplexEpochInfo__SealingBlockSeq: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.SealingBlockSeq); err != nil { + return err + } + if canoto.IsZero(c.SealingBlockSeq) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *SimplexEpochInfo) ValidCanoto() bool { + if c.BlockValidationDescriptor != nil && !(c.BlockValidationDescriptor).ValidCanoto() { + return false + } + if c.NextEpochApprovals != nil && !(c.NextEpochApprovals).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *SimplexEpochInfo) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.PChainReferenceHeight) { + size += uint64(len(canotoTag_SimplexEpochInfo__PChainReferenceHeight)) + canoto.SizeUint(c.PChainReferenceHeight) + } + if !canoto.IsZero(c.EpochNumber) { + size += uint64(len(canotoTag_SimplexEpochInfo__EpochNumber)) + canoto.SizeUint(c.EpochNumber) + } + if !canoto.IsZero(c.PrevSealingBlockHash) { + size += uint64(len(canotoTag_SimplexEpochInfo__PrevSealingBlockHash)) + canoto.SizeBytes((&c.PrevSealingBlockHash)[:]) + } + if !canoto.IsZero(c.NextPChainReferenceHeight) { + size += uint64(len(canotoTag_SimplexEpochInfo__NextPChainReferenceHeight)) + canoto.SizeUint(c.NextPChainReferenceHeight) + } + if !canoto.IsZero(c.PrevVMBlockSeq) { + size += uint64(len(canotoTag_SimplexEpochInfo__PrevVMBlockSeq)) + canoto.SizeUint(c.PrevVMBlockSeq) + } + if c.BlockValidationDescriptor != nil { + (c.BlockValidationDescriptor).CalculateCanotoCache() + fieldSize := (c.BlockValidationDescriptor).CachedCanotoSize() + size += uint64(len(canotoTag_SimplexEpochInfo__BlockValidationDescriptor)) + canoto.SizeUint(fieldSize) + fieldSize + } + if c.NextEpochApprovals != nil { + (c.NextEpochApprovals).CalculateCanotoCache() + fieldSize := (c.NextEpochApprovals).CachedCanotoSize() + size += uint64(len(canotoTag_SimplexEpochInfo__NextEpochApprovals)) + canoto.SizeUint(fieldSize) + fieldSize + } + if !canoto.IsZero(c.SealingBlockSeq) { + size += uint64(len(canotoTag_SimplexEpochInfo__SealingBlockSeq)) + canoto.SizeUint(c.SealingBlockSeq) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *SimplexEpochInfo) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *SimplexEpochInfo) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *SimplexEpochInfo) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.PChainReferenceHeight) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__PChainReferenceHeight) + canoto.AppendUint(&w, c.PChainReferenceHeight) + } + if !canoto.IsZero(c.EpochNumber) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__EpochNumber) + canoto.AppendUint(&w, c.EpochNumber) + } + if !canoto.IsZero(c.PrevSealingBlockHash) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__PrevSealingBlockHash) + canoto.AppendBytes(&w, (&c.PrevSealingBlockHash)[:]) + } + if !canoto.IsZero(c.NextPChainReferenceHeight) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__NextPChainReferenceHeight) + canoto.AppendUint(&w, c.NextPChainReferenceHeight) + } + if !canoto.IsZero(c.PrevVMBlockSeq) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__PrevVMBlockSeq) + canoto.AppendUint(&w, c.PrevVMBlockSeq) + } + if c.BlockValidationDescriptor != nil { + fieldSize := (c.BlockValidationDescriptor).CachedCanotoSize() + canoto.Append(&w, canotoTag_SimplexEpochInfo__BlockValidationDescriptor) + canoto.AppendUint(&w, fieldSize) + w = (c.BlockValidationDescriptor).MarshalCanotoInto(w) + } + if c.NextEpochApprovals != nil { + fieldSize := (c.NextEpochApprovals).CachedCanotoSize() + canoto.Append(&w, canotoTag_SimplexEpochInfo__NextEpochApprovals) + canoto.AppendUint(&w, fieldSize) + w = (c.NextEpochApprovals).MarshalCanotoInto(w) + } + if !canoto.IsZero(c.SealingBlockSeq) { + canoto.Append(&w, canotoTag_SimplexEpochInfo__SealingBlockSeq) + canoto.AppendUint(&w, c.SealingBlockSeq) + } + return w +} + +const ( + canotoNumber_NodeBLSMapping__NodeID = 1 + canotoNumber_NodeBLSMapping__BLSKey = 2 + canotoNumber_NodeBLSMapping__Weight = 3 + + canotoTag_NodeBLSMapping__NodeID = "\x0a" // canoto.Tag(canotoNumber_NodeBLSMapping__NodeID, canoto.Len) + canotoTag_NodeBLSMapping__BLSKey = "\x12" // canoto.Tag(canotoNumber_NodeBLSMapping__BLSKey, canoto.Len) + canotoTag_NodeBLSMapping__Weight = "\x18" // canoto.Tag(canotoNumber_NodeBLSMapping__Weight, canoto.Varint) +) + +type canotoData_NodeBLSMapping struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*NodeBLSMapping) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero NodeBLSMapping + s := &canoto.Spec{ + Name: "NodeBLSMapping", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_NodeBLSMapping__NodeID, + Name: "NodeID", + OneOf: "", + TypeFixedBytes: uint64(len(zero.NodeID)), + }, + { + FieldNumber: canotoNumber_NodeBLSMapping__BLSKey, + Name: "BLSKey", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canotoNumber_NodeBLSMapping__Weight, + Name: "Weight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Weight), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *NodeBLSMapping) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *NodeBLSMapping) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = NodeBLSMapping{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_NodeBLSMapping__NodeID: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.NodeID) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.NodeID)[:], r.B) + if canoto.IsZero(c.NodeID) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case canotoNumber_NodeBLSMapping__BLSKey: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.BLSKey); err != nil { + return err + } + if len(c.BLSKey) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_NodeBLSMapping__Weight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Weight); err != nil { + return err + } + if canoto.IsZero(c.Weight) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *NodeBLSMapping) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *NodeBLSMapping) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.NodeID) { + size += uint64(len(canotoTag_NodeBLSMapping__NodeID)) + canoto.SizeBytes((&c.NodeID)[:]) + } + if len(c.BLSKey) != 0 { + size += uint64(len(canotoTag_NodeBLSMapping__BLSKey)) + canoto.SizeBytes(c.BLSKey) + } + if !canoto.IsZero(c.Weight) { + size += uint64(len(canotoTag_NodeBLSMapping__Weight)) + canoto.SizeUint(c.Weight) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *NodeBLSMapping) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *NodeBLSMapping) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *NodeBLSMapping) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.NodeID) { + canoto.Append(&w, canotoTag_NodeBLSMapping__NodeID) + canoto.AppendBytes(&w, (&c.NodeID)[:]) + } + if len(c.BLSKey) != 0 { + canoto.Append(&w, canotoTag_NodeBLSMapping__BLSKey) + canoto.AppendBytes(&w, c.BLSKey) + } + if !canoto.IsZero(c.Weight) { + canoto.Append(&w, canotoTag_NodeBLSMapping__Weight) + canoto.AppendUint(&w, c.Weight) + } + return w +} + +const ( + canotoNumber_BlockValidationDescriptor__AggregatedMembership = 1 + + canotoTag_BlockValidationDescriptor__AggregatedMembership = "\x0a" // canoto.Tag(canotoNumber_BlockValidationDescriptor__AggregatedMembership, canoto.Len) +) + +type canotoData_BlockValidationDescriptor struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*BlockValidationDescriptor) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[BlockValidationDescriptor]()) + var zero BlockValidationDescriptor + s := &canoto.Spec{ + Name: "BlockValidationDescriptor", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.AggregatedMembership), + /*FieldNumber: */ canotoNumber_BlockValidationDescriptor__AggregatedMembership, + /*Name: */ "AggregatedMembership", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *BlockValidationDescriptor) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *BlockValidationDescriptor) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = BlockValidationDescriptor{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_BlockValidationDescriptor__AggregatedMembership: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + if err := (&c.AggregatedMembership).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *BlockValidationDescriptor) ValidCanoto() bool { + if !(&c.AggregatedMembership).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *BlockValidationDescriptor) CalculateCanotoCache() { + var size uint64 + (&c.AggregatedMembership).CalculateCanotoCache() + if fieldSize := (&c.AggregatedMembership).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canotoTag_BlockValidationDescriptor__AggregatedMembership)) + canoto.SizeUint(fieldSize) + fieldSize + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *BlockValidationDescriptor) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *BlockValidationDescriptor) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *BlockValidationDescriptor) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if fieldSize := (&c.AggregatedMembership).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canotoTag_BlockValidationDescriptor__AggregatedMembership) + canoto.AppendUint(&w, fieldSize) + w = (&c.AggregatedMembership).MarshalCanotoInto(w) + } + return w +} + +const ( + canotoNumber_AggregatedMembership__Members = 1 + + canotoTag_AggregatedMembership__Members = "\x0a" // canoto.Tag(canotoNumber_AggregatedMembership__Members, canoto.Len) +) + +type canotoData_AggregatedMembership struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*AggregatedMembership) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeFor[AggregatedMembership]()) + var zero AggregatedMembership + s := &canoto.Spec{ + Name: "AggregatedMembership", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.Members)), + /*FieldNumber: */ canotoNumber_AggregatedMembership__Members, + /*Name: */ "Members", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*Pointer: */ false, + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *AggregatedMembership) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *AggregatedMembership) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = AggregatedMembership{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_AggregatedMembership__Members: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canotoTag_AggregatedMembership__Members) + if err != nil { + return err + } + + c.Members = canoto.MakeSlice(c.Members, countMinus1+1) + field := c.Members + additionalField := field[1:] + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + if err := (&field[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range additionalField { + r.B = r.B[len(canotoTag_AggregatedMembership__Members):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + if len(msgBytes) == 0 { + continue + } + + remainingBytes := r.B + r.B = msgBytes + if err := (&additionalField[i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *AggregatedMembership) ValidCanoto() bool { + { + field := c.Members + for i := range field { + if !(&field[i]).ValidCanoto() { + return false + } + } + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *AggregatedMembership) CalculateCanotoCache() { + var size uint64 + { + field := c.Members + for i := range field { + (&field[i]).CalculateCanotoCache() + fieldSize := (&field[i]).CachedCanotoSize() + size += uint64(len(canotoTag_AggregatedMembership__Members)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *AggregatedMembership) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *AggregatedMembership) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *AggregatedMembership) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + { + field := c.Members + for i := range field { + canoto.Append(&w, canotoTag_AggregatedMembership__Members) + canoto.AppendUint(&w, (&field[i]).CachedCanotoSize()) + w = (&field[i]).MarshalCanotoInto(w) + } + } + return w +} + +const ( + canotoNumber_NextEpochApprovals__NodeIDs = 1 + canotoNumber_NextEpochApprovals__Signature = 2 + + canotoTag_NextEpochApprovals__NodeIDs = "\x0a" // canoto.Tag(canotoNumber_NextEpochApprovals__NodeIDs, canoto.Len) + canotoTag_NextEpochApprovals__Signature = "\x12" // canoto.Tag(canotoNumber_NextEpochApprovals__Signature, canoto.Len) +) + +type canotoData_NextEpochApprovals struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*NextEpochApprovals) CanotoSpec(...reflect.Type) *canoto.Spec { + s := &canoto.Spec{ + Name: "NextEpochApprovals", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_NextEpochApprovals__NodeIDs, + Name: "NodeIDs", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canotoNumber_NextEpochApprovals__Signature, + Name: "Signature", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *NextEpochApprovals) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *NextEpochApprovals) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = NextEpochApprovals{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_NextEpochApprovals__NodeIDs: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.NodeIDs); err != nil { + return err + } + if len(c.NodeIDs) == 0 { + return canoto.ErrZeroValue + } + case canotoNumber_NextEpochApprovals__Signature: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.Signature); err != nil { + return err + } + if len(c.Signature) == 0 { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *NextEpochApprovals) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *NextEpochApprovals) CalculateCanotoCache() { + var size uint64 + if len(c.NodeIDs) != 0 { + size += uint64(len(canotoTag_NextEpochApprovals__NodeIDs)) + canoto.SizeBytes(c.NodeIDs) + } + if len(c.Signature) != 0 { + size += uint64(len(canotoTag_NextEpochApprovals__Signature)) + canoto.SizeBytes(c.Signature) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *NextEpochApprovals) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *NextEpochApprovals) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *NextEpochApprovals) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if len(c.NodeIDs) != 0 { + canoto.Append(&w, canotoTag_NextEpochApprovals__NodeIDs) + canoto.AppendBytes(&w, c.NodeIDs) + } + if len(c.Signature) != 0 { + canoto.Append(&w, canotoTag_NextEpochApprovals__Signature) + canoto.AppendBytes(&w, c.Signature) + } + return w +} + +const ( + canotoNumber_ValidatorSetApproval__NodeID = 1 + canotoNumber_ValidatorSetApproval__AuxInfoSeqDigest = 2 + canotoNumber_ValidatorSetApproval__PChainHeight = 3 + canotoNumber_ValidatorSetApproval__Signature = 4 + + canotoTag_ValidatorSetApproval__NodeID = "\x0a" // canoto.Tag(canotoNumber_ValidatorSetApproval__NodeID, canoto.Len) + canotoTag_ValidatorSetApproval__AuxInfoSeqDigest = "\x12" // canoto.Tag(canotoNumber_ValidatorSetApproval__AuxInfoSeqDigest, canoto.Len) + canotoTag_ValidatorSetApproval__PChainHeight = "\x18" // canoto.Tag(canotoNumber_ValidatorSetApproval__PChainHeight, canoto.Varint) + canotoTag_ValidatorSetApproval__Signature = "\x22" // canoto.Tag(canotoNumber_ValidatorSetApproval__Signature, canoto.Len) +) + +type canotoData_ValidatorSetApproval struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ValidatorSetApproval) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero ValidatorSetApproval + s := &canoto.Spec{ + Name: "ValidatorSetApproval", + Fields: []canoto.FieldType{ + { + FieldNumber: canotoNumber_ValidatorSetApproval__NodeID, + Name: "NodeID", + OneOf: "", + TypeFixedBytes: uint64(len(zero.NodeID)), + }, + { + FieldNumber: canotoNumber_ValidatorSetApproval__AuxInfoSeqDigest, + Name: "AuxInfoSeqDigest", + OneOf: "", + TypeFixedBytes: uint64(len(zero.AuxInfoSeqDigest)), + }, + { + FieldNumber: canotoNumber_ValidatorSetApproval__PChainHeight, + Name: "PChainHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainHeight), + }, + { + FieldNumber: canotoNumber_ValidatorSetApproval__Signature, + Name: "Signature", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ValidatorSetApproval) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *ValidatorSetApproval) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ValidatorSetApproval{} + atomic.StoreUint64(&c.canotoData.size, uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case canotoNumber_ValidatorSetApproval__NodeID: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.NodeID) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.NodeID)[:], r.B) + if canoto.IsZero(c.NodeID) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case canotoNumber_ValidatorSetApproval__AuxInfoSeqDigest: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.AuxInfoSeqDigest) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.AuxInfoSeqDigest)[:], r.B) + if canoto.IsZero(c.AuxInfoSeqDigest) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case canotoNumber_ValidatorSetApproval__PChainHeight: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.PChainHeight); err != nil { + return err + } + if canoto.IsZero(c.PChainHeight) { + return canoto.ErrZeroValue + } + case canotoNumber_ValidatorSetApproval__Signature: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.Signature); err != nil { + return err + } + if len(c.Signature) == 0 { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *ValidatorSetApproval) ValidCanoto() bool { + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +// +// It is not safe to copy this struct concurrently. +func (c *ValidatorSetApproval) CalculateCanotoCache() { + var size uint64 + if !canoto.IsZero(c.NodeID) { + size += uint64(len(canotoTag_ValidatorSetApproval__NodeID)) + canoto.SizeBytes((&c.NodeID)[:]) + } + if !canoto.IsZero(c.AuxInfoSeqDigest) { + size += uint64(len(canotoTag_ValidatorSetApproval__AuxInfoSeqDigest)) + canoto.SizeBytes((&c.AuxInfoSeqDigest)[:]) + } + if !canoto.IsZero(c.PChainHeight) { + size += uint64(len(canotoTag_ValidatorSetApproval__PChainHeight)) + canoto.SizeUint(c.PChainHeight) + } + if len(c.Signature) != 0 { + size += uint64(len(canotoTag_ValidatorSetApproval__Signature)) + canoto.SizeBytes(c.Signature) + } + atomic.StoreUint64(&c.canotoData.size, size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *ValidatorSetApproval) CachedCanotoSize() uint64 { + return atomic.LoadUint64(&c.canotoData.size) +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ValidatorSetApproval) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +// +// It is not safe to copy this struct concurrently. +func (c *ValidatorSetApproval) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if !canoto.IsZero(c.NodeID) { + canoto.Append(&w, canotoTag_ValidatorSetApproval__NodeID) + canoto.AppendBytes(&w, (&c.NodeID)[:]) + } + if !canoto.IsZero(c.AuxInfoSeqDigest) { + canoto.Append(&w, canotoTag_ValidatorSetApproval__AuxInfoSeqDigest) + canoto.AppendBytes(&w, (&c.AuxInfoSeqDigest)[:]) + } + if !canoto.IsZero(c.PChainHeight) { + canoto.Append(&w, canotoTag_ValidatorSetApproval__PChainHeight) + canoto.AppendUint(&w, c.PChainHeight) + } + if len(c.Signature) != 0 { + canoto.Append(&w, canotoTag_ValidatorSetApproval__Signature) + canoto.AppendBytes(&w, c.Signature) + } + return w +} diff --git a/msm/encoding.go b/msm/encoding.go new file mode 100644 index 00000000..0f2ec601 --- /dev/null +++ b/msm/encoding.go @@ -0,0 +1,343 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "slices" +) + +// go:generate go run github.com/StephenButtolph/canoto/canoto encoding.go + +// OuterBlock is the top-level encoding of a Simplex block. +// It contains the inner block (the block built by the VM), +// as well as metadata created by the StateMachine. +type OuterBlock struct { + // InnerBlock is the block created by the VM, encoded as bytes and opaque to the StateMachine. + InnerBlock []byte `canoto:"bytes,1"` + // Metadata is created by the StateMachine. + Metadata StateMachineMetadata `canoto:"value,2"` + + canotoData canotoData_OuterBlock +} + +// StateMachineMetadata defines the metadata that the StateMachine uses to transition between epochs, +// and maintain ICM epoch information. +// TODO: change SimplexProtocolMetadata and SimplexBlacklist to be non-opaque types. +// TODO: This requires to encode the protocol metadata and blacklist using canoto. +type StateMachineMetadata struct { + // SimplexEpochInfo is the metadata that the StateMachine uses for its own epoching. + SimplexEpochInfo SimplexEpochInfo `canoto:"value,1"` + // SimplexProtocolMetadata is the metadata that Simplex uses for its protocol, such as sequence and round number. + SimplexProtocolMetadata []byte `canoto:"bytes,2"` + // SimplexBlacklist is the metadata that Simplex uses to keep track of blacklisted nodes. + // Blacklisted nodes do not become leaders. + SimplexBlacklist []byte `canoto:"bytes,3"` + // PChainHeight is the P-Chain height that the StateMachine sampled at the time of building the block. + // It's used for ICM epoching, not for Simplex epoching. + // For Simplex epoching, the P-Chain height that matters is the PChainReferenceHeight in the SimplexEpochInfo. + PChainHeight uint64 `canoto:"uint,4"` + // Timestamp is the time when the block is being built, in milliseconds since Unix epoch. + Timestamp uint64 `canoto:"uint,5"` + // AuxiliaryInfo is application-specific information that the StateMachine doesn't need to understand, + // but can be used by applications that care about epoch changes, such as threshold distributed public key generation. + AuxiliaryInfo *AuxiliaryInfo `canoto:"pointer,6"` + // ICMEpochInfo is the metadata that the StateMachine uses for ICM epoching. + ICMEpochInfo ICMEpochInfo `canoto:"value,7"` + + + canotoData canotoData_StateMachineMetadata +} + +// ICMEpochInfo is metadata used for the ICM protocol. +// The StateMachine maintains this metadata in a similar fashion to proposerVM. +type ICMEpochInfo struct { + EpochStartTime uint64 `canoto:"uint,1"` + EpochNumber uint64 `canoto:"uint,2"` + PChainEpochHeight uint64 `canoto:"uint,3"` + + canotoData canotoData_ICMEpochInfo +} + +func (ei *ICMEpochInfo) Equal(other *ICMEpochInfo) bool { + if ei == nil { + return other == nil + } + if other == nil { + return ei == nil + } + return ei.EpochStartTime == other.EpochStartTime && ei.EpochNumber == other.EpochNumber && ei.PChainEpochHeight == other.PChainEpochHeight +} + +// AppID is an identifier for applications that care about epoch changes. +type AppID uint32 + +// AuxiliaryInfo defines application-specific information for applications that might care about epoch change, +// such as threshold distributed public key generation. +type AuxiliaryInfo struct { + // Info is opaque bytes that can be used by applications to encode any information that describes + // the current state for the application. + Info []byte `canoto:"bytes,1"` + // PrevAuxInfoSeq is a sequence number that applications can use to find previous AuxiliaryInfo in the chain. + // It is zero if this is the first AuxiliaryInfo for this epoch. + PrevAuxInfoSeq uint64 `canoto:"uint,2"` + // ApplicationID is an identifier that identifies the application. + // Can be used for backward-compatibility and upgrade purposes. + ApplicationID AppID `canoto:"uint,3"` + + canotoData canotoData_AuxiliaryInfo +} + +func (ai *AuxiliaryInfo) IsZero() bool { + var zero AuxiliaryInfo + return ai.Equal(&zero) +} + +func (ai *AuxiliaryInfo) Equal(a *AuxiliaryInfo) bool { + if ai == nil { + return a == nil + } + if a == nil { + return ai == nil + } + return bytes.Equal(ai.Info, a.Info) && ai.PrevAuxInfoSeq == a.PrevAuxInfoSeq && ai.ApplicationID == a.ApplicationID +} + +// SimplexEpochInfo is metadata used by the StateMachine. +type SimplexEpochInfo struct { + // PChainReferenceHeight is the P-Chain height that the StateMachine uses as a reference for the current epoch. + // The validator set is determined based on the validators on the P-Chain at the PChainReferenceHeight. + PChainReferenceHeight uint64 `canoto:"uint,1"` + // EpochNumber is the current epoch number. + // The first epoch is numbered 1, and each successive epoch is numbered according to the block sequence + // of the sealing block of the previous epoch. + EpochNumber uint64 `canoto:"uint,2"` + // PrevSealingBlockHash is the hash of the sealing block of the previous epoch. + // It is empty for the first epoch, and the second epoch has the PrevSealingBlockHash set to be + // the hash of the first ever block built by the StateMachine. + PrevSealingBlockHash [32]byte `canoto:"fixed bytes,3"` + // NextPChainReferenceHeight is the P-Chain height that the StateMachine uses as a reference for the next epoch. + // When the NextPChainReferenceHeight is > 0, it means the StateMachine is on its way to transition to a new epoch + // in which the validator set will be based on the given P-chain height. + NextPChainReferenceHeight uint64 `canoto:"uint,4"` + // PrevVMBlockSeq is the block sequence of the previous block that has a VM block (inner block). + // This is used to know on which VM block to build the next block. + PrevVMBlockSeq uint64 `canoto:"uint,5"` + // BlockValidationDescriptor is the metadata that describes the validator set of the next epoch. + // It is only set in the sealing block, and nil in all other blocks. + BlockValidationDescriptor *BlockValidationDescriptor `canoto:"pointer,6"` + // NextEpochApprovals is the metadata that contains the approvals from validators for the next epoch. + // It is set only in the sealing block and the blocks preceding it starting from a block that has a NextPChainReferenceHeight set. + NextEpochApprovals *NextEpochApprovals `canoto:"pointer,7"` + // SealingBlockSeq is the block sequence of the sealing block of the current epoch. + // It defines the validator set of the next epoch. + SealingBlockSeq uint64 `canoto:"uint,8"` + + canotoData canotoData_SimplexEpochInfo +} + +func (sei *SimplexEpochInfo) IsZero() bool { + var zero SimplexEpochInfo + return sei.Equal(&zero) +} + +func (sei *SimplexEpochInfo) Equal(other *SimplexEpochInfo) bool { + if sei == nil { + return other == nil + } + if other == nil { + return false + } + if sei.BlockValidationDescriptor == nil && other.BlockValidationDescriptor != nil { + return false + } + if sei.BlockValidationDescriptor != nil && other.BlockValidationDescriptor == nil { + return false + } + if sei.NextEpochApprovals == nil && other.NextEpochApprovals != nil { + return false + } + if sei.NextEpochApprovals != nil && other.NextEpochApprovals == nil { + return false + } + + if sei.PChainReferenceHeight != other.PChainReferenceHeight || sei.EpochNumber != other.EpochNumber || + sei.NextPChainReferenceHeight != other.NextPChainReferenceHeight || + sei.PrevVMBlockSeq != other.PrevVMBlockSeq || sei.SealingBlockSeq != other.SealingBlockSeq { + return false + } + if !bytes.Equal(sei.PrevSealingBlockHash[:], other.PrevSealingBlockHash[:]) { + return false + } + if sei.BlockValidationDescriptor != nil && !sei.BlockValidationDescriptor.Equals(other.BlockValidationDescriptor) { + return false + } + if sei.NextEpochApprovals != nil && !sei.NextEpochApprovals.Equals(other.NextEpochApprovals) { + return false + } + return true +} + +type NodeBLSMapping struct { + NodeID nodeID `canoto:"fixed bytes,1"` + BLSKey []byte `canoto:"bytes,2"` + Weight uint64 `canoto:"uint,3"` + + canotoData canotoData_NodeBLSMapping +} + +func (nbm *NodeBLSMapping) Clone() NodeBLSMapping { + var cloned NodeBLSMapping + copy(cloned.NodeID[:], nbm.NodeID[:]) + cloned.BLSKey = make([]byte, len(nbm.BLSKey)) + copy(cloned.BLSKey, nbm.BLSKey) + cloned.Weight = nbm.Weight + return cloned +} + +func (nbm *NodeBLSMapping) Equals(other *NodeBLSMapping) bool { + if !slices.Equal(nbm.NodeID[:], other.NodeID[:]) { + return false + } + if !slices.Equal(nbm.BLSKey, other.BLSKey) { + return false + } + if nbm.Weight != other.Weight { + return false + } + return true +} + +type BlockValidationDescriptor struct { + AggregatedMembership AggregatedMembership `canoto:"value,1"` + + canotoData canotoData_BlockValidationDescriptor +} + +func (bvd *BlockValidationDescriptor) Equals(other *BlockValidationDescriptor) bool { + if bvd == nil && other == nil { + return true + } + if bvd == nil || other == nil { + return false + } + return bvd.AggregatedMembership.Equals(other.AggregatedMembership.Members) +} + +type AggregatedMembership struct { + Members []NodeBLSMapping `canoto:"repeated value,1"` + + canotoData canotoData_AggregatedMembership +} + +func (c *AggregatedMembership) Equals(members []NodeBLSMapping) bool { + if len(c.Members) != len(members) { + return false + } + + for i := range c.Members { + if !c.Members[i].Equals(&members[i]) { + return false + } + } + return true +} + +type NextEpochApprovals struct { + NodeIDs []byte `canoto:"bytes,1"` + Signature []byte `canoto:"bytes,2"` + + canotoData canotoData_NextEpochApprovals +} + +func (nea *NextEpochApprovals) Equals(other *NextEpochApprovals) bool { + if nea == nil && other == nil { + return true + } + if nea == nil || other == nil { + return false + } + if !bytes.Equal(nea.NodeIDs, other.NodeIDs) { + return false + } + if !bytes.Equal(nea.Signature, other.Signature) { + return false + } + return true +} + +type NodeBLSMappings []NodeBLSMapping + +func (nbms NodeBLSMappings) Clone() NodeBLSMappings { + cloned := make(NodeBLSMappings, len(nbms)) + for i, nbm := range nbms { + cloned[i] = nbm.Clone() + } + return cloned +} + +func (nbms NodeBLSMappings) TotalWeight() (uint64, error) { + var total uint64 + for _, nbm := range nbms { + sum, err := safeAdd(total, nbm.Weight) + if err != nil { + return 0, err + } + total = sum + } + return total, nil +} + +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) + }) +} + +type ValidatorSetApproval struct { + NodeID nodeID `canoto:"fixed bytes,1"` + AuxInfoSeqDigest [32]byte `canoto:"fixed bytes,2"` + PChainHeight uint64 `canoto:"uint,3"` + Signature []byte `canoto:"bytes,4"` + + canotoData canotoData_ValidatorSetApproval +} + +type ValidatorSetApprovals []ValidatorSetApproval + +func (vsa ValidatorSetApprovals) Filter(keep func(ValidatorSetApproval) bool) ValidatorSetApprovals { + result := make(ValidatorSetApprovals, 0, len(vsa)) + for _, v := range vsa { + if keep(v) { + result = append(result, v) + } + } + return result +} + +func (vsa ValidatorSetApprovals) UniqueByNodeID() ValidatorSetApprovals { + seen := make(map[nodeID]struct{}) + result := make(ValidatorSetApprovals, 0, len(vsa)) + for _, v := range vsa { + if _, exists := seen[v.NodeID]; exists { + continue + } + seen[v.NodeID] = struct{}{} + result = append(result, v) + } + return result +} diff --git a/msm/encoding_test.go b/msm/encoding_test.go new file mode 100644 index 00000000..6bafa430 --- /dev/null +++ b/msm/encoding_test.go @@ -0,0 +1,465 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSimplexEpochInfoIsZero(t *testing.T) { + require.True(t, (&SimplexEpochInfo{}).IsZero()) + require.False(t, (&SimplexEpochInfo{EpochNumber: 1}).IsZero()) + require.False(t, (&SimplexEpochInfo{PChainReferenceHeight: 1}).IsZero()) +} + +func TestSimplexEpochInfoEqual(t *testing.T) { + hash1 := [32]byte{1, 2, 3} + hash2 := [32]byte{4, 5, 6} + + tests := []struct { + name string + a *SimplexEpochInfo + b *SimplexEpochInfo + expected bool + }{ + { + name: "first nil second nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "first non-nil second nil", + a: &SimplexEpochInfo{}, + b: nil, + expected: false, + }, + { + name: "first nil second non-nil (via nil receiver)", + a: nil, + b: &SimplexEpochInfo{}, + expected: false, + }, + { + name: "both zero", + a: &SimplexEpochInfo{}, + b: &SimplexEpochInfo{}, + expected: true, + }, + { + name: "equal with all fields", + a: &SimplexEpochInfo{ + PChainReferenceHeight: 10, + EpochNumber: 2, + PrevSealingBlockHash: hash1, + NextPChainReferenceHeight: 20, + PrevVMBlockSeq: 5, + SealingBlockSeq: 15, + }, + b: &SimplexEpochInfo{ + PChainReferenceHeight: 10, + EpochNumber: 2, + PrevSealingBlockHash: hash1, + NextPChainReferenceHeight: 20, + PrevVMBlockSeq: 5, + SealingBlockSeq: 15, + }, + expected: true, + }, + { + name: "different PChainReferenceHeight", + a: &SimplexEpochInfo{PChainReferenceHeight: 1}, + b: &SimplexEpochInfo{PChainReferenceHeight: 2}, + expected: false, + }, + { + name: "different EpochNumber", + a: &SimplexEpochInfo{EpochNumber: 1}, + b: &SimplexEpochInfo{EpochNumber: 2}, + expected: false, + }, + { + name: "different PrevSealingBlockHash", + a: &SimplexEpochInfo{PrevSealingBlockHash: hash1}, + b: &SimplexEpochInfo{PrevSealingBlockHash: hash2}, + expected: false, + }, + { + name: "different NextPChainReferenceHeight", + a: &SimplexEpochInfo{NextPChainReferenceHeight: 1}, + b: &SimplexEpochInfo{NextPChainReferenceHeight: 2}, + expected: false, + }, + { + name: "different PrevVMBlockSeq", + a: &SimplexEpochInfo{PrevVMBlockSeq: 1}, + b: &SimplexEpochInfo{PrevVMBlockSeq: 2}, + expected: false, + }, + { + name: "different SealingBlockSeq", + a: &SimplexEpochInfo{SealingBlockSeq: 1}, + b: &SimplexEpochInfo{SealingBlockSeq: 2}, + expected: false, + }, + { + name: "with BlockValidationDescriptor equal", + a: &SimplexEpochInfo{ + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + }, + b: &SimplexEpochInfo{ + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + }, + expected: true, + }, + { + name: "with BlockValidationDescriptor different", + a: &SimplexEpochInfo{ + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + }, + b: &SimplexEpochInfo{ + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{2}, Weight: 20}}, + }, + }, + }, + expected: false, + }, + { + name: "with NextEpochApprovals equal", + a: &SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1, 2}, Signature: []byte{3, 4}}, + }, + b: &SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1, 2}, Signature: []byte{3, 4}}, + }, + expected: true, + }, + { + name: "with NextEpochApprovals different", + a: &SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1, 2}, Signature: []byte{3, 4}}, + }, + b: &SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{5, 6}, Signature: []byte{7, 8}}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.a.Equal(tt.b)) + }) + } +} + +func TestNodeBLSMappingEquals(t *testing.T) { + tests := []struct { + name string + a NodeBLSMapping + b NodeBLSMapping + expected bool + }{ + { + name: "both zero", + expected: true, + }, + { + name: "equal with values", + a: NodeBLSMapping{NodeID: nodeID{1, 2, 3}, BLSKey: []byte{4, 5}, Weight: 100}, + b: NodeBLSMapping{NodeID: nodeID{1, 2, 3}, BLSKey: []byte{4, 5}, Weight: 100}, + expected: true, + }, + { + name: "different NodeID", + a: NodeBLSMapping{NodeID: nodeID{1}}, + b: NodeBLSMapping{NodeID: nodeID{2}}, + expected: false, + }, + { + name: "different BLSKey", + a: NodeBLSMapping{BLSKey: []byte{1}}, + b: NodeBLSMapping{BLSKey: []byte{2}}, + expected: false, + }, + { + name: "different Weight", + a: NodeBLSMapping{Weight: 1}, + b: NodeBLSMapping{Weight: 2}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.a.Equals(&tt.b)) + }) + } +} + +func TestBlockValidationDescriptorEquals(t *testing.T) { + tests := []struct { + name string + a *BlockValidationDescriptor + b *BlockValidationDescriptor + expected bool + }{ + { + name: "both nil", + expected: true, + }, + { + name: "first nil second non-nil", + b: &BlockValidationDescriptor{}, + expected: false, + }, + { + name: "first non-nil second nil", + a: &BlockValidationDescriptor{}, + expected: false, + }, + { + name: "equal members", + a: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + b: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + expected: true, + }, + { + name: "different members", + a: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{1}, Weight: 10}}, + }, + }, + b: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: []NodeBLSMapping{{NodeID: nodeID{2}, Weight: 20}}, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.a.Equals(tt.b)) + }) + } +} + +func TestAggregatedMembershipEquals(t *testing.T) { + tests := []struct { + name string + members []NodeBLSMapping + other []NodeBLSMapping + expected bool + }{ + { + name: "both empty", + expected: true, + }, + { + name: "different lengths", + members: []NodeBLSMapping{{Weight: 1}}, + expected: false, + }, + { + name: "equal", + members: []NodeBLSMapping{{NodeID: nodeID{1}, BLSKey: []byte{2}, Weight: 3}}, + other: []NodeBLSMapping{{NodeID: nodeID{1}, BLSKey: []byte{2}, Weight: 3}}, + expected: true, + }, + { + name: "different", + members: []NodeBLSMapping{{NodeID: nodeID{1}}}, + other: []NodeBLSMapping{{NodeID: nodeID{2}}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + am := &AggregatedMembership{Members: tt.members} + require.Equal(t, tt.expected, am.Equals(tt.other)) + }) + } +} + +func TestNextEpochApprovalsEquals(t *testing.T) { + tests := []struct { + name string + a *NextEpochApprovals + b *NextEpochApprovals + expected bool + }{ + { + name: "both nil", + expected: true, + }, + { + name: "first nil second non-nil", + b: &NextEpochApprovals{}, + expected: false, + }, + { + name: "first non-nil second nil", + a: &NextEpochApprovals{}, + expected: false, + }, + { + name: "equal", + a: &NextEpochApprovals{NodeIDs: []byte{1, 2}, Signature: []byte{3, 4}}, + b: &NextEpochApprovals{NodeIDs: []byte{1, 2}, Signature: []byte{3, 4}}, + expected: true, + }, + { + name: "different NodeIDs", + a: &NextEpochApprovals{NodeIDs: []byte{1}}, + b: &NextEpochApprovals{NodeIDs: []byte{2}}, + expected: false, + }, + { + name: "different Signature", + a: &NextEpochApprovals{Signature: []byte{1}}, + b: &NextEpochApprovals{Signature: []byte{2}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.a.Equals(tt.b)) + }) + } +} + +func TestNodeBLSMappingsTotalWeight(t *testing.T) { + tests := []struct { + name string + mappings NodeBLSMappings + expected uint64 + expectError bool + }{ + { + name: "empty", + expected: 0, + }, + { + name: "single", + mappings: NodeBLSMappings{{Weight: 42}}, + expected: 42, + }, + { + name: "multiple", + mappings: NodeBLSMappings{{Weight: 10}, {Weight: 20}, {Weight: 30}}, + expected: 60, + }, + { + name: "overflow", + mappings: NodeBLSMappings{{Weight: math.MaxUint64}, {Weight: 1}}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + total, err := tt.mappings.TotalWeight() + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, total) + } + }) + } +} + +func TestNodeBLSMappingsCompare(t *testing.T) { + tests := []struct { + name string + a NodeBLSMappings + b NodeBLSMappings + expected bool + }{ + { + name: "both nil", + expected: true, + }, + { + name: "different lengths", + a: NodeBLSMappings{{Weight: 1}}, + expected: false, + }, + { + name: "equal same order", + a: NodeBLSMappings{{NodeID: nodeID{1}, Weight: 10}, {NodeID: nodeID{2}, Weight: 20}}, + b: NodeBLSMappings{{NodeID: nodeID{1}, Weight: 10}, {NodeID: nodeID{2}, Weight: 20}}, + expected: true, + }, + { + name: "equal different order", + a: NodeBLSMappings{{NodeID: nodeID{2}, Weight: 20}, {NodeID: nodeID{1}, Weight: 10}}, + b: NodeBLSMappings{{NodeID: nodeID{1}, Weight: 10}, {NodeID: nodeID{2}, Weight: 20}}, + expected: true, + }, + { + name: "different values", + a: NodeBLSMappings{{NodeID: nodeID{1}, Weight: 10}}, + b: NodeBLSMappings{{NodeID: nodeID{1}, Weight: 99}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.a.Equal(tt.b)) + }) + } +} + +func TestValidatorSetApprovalsFilter(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: nodeID{1}, PChainHeight: 10}, + {NodeID: nodeID{2}, PChainHeight: 20}, + {NodeID: nodeID{3}, PChainHeight: 30}, + } + + filtered := approvals.Filter(func(v ValidatorSetApproval) bool { + return v.PChainHeight > 15 + }) + require.Len(t, filtered, 2) + require.Equal(t, uint64(20), filtered[0].PChainHeight) + require.Equal(t, uint64(30), filtered[1].PChainHeight) + + // Filter all + filtered = approvals.Filter(func(ValidatorSetApproval) bool { + return false + }) + require.Empty(t, filtered) +} diff --git a/msm/helpers_test.go b/msm/helpers_test.go new file mode 100644 index 00000000..93a99533 --- /dev/null +++ b/msm/helpers_test.go @@ -0,0 +1,197 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "encoding/asn1" + "fmt" + "testing" + "time" + + "github.com/ava-labs/simplex" + "github.com/ava-labs/simplex/testutil" +) + +// fakeVMBlock is a minimal VMBlock implementation for tests. +type fakeVMBlock struct { + height uint64 +} + +func (f *fakeVMBlock) Digest() [32]byte { return [32]byte{} } +func (f *fakeVMBlock) Height() uint64 { return f.height } +func (f *fakeVMBlock) Timestamp() time.Time { return time.Time{} } +func (f *fakeVMBlock) Verify(_ context.Context) error { return nil } + +type outerBlock struct { + finalization *simplex.Finalization + block StateMachineBlock +} + +type blockStore map[uint64]*outerBlock + +func (bs blockStore) clone() blockStore { + newStore := make(blockStore) + for k, v := range bs { + newStore[k] = v + } + return newStore +} + +func (bs blockStore) getBlock(seq uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + blk, exists := bs[seq] + if !exists { + return StateMachineBlock{}, nil, fmt.Errorf("%w: block %d not found", simplex.ErrBlockNotFound, seq) + } + return blk.block, blk.finalization, nil +} + +type approvalsRetriever struct { + result ValidatorSetApprovals +} + +func (a approvalsRetriever) RetrieveApprovals() ValidatorSetApprovals { + return a.result +} + +type signatureVerifier struct { + err error +} + +func (sv *signatureVerifier) VerifySignature(signature []byte, message []byte, publicKey []byte) error { + return sv.err +} + +type signatureAggregator struct{} + +type aggregatrdSignature struct { + Signatures [][]byte +} + +func (sv *signatureAggregator) AggregateSignatures(signatures ...[]byte) ([]byte, error) { + bytes, err := asn1.Marshal(aggregatrdSignature{Signatures: signatures}) + if err != nil { + return nil, err + } + return bytes, nil +} + +type noOpPChainListener struct{} + +func (n *noOpPChainListener) WaitForProgress(ctx context.Context, _ uint64) error { + <-ctx.Done() + return ctx.Err() +} + +type blockBuilder struct { + block VMBlock + err error +} + +func (bb *blockBuilder) WaitForPendingBlock(_ context.Context) { + // Block is always ready in tests. +} + +func (bb *blockBuilder) BuildBlock(_ context.Context, _ uint64) (VMBlock, error) { + return bb.block, bb.err +} + +type validatorSetRetriever struct { + result NodeBLSMappings + resultMap map[uint64]NodeBLSMappings + err error +} + +func (vsr *validatorSetRetriever) getValidatorSet(height uint64) (NodeBLSMappings, error) { + if vsr.resultMap != nil { + if result, ok := vsr.resultMap[height]; ok { + return result, vsr.err + } + } + return vsr.result, vsr.err +} + +type keyAggregator struct{} + +func (ka *keyAggregator) AggregateKeys(keys ...[]byte) ([]byte, error) { + aggregated := make([]byte, 0) + for _, key := range keys { + aggregated = append(aggregated, key...) + } + return aggregated, nil +} + +type auxiliaryInfoGenerator struct { + info []byte + appID AppID +} + +func (g *auxiliaryInfoGenerator) GenerateAuxiliaryInfo(_ []byte) ([]byte, AppID) { + return g.info, g.appID +} + +var ( + genesisBlock = StateMachineBlock{ + // Genesis block metadata has all zero values + InnerBlock: &InnerBlock{ + TS: time.Now(), + Bytes: []byte{1, 2, 3}, + }, + } + emptyBlacklistBytes = func() []byte { + var b simplex.Blacklist + return b.Bytes() + }() +) + +type testConfig struct { + blockStore blockStore + approvalsRetriever approvalsRetriever + signatureVerifier signatureVerifier + signatureAggregator signatureAggregator + blockBuilder blockBuilder + keyAggregator keyAggregator + validatorSetRetriever validatorSetRetriever + auxiliaryInfoGenerator auxiliaryInfoGenerator +} + +func newStateMachine(t *testing.T) (*StateMachine, *testConfig) { + bs := make(blockStore) + + var testConfig testConfig + testConfig.blockStore = bs + testConfig.validatorSetRetriever.result = NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, {BLSKey: []byte{2}, Weight: 1}, + } + + sm := NewStateMachine(Config{ + ComputeICMEpoch: func(config UpgradeConfig, input ICMEpochInput) ICMEpoch { + return ICMEpoch{ + EpochNumber: 0, + EpochStartTime: 0, + PChainEpochHeight: 0, + } + }, + GetTime: time.Now, + TimeSkewLimit: time.Second * 5, + Logger: testutil.MakeLogger(t), + GetBlock: testConfig.blockStore.getBlock, + MaxBlockBuildingWaitTime: time.Second, + ApprovalsRetriever: &testConfig.approvalsRetriever, + SignatureVerifier: &testConfig.signatureVerifier, + SignatureAggregator: &testConfig.signatureAggregator, + BlockBuilder: &testConfig.blockBuilder, + KeyAggregator: &testConfig.keyAggregator, + GetPChainHeight: func() uint64 { + return 100 + }, + GetUpgrades: func() any { + return nil + }, + GetValidatorSet: testConfig.validatorSetRetriever.getValidatorSet, + PChainProgressListener: &noOpPChainListener{}, + AuxiliaryInfoGenerator: &testConfig.auxiliaryInfoGenerator, + }) + return sm, &testConfig +} diff --git a/msm/misc.go b/msm/misc.go new file mode 100644 index 00000000..8b74cd73 --- /dev/null +++ b/msm/misc.go @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "fmt" + "math" + "math/big" + "time" + + "go.uber.org/zap" +) + +// This file contains implementations of utility methods and structures that exists in Avalanchego, +// but are not imported here to prevent us from importing the entire Avalanchego codebase. +// Once we incorporate Simplex into Avalanchego, we can remove this file and import the relevant code from Avalanchego instead. + +func safeAdd(a, b uint64) (uint64, error) { + if a > math.MaxUint64-b { + return 0, fmt.Errorf("overflow: %d + %d > maxuint64", a, b) + } + return a + b, nil +} + +type nodeID [20]byte + +type VMBlock interface { + // Digest returns a succinct representation of this block. + Digest() [32]byte + + // Height returns the height of this block in the chain. + Height() uint64 + + // Time this block was proposed at. This value should be consistent across + // all nodes. If this block hasn't been successfully verified, any value can + // be returned. If this block is the last accepted block, the timestamp must + // be returned correctly. Otherwise, accepted blocks can return any value. + Timestamp() time.Time + + // Verify that the state transition this block would make if accepted is + // valid. If the state transition is invalid, a non-nil error should be + // returned. + // + // It is guaranteed that the Parent has been successfully verified. + // + // If nil is returned, it is guaranteed that either Accept or Reject will be + // called on this block, unless the VM is shut down. + Verify(context.Context) error +} + +type UpgradeConfig = any + +type bitmask big.Int + +func (bm *bitmask) Bytes() []byte { + return (*big.Int)(bm).Bytes() +} + +func (bm *bitmask) Clone() *bitmask { + var newBM bitmask + (*big.Int)(&newBM).Set((*big.Int)(bm)) + return &newBM +} + +func (bm *bitmask) Contains(i int) bool { + return (*big.Int)(bm).Bit(i) == 1 +} + +func (bm *bitmask) Add(i int) { + bits := (*big.Int)(bm) + bits.SetBit(bits, i, 1) +} + +func (bm *bitmask) Difference(bm2 *bitmask) { + bits := (*big.Int)(bm) + bits2 := (*big.Int)(bm2) + bits.AndNot(bits, bits2) +} + +func (bm *bitmask) Len() int { + bmAsBigInt := (*big.Int)(bm) + bits := new(big.Int).Set(bmAsBigInt) + + result := 0 + var zero big.Int + for bits.Cmp(&zero) > 0 { + lsb := bits.Bit(0) + if lsb == 1 { + result++ + } + bits.Rsh(bits, 1) + } + return result +} + +func bitmaskFromBytes(bytes []byte) bitmask { + var bm bitmask + (*big.Int)(&bm).SetBytes(bytes) + return bm +} + +// Logger defines the interface that is used to keep a record of all events that +// happen to the program +type Logger interface { + // Log that a fatal error has occurred. The program should likely exit soon + // after this is called + Fatal(msg string, fields ...zap.Field) + // Log that an error has occurred. The program should be able to recover + // from this error + Error(msg string, fields ...zap.Field) + // Log that an event has occurred that may indicate a future error or + // vulnerability + Warn(msg string, fields ...zap.Field) + // Log an event that may be useful for a user to see to measure the progress + // of the protocol + Info(msg string, fields ...zap.Field) + // Log an event that may be useful for understanding the order of the + // execution of the protocol + Trace(msg string, fields ...zap.Field) + // Log an event that may be useful for a programmer to see when debugging the + // execution of the protocol + Debug(msg string, fields ...zap.Field) + // Log extremely detailed events that can be useful for inspecting every + // aspect of the program + Verbo(msg string, fields ...zap.Field) +} diff --git a/msm/misc_test.go b/msm/misc_test.go new file mode 100644 index 00000000..b899aa6a --- /dev/null +++ b/msm/misc_test.go @@ -0,0 +1,144 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSafeAdd(t *testing.T) { + for _, tc := range []struct { + name string + a, b uint64 + sum uint64 + err string + }{ + { + name: "zero plus zero", + a: 0, b: 0, + sum: 0, + }, + { + name: "normal addition", + a: 10, b: 20, + sum: 30, + }, + { + name: "max uint64 plus zero", + a: math.MaxUint64, b: 0, + sum: math.MaxUint64, + }, + { + name: "zero plus max uint64", + a: 0, b: math.MaxUint64, + sum: math.MaxUint64, + }, + { + name: "overflow by one", + a: math.MaxUint64, b: 1, + err: "overflow", + }, + { + name: "overflow both large", + a: math.MaxUint64 - 5, b: 10, + err: "overflow", + }, + { + name: "max uint64 boundary no overflow", + a: math.MaxUint64 - 5, b: 5, + sum: math.MaxUint64, + }, + } { + t.Run(tc.name, func(t *testing.T) { + result, err := safeAdd(tc.a, tc.b) + if tc.err != "" { + require.ErrorContains(t, err, tc.err) + } else { + require.NoError(t, err) + require.Equal(t, tc.sum, result) + } + }) + } +} + +func TestBitmask(t *testing.T) { + t.Run("empty bitmask", func(t *testing.T) { + bm := bitmaskFromBytes(nil) + require.Equal(t, 0, bm.Len()) + require.False(t, bm.Contains(0)) + require.False(t, bm.Contains(5)) + }) + + t.Run("from bytes and Contains", func(t *testing.T) { + // 0b00000111 = 7 → bits 0, 1, 2 are set + bm := bitmaskFromBytes([]byte{7}) + require.True(t, bm.Contains(0)) + require.True(t, bm.Contains(1)) + require.True(t, bm.Contains(2)) + require.False(t, bm.Contains(3)) + require.Equal(t, 3, bm.Len()) + }) + + t.Run("Add", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{1}) // bit 0 + require.True(t, bm.Contains(0)) + require.False(t, bm.Contains(3)) + + bm.Add(3) + require.True(t, bm.Contains(3)) + require.Equal(t, 2, bm.Len()) + }) + + t.Run("Bytes round-trip", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{0xAB}) + bm2 := bitmaskFromBytes(bm.Bytes()) + require.Equal(t, bm.Len(), bm2.Len()) + for i := 0; i < 8; i++ { + require.Equal(t, bm.Contains(i), bm2.Contains(i)) + } + }) + + t.Run("Difference", func(t *testing.T) { + // bm1 = bits 0,1,2 (0b111 = 7) + // bm2 = bits 0,1 (0b011 = 3) + // bm1.Difference(bm2) should leave only bit 2 + bm1 := bitmaskFromBytes([]byte{7}) + bm2 := bitmaskFromBytes([]byte{3}) + bm1.Difference(&bm2) + require.False(t, bm1.Contains(0)) + require.False(t, bm1.Contains(1)) + require.True(t, bm1.Contains(2)) + require.Equal(t, 1, bm1.Len()) + }) + + t.Run("Len with multiple bytes", func(t *testing.T) { + // 0xFF = 8 bits set, 0x01 = 1 bit set → 9 total + bm := bitmaskFromBytes([]byte{0x01, 0xFF}) + require.Equal(t, 9, bm.Len()) + }) + + t.Run("Clone produces independent copy", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{7}) // bits 0,1,2 + cloned := bm.Clone() + + // Clone matches original + require.Equal(t, bm.Len(), cloned.Len()) + for i := 0; i < 3; i++ { + require.Equal(t, bm.Contains(i), cloned.Contains(i)) + } + + // Mutating clone does not affect original + cloned.Add(5) + require.True(t, cloned.Contains(5)) + require.False(t, bm.Contains(5)) + + // Mutating original does not affect clone + bm.Add(7) + require.True(t, bm.Contains(7)) + require.False(t, cloned.Contains(7)) + }) +} diff --git a/msm/msm.go b/msm/msm.go new file mode 100644 index 00000000..146652a6 --- /dev/null +++ b/msm/msm.go @@ -0,0 +1,1107 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "math" + "math/big" + "sort" + "time" + + "github.com/ava-labs/simplex" + "go.uber.org/zap" +) + +// ICMEpochInput defines the input for computing the ICM Epoch information for the next block. +type ICMEpochInput struct { + // ParentPChainHeight is the P-chain height recorded in the parent block. + ParentPChainHeight uint64 + // ParentTimestamp is the timestamp of the parent block. + ParentTimestamp time.Time + // ChildTimestamp is the timestamp of the block being built. + ChildTimestamp time.Time + // ParentEpoch is the ICM epoch information from the parent block. + ParentEpoch ICMEpoch +} + +// ICMEpoch defines the ICM epoch information that is maintained by the StateMachine and used for the ICM protocol. +// The Statemachine maintains this information identically to how the proposerVM maintains this information, +// and it does so by building the ICMEpochInput and then passing it into the StateMachine's ComputeICMEpoch function. +type ICMEpoch struct { + // EpochStartTime is the Unix timestamp when this ICM epoch started. + EpochStartTime uint64 + // EpochNumber is the sequential identifier of this ICM epoch. + EpochNumber uint64 + // PChainEpochHeight is the P-chain height associated with this ICM epoch. + PChainEpochHeight uint64 +} + +// A StateMachineBlock is a representation of a parsed OuterBlock, containing the inner block and the metadata. +type StateMachineBlock struct { + // InnerBlock is the VM-level block, or nil if this is a block without an inner block (e.g., a Telock block). + InnerBlock VMBlock + // Metadata contains the state machine metadata associated with this block. + Metadata StateMachineMetadata +} + +// Digest returns the SHA-256 hash of the combined inner block digest and metadata digest. +func (smb *StateMachineBlock) Digest() [32]byte { + var blockDigest [32]byte + if smb.InnerBlock != nil { + blockDigest = smb.InnerBlock.Digest() + } else { + blockDigest = [32]byte{} + } + mdDigest := sha256.Sum256(smb.Metadata.MarshalCanoto()) + combined := make([]byte, 64) + copy(combined[:32], blockDigest[:]) + copy(combined[32:], mdDigest[:]) + return sha256.Sum256(combined) +} + +// AuxiliaryInfoGenerator generates the AuxiliaryInfo for the next block in case the MSM is collecting approvals. +// The AuxiliaryInfo is opaque information that is included in the block metadata +// and is contributed by the block builder. +type AuxiliaryInfoGenerator interface { + // GenerateAuxiliaryInfo generates the AuxiliaryInfo for the next block and returns it along with the AppID. + // The AppID is used to identify the type of the AuxiliaryInfo and it exists for the purpose of backwards compatibility. + // The input is the previous AuxiliaryInfo, if applicable, which can be used to determine if the current node has already contributed + // its own AuxiliaryInfo for the current epoch transition. + GenerateAuxiliaryInfo([]byte) ([]byte, AppID) +} + +// AuxiliaryInfoVerifier verifies the AuxiliaryInfo included in the block metadata when we are collecting approvals. +type AuxiliaryInfoVerifier interface { + // VerifyAuxiliaryInfo verifies the given new AuxiliaryInfo included in the block metadata. + // The given prevAuxInfos are the AuxiliaryInfo from previous blocks in the current epoch transition, + // which can be used for verification. + VerifyAuxiliaryInfo(auxInfo *AuxiliaryInfo, prevAuxInfos []AuxiliaryInfo) error +} + +// ApprovalsRetriever retrieves the approvals from validators of the next epoch for the epoch change. +type ApprovalsRetriever interface { + RetrieveApprovals() ValidatorSetApprovals +} + +// SignatureVerifier verifies a cryptographic signature against a message and public key. +// Used to verify Approvals from validators for epoch transitions. +type SignatureVerifier interface { + VerifySignature(signature []byte, message []byte, publicKey []byte) error +} + +// SignatureAggregator combines multiple cryptographic signatures into a single aggregated signature. +// Used to aggregate validator signatures for epoch transitions. +type SignatureAggregator interface { + AggregateSignatures(signatures ...[]byte) ([]byte, error) +} + +// KeyAggregator combines multiple public keys into a single aggregated public key. +type KeyAggregator interface { + AggregateKeys(keys ...[]byte) ([]byte, error) +} + +// ICMEpochTransition computes the next ICM epoch given the current upgrade configuration and epoch input. +type ICMEpochTransition func(UpgradeConfig, ICMEpochInput) ICMEpoch + +// ValidatorSetRetriever retrieves the validator set at a given P-chain height. +type ValidatorSetRetriever func(pChainHeight uint64) (NodeBLSMappings, error) + +// BlockRetriever retrieves the block at the given sequence number along with +// its finalization status. The digest is the expected hash of the block and +// can be used for validation; callers that don't care about digest validation +// may pass the zero value. +// If the block cannot be found it returns ErrBlockNotFound. +// If an error occurs during retrieval, it returns a non-nil error. +type BlockRetriever func(seq uint64, digest [32]byte) (StateMachineBlock, *simplex.Finalization, error) + +// BlockBuilder builds a new VM block with the given observed P-chain height. +type BlockBuilder interface { + BuildBlock(ctx context.Context, pChainHeight uint64) (VMBlock, error) + + // WaitForPendingBlock returns when either the given context is cancelled, + // or when the VM signals that a block should be built. + WaitForPendingBlock(ctx context.Context) +} + +type Config struct { + // AuxiliaryInfoVerifier verifies the AuxiliaryInfo included in the block metadata when we are collecting approvals for an epoch transition. + AuxiliaryInfoVerifier AuxiliaryInfoVerifier + // AuxiliaryInfoGenerator generates the AuxiliaryInfo for the next block when we are collecting approvals for an epoch transition. + AuxiliaryInfoGenerator AuxiliaryInfoGenerator + // LatestPersistedHeight is the height of the most recently persisted block. + LatestPersistedHeight uint64 + // MaxBlockBuildingWaitTime is the maximum duration to wait for the VM to build a block + // before producing a block without an inner block. + MaxBlockBuildingWaitTime time.Duration + // TimeSkewLimit is the maximum allowed time difference between a block's timestamp and the current time. + TimeSkewLimit time.Duration + // GetTime returns the current time. + GetTime func() time.Time + // ComputeICMEpoch computes the ICM epoch for the next block. + ComputeICMEpoch ICMEpochTransition + // GetPChainHeight returns the latest known P-chain height. + GetPChainHeight func() uint64 + // GetUpgrades returns the current upgrade configuration. + GetUpgrades func() UpgradeConfig + // BlockBuilder builds new VM blocks. + BlockBuilder BlockBuilder + // Logger is used for logging state machine operations. + Logger Logger + // GetValidatorSet retrieves the validator set at a given P-chain height. + GetValidatorSet ValidatorSetRetriever + // GetBlock retrieves a previously built or finalized block. + GetBlock BlockRetriever + // ApprovalsRetriever retrieves validator approvals for epoch transitions. + ApprovalsRetriever ApprovalsRetriever + // SignatureAggregator aggregates signatures from validators. + SignatureAggregator SignatureAggregator + // KeyAggregator aggregates public keys from validators. + KeyAggregator KeyAggregator + // SignatureVerifier verifies signatures from validators. + SignatureVerifier SignatureVerifier + // PChainProgressListener listens for changes in the P-chain height to trigger block building or epoch transitions. + PChainProgressListener PChainProgressListener +} + +// StateMachine manages block building and verification across epoch transitions. +type StateMachine struct { + Config + + // verifiers is the list of verifiers used to verify proposed blocks. + // Each verifier is responsible for verifying a specific aspect of the block's metadata. + verifiers []verifier +} + +// NewStateMachine builds a StateMachine from the given configuration and wires +// up its block verifiers. Callers should use this constructor rather than +// constructing a StateMachine literal directly. +// +// The verifiers read their dependencies through closures over the returned +// pointer, so later mutations of sm.GetBlock, sm.GetValidatorSet, and the +// other function-valued fields are visible to them. +func NewStateMachine(config Config) *StateMachine { + out := &StateMachine{Config: config} + + getPChainHeight := func() uint64 { return out.GetPChainHeight() } + getTime := func() time.Time { return out.GetTime() } + getBlock := func(seq uint64, digest [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + return out.GetBlock(seq, digest) + } + getValidatorSet := func(height uint64) (NodeBLSMappings, error) { + return out.GetValidatorSet(height) + } + + out.verifiers = []verifier{ + &AuxInfoVerifier{}, + &icmEpochInfoVerifier{ + computeICMEpoch: config.ComputeICMEpoch, + getUpdates: config.GetUpgrades, + }, + &pChainHeightVerifier{ + getPChainHeight: getPChainHeight, + }, + ×tampVerifier{ + timeSkewLimit: out.TimeSkewLimit, + getTime: getTime, + }, + &pChainReferenceHeightVerifier{}, + &epochNumberVerifier{}, + &prevSealingBlockHashVerifier{ + getBlock: getBlock, + latestPersistedHeight: &out.LatestPersistedHeight, + }, + &nextPChainReferenceHeightVerifier{ + getPChainHeight: getPChainHeight, + getValidatorSet: getValidatorSet, + }, + &vmBlockSeqVerifier{ + getBlock: getBlock, + }, + &validationDescriptorVerifier{ + getValidatorSet: getValidatorSet, + }, + &nextEpochApprovalsVerifier{ + getValidatorSet: getValidatorSet, + keyAggregator: out.KeyAggregator, + sigVerifier: out.SignatureVerifier, + }, + &sealingBlockSeqVerifier{}, + } + return out +} + +type state uint8 + +const ( + stateFirstSimplexBlock state = iota + 1 + stateBuildBlockNormalOp + stateBuildCollectingApprovals + stateBuildBlockEpochSealed +) + +// BuildBlock constructs the next block on top of the given parent block, and passes in the provided simplex metadata and blacklist. +func (sm *StateMachine) BuildBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist simplex.Blacklist) (*StateMachineBlock, error) { + // The zero sequence number is reserved for the genesis block, which should never be built. + if simplexMetadata.Seq == 0 { + return nil, fmt.Errorf("invalid ProtocolMetadata sequence number: should be > 0, got %d", simplexMetadata.Seq) + } + + start := time.Now() + + sm.Logger.Debug("Building block", + zap.Uint64("seq", simplexMetadata.Seq), + zap.Uint64("epoch", simplexMetadata.Epoch), + zap.Stringer("prevHash", simplexMetadata.Prev)) + + defer func() { + elapsed := time.Since(start) + sm.Logger.Debug("Built block", + zap.Uint64("seq", simplexMetadata.Seq), + zap.Uint64("epoch", simplexMetadata.Epoch), + zap.Stringer("prevHash", simplexMetadata.Prev), + zap.Duration("elapsed", elapsed), + ) + }() + + blacklistBytes := blacklist.Bytes() + + // In order to know where in the epoch change process we are, + // we identify the current state by looking at the parent block's epoch info. + currentState := parentBlock.Metadata.SimplexEpochInfo.CurrentState() + + switch currentState { + case stateFirstSimplexBlock: + return sm.buildBlockZero(ctx, parentBlock, simplexMetadata, blacklistBytes) + case stateBuildBlockNormalOp: + return sm.buildBlockNormalOp(ctx, parentBlock, simplexMetadata, blacklistBytes) + case stateBuildCollectingApprovals: + return sm.buildBlockCollectingApprovals(ctx, parentBlock, simplexMetadata, blacklistBytes) + case stateBuildBlockEpochSealed: + return sm.buildBlockEpochSealed(ctx, parentBlock, simplexMetadata, blacklistBytes) + default: + return nil, fmt.Errorf("unknown state %d", currentState) + } +} + +// VerifyBlock validates a proposed block by checking its metadata, epoch info, +// and inner block against the previous block and the current state. +func (sm *StateMachine) VerifyBlock(ctx context.Context, block *StateMachineBlock) error { + if block == nil { + return fmt.Errorf("InnerBlock is nil") + } + + pmd, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + if err != nil { + return fmt.Errorf("failed to parse ProtocolMetadata: %w", err) + } + + seq := pmd.Seq + + if seq == 0 { + return fmt.Errorf("attempted to build a genesis inner block") + } + + prevBlock, _, err := sm.GetBlock(seq-1, pmd.Prev) + if err != nil { + return fmt.Errorf("failed to retrieve previous (%d) inner block: %w", seq-1, err) + } + + prevMD := prevBlock.Metadata + currentState := prevMD.SimplexEpochInfo.CurrentState() + + switch currentState { + case stateFirstSimplexBlock: + err = sm.verifyBlockZero(ctx, block, prevBlock) + default: + err = sm.verifyNonZeroBlock(ctx, block, prevBlock.Metadata, currentState, pmd.Prev, seq-1) + } + return err +} + +func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block *StateMachineBlock, prevBlockMD StateMachineMetadata, state state, prevHash [32]byte, prevSeq uint64) error { + blockType := IdentifyBlockType(block.Metadata, prevBlockMD, prevSeq) + sm.Logger.Debug("Identified block type", + zap.Stringer("blockType", blockType), + zap.Bool("nextHasBVD", block.Metadata.SimplexEpochInfo.BlockValidationDescriptor != nil), + zap.Uint64("nextEpochNumber", block.Metadata.SimplexEpochInfo.EpochNumber), + zap.Bool("prevHasBVD", prevBlockMD.SimplexEpochInfo.BlockValidationDescriptor != nil), + zap.Uint64("prevEpochNumber", prevBlockMD.SimplexEpochInfo.EpochNumber), + zap.Uint64("prevNextPChainRefHeight", prevBlockMD.SimplexEpochInfo.NextPChainReferenceHeight), + zap.Uint64("prevSealingBlockSeq", prevBlockMD.SimplexEpochInfo.SealingBlockSeq), + zap.Uint64("prevSeq", prevSeq), + ) + + var innerBlockTimestamp time.Time + if block.InnerBlock != nil { + innerBlockTimestamp = block.InnerBlock.Timestamp() + } + + for _, verifier := range sm.verifiers { + if err := verifier.Verify(verificationInput{ + proposedBlockMD: block.Metadata, + nextBlockType: blockType, + prevMD: prevBlockMD, + state: state, + prevBlockSeq: prevSeq, + prevBlockHash: prevHash, + hasInnerBlock: block.InnerBlock != nil, + innerBlockTimestamp: innerBlockTimestamp, + }); err != nil { + sm.Logger.Debug("Invalid block", zap.Error(err)) + return err + } + } + + if block.InnerBlock == nil { + return nil + } + + return block.InnerBlock.Verify(ctx) +} + +// CurrentState identifies, given the SimplexEpochInfo of the parent block, which +// state the state machine is in for the purpose of building the next block. +func (sei *SimplexEpochInfo) CurrentState() state { + // If this is the first ever epoch, then this is also the first ever block to be built by Simplex. + if sei.EpochNumber == 0 { + return stateFirstSimplexBlock + } + + // If we don't have a next P-chain preference height, it means we are not transitioning to a new epoch just yet. + if sei.NextPChainReferenceHeight == 0 { + return stateBuildBlockNormalOp + } + + // If the previous block has a sealing block sequence, it's a Telock. + // If it has a block validation descriptor, it's a sealing block. + // Either way, the epoch has been sealed. + if sei.SealingBlockSeq > 0 || sei.BlockValidationDescriptor != nil { + return stateBuildBlockEpochSealed + } + + // In any other case, NextPChainReferenceHeight > 0 but the previous block is not a Telock or sealing block, + // it means we are in the process of collecting approvals for the next epoch. + return stateBuildCollectingApprovals +} + +// buildBlockNormalOp builds a block while not trying to transition to a new epoch. +func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte) (*StateMachineBlock, error) { + // Since in the previous block, we were not transitioning to a new epoch, + // the P-chain reference height and epoch of the new block should remain the same. + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, simplexMetadata.Seq-1), + } + + blockBuildingDecider := newBlockBuildingDecider(sm, parentBlock) + decisionToBuildBlock, pChainHeight, err := blockBuildingDecider.shouldBuildBlock(ctx) + if err != nil { + return nil, err + } + + sm.Logger.Debug("Block building decision", zap.Stringer("decision", decisionToBuildBlock)) + + switch decisionToBuildBlock { + case blockBuildingDecisionBuildBlock, blockBuildingDecisionBuildBlockAndTransitionEpoch: + // If we reached here, we need to build a new block, and maybe also transition to a new epoch. + transitionEpoch := decisionToBuildBlock == blockBuildingDecisionBuildBlockAndTransitionEpoch + return sm.buildBlockAndMaybeTransitionEpoch(ctx, parentBlock, simplexMetadata, blacklist, newSimplexEpochInfo, pChainHeight, transitionEpoch) + case blockBuildingDecisionTransitionEpoch: + // If we reached here, we don't need to build an inner block, yet we need to transition to a new epoch. + // Initiate the epoch transition by setting the next P-chain reference height for the new epoch info, + // and build a block without an inner block. + newSimplexEpochInfo.NextPChainReferenceHeight = pChainHeight + return sm.wrapBlock(parentBlock, nil, newSimplexEpochInfo, pChainHeight, simplexMetadata, blacklist, nil), nil + case blockBuildingDecisionContextCanceled: + return nil, ctx.Err() + default: + return nil, fmt.Errorf("unknown block building decision %d", decisionToBuildBlock) + } +} + +func (sm *StateMachine) buildBlockAndMaybeTransitionEpoch( + ctx context.Context, + parentBlock StateMachineBlock, + simplexMetadata simplex.ProtocolMetadata, + blacklist []byte, + newSimplexEpochInfo SimplexEpochInfo, + pChainHeight uint64, + transitionEpoch bool, +) (*StateMachineBlock, error) { + childBlock, err := sm.BlockBuilder.BuildBlock(ctx, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) + if err != nil { + return nil, err + } + + if transitionEpoch { + // We need to also transition to a new epoch, in addition to building an inner block, + // so set the next P-chain reference height for the new epoch info. + newSimplexEpochInfo.NextPChainReferenceHeight = pChainHeight + } + + return sm.wrapBlock(parentBlock, childBlock, newSimplexEpochInfo, pChainHeight, simplexMetadata, blacklist, nil), nil +} + +// buildBlockZero builds the first ever block for Simplex, +// which is a special block that introduces the first validator set and starts the first epoch. +func (sm *StateMachine) buildBlockZero(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte) (*StateMachineBlock, error) { + pChainHeight := sm.GetPChainHeight() + + newValidatorSet, err := sm.GetValidatorSet(pChainHeight) + if err != nil { + return nil, err + } + + var prevVMBlockSeq uint64 + if parentBlock.InnerBlock != nil { + prevVMBlockSeq = parentBlock.InnerBlock.Height() + } else { + // We can only have blocks without inner blocks in Simplex blocks, but this is the first Simplex block. + // Therefore, the parent block must have an inner block. + sm.Logger.Error("Parent block has no inner block, cannot determine previous VM block sequence for zero block") + return nil, fmt.Errorf("failed constructing zero block: parent block has no inner block") + } + simplexEpochInfo := constructSimplexZeroBlock(pChainHeight, newValidatorSet, prevVMBlockSeq) + + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, blacklist, simplexEpochInfo, pChainHeight) +} + +func (sm *StateMachine) verifyBlockZero(ctx context.Context, block *StateMachineBlock, prevBlock StateMachineBlock) error { + if block == nil { + return fmt.Errorf("block is nil") + } + + simplexEpochInfo := block.Metadata.SimplexEpochInfo + + if simplexEpochInfo.EpochNumber != 1 { + return fmt.Errorf("invalid epoch number (%d), should be 1", simplexEpochInfo.EpochNumber) + } + + if prevBlock.InnerBlock == nil { + return fmt.Errorf("parent inner block (%s) has no inner block", prevBlock.Digest()) + } + + prevVMBlockSeq := prevBlock.InnerBlock.Height() + + currentPChainHeight := sm.GetPChainHeight() + + if block.Metadata.PChainHeight > currentPChainHeight { + return fmt.Errorf("invalid P-chain height (%d) is too big, expected to be ≤ %d", + block.Metadata.PChainHeight, currentPChainHeight) + } + + if prevBlock.Metadata.PChainHeight > block.Metadata.PChainHeight { + return fmt.Errorf("invalid P-chain height (%d) is smaller than parent InnerBlock's P-chain height (%d)", + block.Metadata.PChainHeight, prevBlock.Metadata.PChainHeight) + } + + expectedValidatorSet, err := sm.GetValidatorSet(simplexEpochInfo.PChainReferenceHeight) + if err != nil { + return fmt.Errorf("failed to retrieve validator set at height %d: %w", simplexEpochInfo.PChainReferenceHeight, err) + } + + if simplexEpochInfo.BlockValidationDescriptor == nil { + return fmt.Errorf("invalid BlockValidationDescriptor: should not be nil") + } + + membership := simplexEpochInfo.BlockValidationDescriptor.AggregatedMembership.Members + if !NodeBLSMappings(membership).Equal(expectedValidatorSet) { + return fmt.Errorf("invalid BlockValidationDescriptor: should match validator set at P-chain height %d", simplexEpochInfo.PChainReferenceHeight) + } + + // If we have compared all fields so far, the rest of the fields we compare by constructing an explicit expected SimplexEpochInfo + expectedSimplexEpochInfo := constructSimplexZeroBlock(simplexEpochInfo.PChainReferenceHeight, expectedValidatorSet, prevVMBlockSeq) + + if !expectedSimplexEpochInfo.Equal(&simplexEpochInfo) { + return fmt.Errorf("invalid SimplexEpochInfo: expected %v, got %v", expectedSimplexEpochInfo, simplexEpochInfo) + } + + proposedTime, err := sm.verifyZeroBlockTimestamp(block, prevBlock) + if err != nil { + return err + } + + // Verify ICM epoch info + expectedICMInfo := nextICMEpochInfo(prevBlock.Metadata, block.InnerBlock != nil, sm.GetUpgrades, sm.ComputeICMEpoch, proposedTime) + if !expectedICMInfo.Equal(&block.Metadata.ICMEpochInfo) { + return fmt.Errorf("expected ICM epoch info to be %v but got %v", expectedICMInfo, block.Metadata.ICMEpochInfo) + } + + if block.InnerBlock == nil { + return nil + } + + return block.InnerBlock.Verify(ctx) +} + +func (sm *StateMachine) verifyZeroBlockTimestamp(block *StateMachineBlock, prevBlock StateMachineBlock) (time.Time, error) { + var proposedTime time.Time + if block.InnerBlock != nil { + proposedTime = block.InnerBlock.Timestamp() + } else { + proposedTime = time.UnixMilli(int64(prevBlock.Metadata.Timestamp)) + } + + expectedTimestamp := proposedTime.UnixMilli() + if expectedTimestamp != int64(block.Metadata.Timestamp) { + return time.Time{}, fmt.Errorf("expected timestamp to be %d but got %d", expectedTimestamp, int64(block.Metadata.Timestamp)) + } + currentTime := sm.GetTime() + if currentTime.Add(sm.TimeSkewLimit).Before(proposedTime) { + return time.Time{}, fmt.Errorf("proposed block timestamp is too far in the future, current time is %s but got %s", currentTime.String(), proposedTime.String()) + } + if prevBlock.Metadata.Timestamp > block.Metadata.Timestamp { + return time.Time{}, fmt.Errorf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", prevBlock.Metadata.Timestamp, block.Metadata.Timestamp) + } + return proposedTime, nil +} + +func (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte) (*StateMachineBlock, error) { + // The P-chain reference height and epoch number should remain the same until we transition to the new epoch. + // The next P-chain reference height should have been set in the previous block, + // which is the reason why we are collecting approvals in the first place. + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + NextPChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, simplexMetadata.Seq-1), + } + + // We prepare information that is needed to compute the approvals for the new epoch, + // such as the validator set for the next epoch, and the approvals from peers. + validators, err := sm.GetValidatorSet(parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight) + if err != nil { + return nil, err + } + + // We retrieve approvals that validators have sent us for the next epoch. + // These approvals are signed by validators of the next epoch. + approvalsFromPeers := sm.ApprovalsRetriever.RetrieveApprovals() + auxInfo := parentBlock.Metadata.AuxiliaryInfo + nextPChainHeight := newSimplexEpochInfo.NextPChainReferenceHeight + prevNextEpochApprovals := parentBlock.Metadata.SimplexEpochInfo.NextEpochApprovals + + newApprovals, err := computeNewApprovals(prevNextEpochApprovals, auxInfo, approvalsFromPeers, nextPChainHeight, sm.SignatureAggregator, validators) + if err != nil { + return nil, err + } + + // This might be the first time we created approvals for the next epoch, + // so we need to initialize the NextEpochApprovals. + if newSimplexEpochInfo.NextEpochApprovals == nil { + newSimplexEpochInfo.NextEpochApprovals = &NextEpochApprovals{} + } + // The node IDs and signature are aggregated across all past and present approvals. + newSimplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs + newSimplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature + pChainHeight := parentBlock.Metadata.PChainHeight + + // We might not have enough approvals to seal the current epoch, + // in which case we just carry over the approvals we have so far to the next block, + // so that eventually we'll have enough approvals to seal the epoch. + if !newApprovals.canSeal { + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, blacklist, newSimplexEpochInfo, pChainHeight) + } + + // Else, we have enough approvals to seal the epoch, so we create the sealing block. + return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, blacklist, newSimplexEpochInfo, pChainHeight) +} + +// buildBlockImpatiently builds a block by waiting for the VM to build a block until MaxBlockBuildingWaitTime. +// If the VM fails to build a block within that time, we build a block without an inner block, +// so that we can continue making progress and not get stuck waiting for the VM. +func (sm *StateMachine) buildBlockImpatiently(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64) (*StateMachineBlock, error) { + impatientContext, cancel := context.WithTimeout(ctx, sm.MaxBlockBuildingWaitTime) + defer cancel() + + start := time.Now() + + childBlock, err := sm.BlockBuilder.BuildBlock(impatientContext, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) + if err != nil && impatientContext.Err() == nil { + // If we got an error building the block, and we didn't time out, log the error but continue building the block without the inner block, + // so that we can continue making progress and not get stuck on a single block. + sm.Logger.Error("Error building block, building block without inner block instead", zap.Error(err)) + } + if impatientContext.Err() != nil { + sm.Logger.Debug("Timed out waiting for block to be built, building block without inner block instead", + zap.Duration("elapsed", time.Since(start)), zap.Duration("maxBlockBuildingWaitTime", sm.MaxBlockBuildingWaitTime)) + } + + newAuxInfo, err := sm.generateAuxInfoForNextBlock(parentBlock, simplexMetadata, simplexEpochInfo) + if err != nil { + return nil, err + } + + return sm.wrapBlock(parentBlock, childBlock, simplexEpochInfo, pChainHeight, simplexMetadata, blacklist, newAuxInfo), nil +} + +func (sm *StateMachine) generateAuxInfoForNextBlock(parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, simplexEpochInfo SimplexEpochInfo) (*AuxiliaryInfo, error) { + // The zero block shouldn't have auxiliary info. + if parentBlock.Metadata.SimplexEpochInfo.EpochNumber == 0 { + return nil, nil + } + + // The sealing block also shouldn't have auxiliary info, as we have decided that we can seal the epoch + // based on the auxiliary info of the previous block. + if simplexEpochInfo.BlockValidationDescriptor != nil { + return nil, nil + } + + parentSeq := simplexMetadata.Seq - 1 + prevAuxInfoSeq, prevAuxInfo, err := sm.retrievePreviousAuxiliaryInfo(parentBlock, parentSeq) + if err != nil { + return nil, fmt.Errorf("failed to retrieve previous AuxiliaryInfo: %w", err) + } + + auxInfo, appID := sm.AuxiliaryInfoGenerator.GenerateAuxiliaryInfo(prevAuxInfo) + + newAuxInfo := &AuxiliaryInfo{ + PrevAuxInfoSeq: prevAuxInfoSeq, + ApplicationID: appID, + Info: auxInfo, + } + return newAuxInfo, nil +} + +func (sm *StateMachine) retrievePreviousAuxiliaryInfo(parentBlock StateMachineBlock, parentSeq uint64) (uint64, []byte, error) { + prevAuxInfo := parentBlock.Metadata.AuxiliaryInfo + + if prevAuxInfo == nil || prevAuxInfo.IsZero() { + return 0, nil, nil + } + + if len(prevAuxInfo.Info) > 0 { + return parentSeq, prevAuxInfo.Info, nil + } + + if prevAuxInfo.PrevAuxInfoSeq > 0 { + block, _, err := sm.GetBlock(prevAuxInfo.PrevAuxInfoSeq, [32]byte{}) + if err != nil { + return 0, nil, fmt.Errorf("failed to retrieve block for previous AuxiliaryInfo sequence %d: %w", prevAuxInfo.PrevAuxInfoSeq, err) + } + if block.Metadata.AuxiliaryInfo == nil { + return 0, nil, fmt.Errorf("block for previous AuxiliaryInfo sequence %d has no AuxiliaryInfo in metadata", prevAuxInfo.PrevAuxInfoSeq) + } + return prevAuxInfo.PrevAuxInfoSeq, block.Metadata.AuxiliaryInfo.Info, nil + } + + // Else, prevAuxInfo.PrevAuxInfoSeq is 0 and prevAuxInfo.Info is empty. + return 0, nil, nil +} + +func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte, simplexEpochInfo SimplexEpochInfo, pChainHeight uint64) (*StateMachineBlock, error) { + validators, err := sm.GetValidatorSet(simplexEpochInfo.NextPChainReferenceHeight) + if err != nil { + return nil, err + } + if simplexEpochInfo.BlockValidationDescriptor == nil { + simplexEpochInfo.BlockValidationDescriptor = &BlockValidationDescriptor{} + } + simplexEpochInfo.BlockValidationDescriptor.AggregatedMembership.Members = validators + + // If this is not the first epoch, and this is the sealing block, we set the hash of the previous sealing block. + if simplexEpochInfo.EpochNumber > 1 { + prevSealingBlock, finalization, err := sm.GetBlock(simplexEpochInfo.EpochNumber, [32]byte{}) + if err != nil { + sm.Logger.Error("Error retrieving previous sealing block", zap.Uint64("seq", simplexEpochInfo.EpochNumber), zap.Error(err)) + return nil, fmt.Errorf("failed to retrieve previous sealing InnerBlock at epoch %d: %w", simplexEpochInfo.EpochNumber-1, err) + } + if finalization == nil { + sm.Logger.Error("Previous sealing block is not finalized", zap.Uint64("seq", simplexEpochInfo.EpochNumber)) + return nil, fmt.Errorf("previous sealing InnerBlock at epoch %d is not finalized", simplexEpochInfo.EpochNumber-1) + } + simplexEpochInfo.PrevSealingBlockHash = prevSealingBlock.Digest() + } else { // Else, this is the first epoch, so we use the hash of the first ever Simplex block. + + firstSimplexBlock, err := findFirstSimplexBlock(sm.GetBlock, sm.LatestPersistedHeight+1) + if err != nil { + return nil, fmt.Errorf("failed to find first simplex block: %w", err) + } + firstSimplexBlockRetrieved, _, err := sm.GetBlock(firstSimplexBlock, [32]byte{}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve first simplex block at height %d: %w", firstSimplexBlock, err) + } + simplexEpochInfo.PrevSealingBlockHash = firstSimplexBlockRetrieved.Digest() + } + + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, blacklist, simplexEpochInfo, pChainHeight) +} + +// wrapBlock creates a new StateMachineBlock by wrapping the VM block (if applicable) and adding the appropriate metadata. +func (sm *StateMachine) wrapBlock(parentBlock StateMachineBlock, childBlock VMBlock, newSimplexEpochInfo SimplexEpochInfo, pChainHeight uint64, simplexMetadata simplex.ProtocolMetadata, blacklist []byte, auxInfo *AuxiliaryInfo) *StateMachineBlock { + parentMetadata := parentBlock.Metadata + timestamp := parentMetadata.Timestamp + + hasChildBlock := childBlock != nil + getUpgrades := sm.GetUpgrades + icmEpochTransition := sm.ComputeICMEpoch + + var newTimestamp time.Time + if hasChildBlock { + newTimestamp = childBlock.Timestamp() + timestamp = uint64(newTimestamp.UnixMilli()) + } + + icmEpochInfo := nextICMEpochInfo(parentMetadata, hasChildBlock, getUpgrades, icmEpochTransition, newTimestamp) + + return &StateMachineBlock{ + InnerBlock: childBlock, + Metadata: StateMachineMetadata{ + AuxiliaryInfo: auxInfo, + Timestamp: timestamp, + SimplexProtocolMetadata: simplexMetadata.Bytes(), + SimplexBlacklist: blacklist, + SimplexEpochInfo: newSimplexEpochInfo, + PChainHeight: pChainHeight, + ICMEpochInfo: icmEpochInfo, + }, + } +} + +// buildBlockEpochSealed builds a block where the epoch is being sealed due to a sealing block already created in this epoch. +func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, blacklist []byte) (*StateMachineBlock, error) { + // We check if the sealing block has already been finalized. + // If not, we build a Telock block. + + prevBlockSeq := simplexMetadata.Seq - 1 + sealingBlockSeq := parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq + + // If the sealing block sequence is still 0, it means previous block was the sealing block. + if sealingBlockSeq == 0 { + sealingBlockSeq = prevBlockSeq + } + + if sealingBlockSeq == 0 { + return nil, fmt.Errorf("cannot build epoch sealed block: sealing block sequence is 0 or undefined") + } + + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + NextPChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight, + SealingBlockSeq: sealingBlockSeq, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), + } + + _, finalization, err := sm.GetBlock(sealingBlockSeq, [32]byte{}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve sealing block at sequence %d: %w", sealingBlockSeq, err) + } + + isSealingBlockFinalized := finalization != nil + + if !isSealingBlockFinalized { + pChainHeight := parentBlock.Metadata.PChainHeight + return sm.wrapBlock(parentBlock, nil, newSimplexEpochInfo, pChainHeight, simplexMetadata, blacklist, nil), nil + } + + // Else, we build a block for the new epoch. + newSimplexEpochInfo = SimplexEpochInfo{ + // P-chain reference height is previous block's NextPChainReferenceHeight. + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight, + // The epoch number is the sequence of the sealing block. + EpochNumber: sealingBlockSeq, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), + } + + childBlock, err := sm.BlockBuilder.BuildBlock(ctx, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) + if err != nil { + return nil, err + } + + return sm.wrapBlock(parentBlock, childBlock, newSimplexEpochInfo, parentBlock.Metadata.PChainHeight, simplexMetadata, blacklist, nil), nil +} + +// constructSimplexZeroBlock constructs the SimplexEpochInfo for the zero block, which is the first ever block built by Simplex. +func constructSimplexZeroBlock(pChainHeight uint64, newValidatorSet NodeBLSMappings, prevVMBlockSeq uint64) SimplexEpochInfo { + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight, + EpochNumber: 1, + // We treat the zero block as a special case, and we encode in it the block validation descriptor, + // despite it not actually being a sealing block. This is because the zero block is the first block that introduces the validator set. + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: newValidatorSet, + }, + }, + NextEpochApprovals: nil, // We don't need to collect approvals to seal the first ever epoch. + PrevVMBlockSeq: prevVMBlockSeq, + SealingBlockSeq: 0, // We don't have a sealing block in the zero block. + PrevSealingBlockHash: [32]byte{}, // The zero block has no previous sealing block. + NextPChainReferenceHeight: 0, + } + return newSimplexEpochInfo +} + +func computeNewApprovals( + nextEpochApprovals *NextEpochApprovals, + auxInfo *AuxiliaryInfo, + approvalsFromPeers ValidatorSetApprovals, + pChainHeight uint64, + aggregator SignatureAggregator, + validators NodeBLSMappings, +) (*approvals, error) { + if nextEpochApprovals == nil { + nextEpochApprovals = &NextEpochApprovals{} + } + + oldApprovingNodes := bitmaskFromBytes(nextEpochApprovals.NodeIDs) + + // We map each validator to its relative index in the validator set. + nodeID2ValidatorIndex := make(map[nodeID]int, len(validators)) + for i, nbm := range validators { + nodeID2ValidatorIndex[nbm.NodeID] = i + } + + // We have the approvals obtained from peers, but we need to sanitize them by filtering out approvals that are not valid, + // such as approvals that do not agree with our candidate auxiliary info digest and P-Chain height, + // and approvals that are from nodes that are not in the validator set or have already approved in prior blocks. + approvalsFromPeers = sanitizeApprovals(approvalsFromPeers, pChainHeight, nodeID2ValidatorIndex, oldApprovingNodes) + + // Next we aggregate both previous and new approvals to compute the new aggregated signatures and the new bitmask of approving nodes. + aggregatedSignature, newApprovingNodes, err := computeNewApproverSignaturesAndSigners(nextEpochApprovals, approvalsFromPeers, oldApprovingNodes, nodeID2ValidatorIndex, aggregator) + if err != nil { + return nil, err + } + + // we check if we have enough approvals to seal the epoch by computing the relative approval ratio, + // which is the ratio of the total weight of approving nodes divided by the total weight of all validators. + canSeal, err := canSealBlock(validators, newApprovingNodes) + if err != nil { + return nil, err + } + + return &approvals{ + canSeal: canSeal, + signature: aggregatedSignature, + nodeIDs: newApprovingNodes.Bytes(), + }, nil +} + +// computeNewApproverSignaturesAndSigners computes the signatures of the nodes that approve the next epoch including the previous aggregated signature, +// and bitmask of nodes that correspond to those signatures, and aggregates all signatures together. +func computeNewApproverSignaturesAndSigners(nextEpochApprovals *NextEpochApprovals, approvalsFromPeers ValidatorSetApprovals, oldApprovingNodes bitmask, nodeID2ValidatorIndex map[nodeID]int, aggregator SignatureAggregator) ([]byte, bitmask, error) { + // Prepare the new signatures from the new approvals that haven't approved yet and that agree with our candidate auxiliary info digest and P-Chain height. + newSignatures := make([][]byte, 0, len(approvalsFromPeers)+1) + + // We will overwrite the old approving nodes with the new approving nodes, by turning on the bits for the new approvers. + newApprovingNodes := oldApprovingNodes.Clone() + + for _, approval := range approvalsFromPeers { + approvingNodeIndexOfNewApprover, exists := nodeID2ValidatorIndex[approval.NodeID] + if !exists { + // This should not happen, because we have already filtered approvals that are not in the validator set, but we check just in case. + continue + } + // Turn on the bit for the new approver + newApprovingNodes.Add(approvingNodeIndexOfNewApprover) + newSignatures = append(newSignatures, approval.Signature) + } + + // Add the existing signature into the list of signatures to aggregate + existingSignature := nextEpochApprovals.Signature + if existingSignature != nil { + newSignatures = append(newSignatures, existingSignature) + } + + // Finally, we aggregate all signatures together, to compute the new aggregated signature. + aggregatedSignature, err := aggregator.AggregateSignatures(newSignatures...) + if err != nil { + return nil, bitmask{}, fmt.Errorf("failed to aggregate signatures: %w", err) + } + + return aggregatedSignature, *newApprovingNodes, nil +} + +func canSealBlock(validators NodeBLSMappings, newApprovingNodes bitmask) (bool, error) { + approvingWeight, err := computeApprovingWeight(validators, &newApprovingNodes) + if err != nil { + return false, err + } + + totalWeight, err := computeTotalWeight(validators) + if err != nil { + return false, err + } + + threshold := big.NewRat(2, 3) + + approvingRatio := big.NewRat(approvingWeight, totalWeight) + + canSeal := approvingRatio.Cmp(threshold) > 0 + return canSeal, nil +} + +// sanitizeApprovals filters out approvals that are not valid by checking if they agree with our candidate auxiliary info digest and P-Chain height, +// and if they are from the validator set and haven't already been approved. +func sanitizeApprovals(approvals ValidatorSetApprovals, pChainHeight uint64, nodeID2ValidatorIndex map[nodeID]int, oldApprovingNodes bitmask) ValidatorSetApprovals { + filter1 := approvalsThatAgreeWithAuxInfoAndPChainHeight(pChainHeight) + filter2 := approvalsThatAreInValidatorSetAndHaveNotAlreadyApproved(oldApprovingNodes.Clone(), nodeID2ValidatorIndex) + return approvals.Filter(filter1).Filter(filter2).UniqueByNodeID() +} + +func approvalsThatAgreeWithAuxInfoAndPChainHeight(pChainHeight uint64) func(approval ValidatorSetApproval) bool { + return func(approval ValidatorSetApproval) bool { + // Pick only approvals that agree with our candidate auxiliary info digest and P-Chain height + return approval.PChainHeight == pChainHeight + } +} + +func approvalsThatAreInValidatorSetAndHaveNotAlreadyApproved(oldApprovingNodes *bitmask, nodeID2ValidatorIndex map[nodeID]int) func(approval ValidatorSetApproval) bool { + return func(approval ValidatorSetApproval) bool { + approvingNodeIndexOfNewApprover, exists := nodeID2ValidatorIndex[approval.NodeID] + if !exists { + // If the approving node is not in the validator set, we ignore this approval. + return false + } + // Only pick approvals from nodes that haven't already approved + return !oldApprovingNodes.Contains(approvingNodeIndexOfNewApprover) + } +} + +func computeApprovingWeight(validators NodeBLSMappings, approvingNodes *bitmask) (int64, error) { + var approvingWeight uint64 + for i, nbm := range validators { + if !approvingNodes.Contains(i) { + continue + } + sum, err := safeAdd(approvingWeight, nbm.Weight) + if err != nil { + return 0, fmt.Errorf("failed to compute approving weights: %w", err) + } + approvingWeight = sum + } + + if approvingWeight > math.MaxInt64 { + return 0, fmt.Errorf("approving weight of validators is too big, overflows int64: %d", approvingWeight) + } + + return int64(approvingWeight), nil +} + +func computeTotalWeight(validators NodeBLSMappings) (int64, error) { + totalWeight, err := validators.TotalWeight() + if err != nil { + return 0, fmt.Errorf("failed to sum weights of all nodes: %w", err) + } + + if totalWeight == 0 { + return 0, fmt.Errorf("total weight of validators is 0") + } + + if totalWeight > math.MaxInt64 { + return 0, fmt.Errorf("total weight of validators is too big, overflows int64: %d", totalWeight) + } + return int64(totalWeight), nil +} + +func computeICMEpochInfo(getUpgrades func() UpgradeConfig, icmEpochTransition ICMEpochTransition, parentMetadata StateMachineMetadata, parentTimestamp, childTimestamp time.Time) ICMEpoch { + upgrades := getUpgrades() + + icmEpoch := icmEpochTransition(upgrades, ICMEpochInput{ + ParentPChainHeight: parentMetadata.PChainHeight, + ParentTimestamp: parentTimestamp, + ChildTimestamp: childTimestamp, + ParentEpoch: ICMEpoch{ + EpochStartTime: parentMetadata.ICMEpochInfo.EpochStartTime, + EpochNumber: parentMetadata.ICMEpochInfo.EpochNumber, + PChainEpochHeight: parentMetadata.ICMEpochInfo.PChainEpochHeight, + }, + }) + return icmEpoch +} + + + +func nextICMEpochInfo(parentMetadata StateMachineMetadata, hasChildBlock bool, getUpgrades func() UpgradeConfig, icmEpochTransition ICMEpochTransition, newTimestamp time.Time) ICMEpochInfo { + icmEpochInfo := parentMetadata.ICMEpochInfo + + if hasChildBlock { + parentTimestamp := time.UnixMilli(int64(parentMetadata.Timestamp)) + icmEpoch := computeICMEpochInfo(getUpgrades, icmEpochTransition, parentMetadata, parentTimestamp, newTimestamp) + icmEpochInfo = ICMEpochInfo{ + EpochStartTime: icmEpoch.EpochStartTime, + EpochNumber: icmEpoch.EpochNumber, + PChainEpochHeight: icmEpoch.PChainEpochHeight, + } + } + return icmEpochInfo +} + +func findFirstSimplexBlock(getBlock BlockRetriever, endHeight uint64) (uint64, error) { + var haltError error + + // Make sure the bound passed to sort.Search is valid, to avoid illegal input caused by overflow. + if endHeight >= math.MaxInt { + endHeight = math.MaxInt - 1 + } + + firstSimplexBlock := sort.Search(int(endHeight+1), func(i int) bool { + if haltError != nil { + return true + } + block, _, err := getBlock(uint64(i), [32]byte{}) + if errors.Is(err, simplex.ErrBlockNotFound) { + return false + } + if err != nil { + haltError = fmt.Errorf("error retrieving block at height %d: %w", i, err) + return false + } + // The first Simplex block is such that its epoch info isn't the zero value. + return !block.Metadata.SimplexEpochInfo.IsZero() + }) + if haltError != nil { + return 0, haltError + } + + if uint64(firstSimplexBlock) > endHeight { + return 0, fmt.Errorf("no simplex blocks found in range [%d, %d]", 0, endHeight) + } + + return uint64(firstSimplexBlock), nil +} + +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. + if parentBlock.InnerBlock == nil { + return parentBlock.Metadata.SimplexEpochInfo.PrevVMBlockSeq + } + // Otherwise, it has an inner block, in which case it is the previous block sequence. + return prevBlockSeq +} + +type approvals struct { + canSeal bool + nodeIDs []byte + signature []byte +} + +func ensureNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(prev SimplexEpochInfo, next SimplexEpochInfo) error { + if prev.NextEpochApprovals == nil { + // Condition satisifed vacously. + return nil + } + // Else, prev.NextEpochApprovals is not nil. + // If next.NextEpochApprovals is nil, condition is not satisfied. + if next.NextEpochApprovals == nil { + return fmt.Errorf("previous block has next epoch approvals but proposed block doesn't have next epoch approvals") + } + + // Make sure that previous signers are still there. + prevSigners := bitmaskFromBytes(prev.NextEpochApprovals.NodeIDs) + nextSigners := bitmaskFromBytes(next.NextEpochApprovals.NodeIDs) + // Remove all bits in nextSigners from prevSigners + prevSigners.Difference(&nextSigners) + // If we have some bits left, it means there was a bit in prevSigners that wasn't in nextSigners + if prevSigners.Len() > 0 { + return fmt.Errorf("some signers from parent block are missing from next epoch approvals of proposed block") + } + return nil +} diff --git a/msm/msm_test.go b/msm/msm_test.go new file mode 100644 index 00000000..7a4a6131 --- /dev/null +++ b/msm/msm_test.go @@ -0,0 +1,1364 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "fmt" + "testing" + "time" + + "github.com/ava-labs/simplex" + "github.com/stretchr/testify/require" +) + +func TestMSMFirstBlockAfterGenesis(t *testing.T) { + validMD := simplex.ProtocolMetadata{ + Round: 0, + Seq: 1, + Epoch: 1, + Prev: genesisBlock.Digest(), + } + + for _, testCase := range []struct { + name string + md simplex.ProtocolMetadata + err string + configure func(*StateMachine, *testConfig) + mutateBlock func(*StateMachineBlock) + }{ + { + name: "correct information", + md: validMD, + }, + { + name: "trying to build a genesis block", + md: validMD, + mutateBlock: func(block *StateMachineBlock) { + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + require.NoError(t, err) + md.Seq = 0 + block.Metadata.SimplexProtocolMetadata = md.Bytes() + }, + err: "attempted to build a genesis inner block", + }, + { + name: "previous block not found", + md: validMD, + configure: func(_ *StateMachine, tc *testConfig) { + delete(tc.blockStore, 0) + }, + err: "failed to retrieve previous (0) inner block", + }, + { + name: "parent has no inner block", + md: validMD, + configure: func(_ *StateMachine, tc *testConfig) { + tc.blockStore[0] = &outerBlock{ + block: StateMachineBlock{}, + } + }, + err: "parent inner block (", + }, + { + name: "wrong epoch number", + md: validMD, + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.EpochNumber = 2 + }, + err: "invalid epoch number (2), should be 1", + }, + { + name: "P-chain height too big", + md: validMD, + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.PChainHeight = 110 + }, + err: "invalid P-chain height (110) is too big", + }, + { + name: "P-chain height smaller than parent", + md: validMD, + configure: func(_ *StateMachine, tc *testConfig) { + tc.blockStore[0] = &outerBlock{ + block: StateMachineBlock{ + InnerBlock: &InnerBlock{TS: time.Now(), Bytes: []byte{1, 2, 3}}, + Metadata: StateMachineMetadata{PChainHeight: 110}, + }, + } + }, + err: "invalid P-chain height (100) is smaller than parent InnerBlock's P-chain height (110)", + }, + { + name: "validator set retrieval fails", + md: validMD, + configure: func(_ *StateMachine, tc *testConfig) { + tc.validatorSetRetriever.err = fmt.Errorf("validator set unavailable") + }, + err: "failed to retrieve validator set", + }, + { + name: "nil BlockValidationDescriptor", + md: validMD, + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.BlockValidationDescriptor = nil + }, + err: "invalid BlockValidationDescriptor: should not be nil", + }, + { + name: "membership mismatch", + md: validMD, + configure: func(_ *StateMachine, tc *testConfig) { + tc.validatorSetRetriever.result = NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, + } + }, + err: "invalid BlockValidationDescriptor: should match validator set", + }, + { + name: "SimplexEpochInfo mismatch", + md: validMD, + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.PrevVMBlockSeq = 999 + }, + err: "invalid SimplexEpochInfo", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + sm1, testConfig1 := newStateMachine(t) + sm2, testConfig2 := newStateMachine(t) + + testConfig1.blockStore[0] = &outerBlock{ + block: genesisBlock, + } + + testConfig2.blockStore[0] = &outerBlock{ + block: genesisBlock, + } + + if testCase.configure != nil { + testCase.configure(sm2, testConfig2) + } + + block, err := sm1.BuildBlock(context.Background(), genesisBlock, testCase.md, simplex.Blacklist{}) + require.NoError(t, err) + require.NotNil(t, block) + + if testCase.mutateBlock != nil { + testCase.mutateBlock(block) + } + + err = sm2.VerifyBlock(context.Background(), block) + if testCase.err != "" { + require.ErrorContains(t, err, testCase.err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMSMBuildBlockRejectsZeroSeq(t *testing.T) { + sm, tc := newStateMachine(t) + tc.blockStore[0] = &outerBlock{block: genesisBlock} + + md := simplex.ProtocolMetadata{ + Round: 0, + Seq: 0, + Epoch: 1, + Prev: genesisBlock.Digest(), + } + + block, err := sm.BuildBlock(context.Background(), genesisBlock, md, simplex.Blacklist{}) + require.Nil(t, block) + require.ErrorContains(t, err, "invalid ProtocolMetadata sequence number: should be > 0, got 0") +} + +func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { + preSimplexParent := StateMachineBlock{ + InnerBlock: &InnerBlock{ + TS: time.Now(), + BlockHeight: 42, + Bytes: []byte{4, 5, 6}, + }, + // Zero-valued metadata means this is a pre-Simplex block or a genesis block. + // But since the height is 42, it can't be a genesis block, so it must be a pre-Simplex block. + Metadata: StateMachineMetadata{}, + } + + md := simplex.ProtocolMetadata{ + Round: 0, + Seq: 43, + Epoch: 1, + Prev: preSimplexParent.Digest(), + } + + sm1, testConfig1 := newStateMachine(t) + sm2, testConfig2 := newStateMachine(t) + + testConfig1.blockStore[42] = &outerBlock{block: preSimplexParent} + testConfig2.blockStore[42] = &outerBlock{block: preSimplexParent} + + testConfig1.blockBuilder.block = &InnerBlock{ + TS: time.Now(), + BlockHeight: 43, + Bytes: []byte{7, 8, 9}, + } + + block, err := sm1.BuildBlock(context.Background(), preSimplexParent, md, simplex.Blacklist{}) + require.NoError(t, err) + require.NotNil(t, block) + + require.NoError(t, sm2.VerifyBlock(context.Background(), block)) + + require.Equal(t, &StateMachineBlock{ + InnerBlock: &InnerBlock{ + TS: testConfig1.blockBuilder.block.Timestamp(), + BlockHeight: 43, + Bytes: []byte{7, 8, 9}, + }, + Metadata: StateMachineMetadata{ + Timestamp: uint64(testConfig1.blockBuilder.block.Timestamp().UnixMilli()), + PChainHeight: 100, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: 100, + EpochNumber: 1, + PrevVMBlockSeq: 42, + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: testConfig1.validatorSetRetriever.result, + }, + }, + }, + }, + }, block) +} + +func TestMSMNormalOp(t *testing.T) { + newPChainHeight := uint64(200) + newValidatorSet := NodeBLSMappings{ + {BLSKey: []byte{5}, Weight: 1}, {BLSKey: []byte{6}, Weight: 1}, {BLSKey: []byte{7}, Weight: 1}, + } + + for _, testCase := range []struct { + name string + setup func(*StateMachine, *testConfig) + mutateBlock func(*StateMachineBlock) + err string + expectedPChainHeight uint64 + expectedNextPChainRefHeight uint64 + }{ + { + name: "correct information", + expectedPChainHeight: 100, + }, + { + name: "trying to build a genesis block", + mutateBlock: func(block *StateMachineBlock) { + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + require.NoError(t, err) + md.Seq = 0 + block.Metadata.SimplexProtocolMetadata = md.Bytes() + }, + err: "attempted to build a genesis inner block", + }, + { + name: "previous block not found", + mutateBlock: func(block *StateMachineBlock) { + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + require.NoError(t, err) + md.Seq = 999 + block.Metadata.SimplexProtocolMetadata = md.Bytes() + }, + err: "failed to retrieve previous (998) inner block", + }, + { + name: "P-chain height too big", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.PChainHeight = 110 + }, + err: "invalid P-chain height (110) is too big", + }, + { + name: "P-chain height smaller than parent", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.PChainHeight = 0 + }, + err: "invalid P-chain height (0) is smaller than parent block's P-chain height (100)", + }, + { + name: "wrong epoch number", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.EpochNumber = 2 + }, + err: "expected epoch number to be 1 but got 2", + }, + { + name: "non-nil BlockValidationDescriptor", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.BlockValidationDescriptor = &BlockValidationDescriptor{} + }, + err: "failed to find first Simplex block", + }, + { + name: "non-zero sealing block seq", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.SealingBlockSeq = 5 + }, + err: "expected sealing block sequence number to be 0 but got 5", + }, + { + name: "wrong PChainReferenceHeight", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.PChainReferenceHeight = 50 + }, + err: "expected P-chain reference height to be 100 but got 50", + }, + { + name: "non-empty PrevSealingBlockHash", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.PrevSealingBlockHash = [32]byte{1, 2, 3} + }, + err: "expected prev sealing block hash of a non sealing block to be empty", + }, + { + name: "wrong PrevVMBlockSeq", + mutateBlock: func(block *StateMachineBlock) { + block.Metadata.SimplexEpochInfo.PrevVMBlockSeq = 999 + }, + err: "expected PrevVMBlockSeq to be", + }, + { + name: "validator set change detected", + setup: func(sm *StateMachine, tc *testConfig) { + tc.validatorSetRetriever.resultMap = map[uint64]NodeBLSMappings{ + newPChainHeight: newValidatorSet, + } + sm.GetPChainHeight = func() uint64 { return newPChainHeight } + }, + expectedPChainHeight: newPChainHeight, + expectedNextPChainRefHeight: newPChainHeight, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + chain := makeChain(t, 5, 10) + sm1, testConfig1 := newStateMachine(t) + sm2, testConfig2 := newStateMachine(t) + + for i, block := range chain { + testConfig1.blockStore[uint64(i)] = &outerBlock{block: block} + testConfig2.blockStore[uint64(i)] = &outerBlock{block: block} + } + + lastBlock := chain[len(chain)-1] + md, err := simplex.ProtocolMetadataFromBytes(lastBlock.Metadata.SimplexProtocolMetadata) + require.NoError(t, err) + + md.Seq++ + md.Round++ + md.Prev = lastBlock.Digest() + + var blacklist simplex.Blacklist + blacklist.NodeCount = 4 + + blockTime := lastBlock.InnerBlock.Timestamp().Add(time.Second) + + content := make([]byte, 10) + _, err = rand.Read(content) + require.NoError(t, err) + + testConfig1.blockBuilder.block = &InnerBlock{ + TS: blockTime, + BlockHeight: lastBlock.InnerBlock.Height(), + Bytes: content, + } + + if testCase.setup != nil { + testCase.setup(sm1, testConfig1) + testCase.setup(sm2, testConfig2) + } + + block1, err := sm1.BuildBlock(context.Background(), lastBlock, *md, blacklist) + require.NoError(t, err) + require.NotNil(t, block1) + + if testCase.mutateBlock != nil { + testCase.mutateBlock(block1) + } + + err = sm2.VerifyBlock(context.Background(), block1) + if testCase.err != "" { + require.ErrorContains(t, err, testCase.err) + return + } + require.NoError(t, err) + + require.Equal(t, &StateMachineBlock{ + InnerBlock: &InnerBlock{ + TS: blockTime, + BlockHeight: lastBlock.InnerBlock.Height(), + Bytes: content, + }, + Metadata: StateMachineMetadata{ + SimplexBlacklist: blacklist.Bytes(), + Timestamp: uint64(blockTime.UnixMilli()), + PChainHeight: testCase.expectedPChainHeight, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: 100, + EpochNumber: 1, + PrevVMBlockSeq: lastBlock.InnerBlock.Height(), + NextPChainReferenceHeight: testCase.expectedNextPChainRefHeight, + }, + }, + }, block1) + }) + } +} + +func TestMSMFullEpochLifecycle(t *testing.T) { + // Validator sets: epoch 1 uses validatorSet1, epoch 2 uses validatorSet2. + node1 := [20]byte{1} + node2 := [20]byte{2} + node3 := [20]byte{3} + + validatorSet1 := NodeBLSMappings{ + {NodeID: node1, BLSKey: []byte{1}, Weight: 1}, + {NodeID: node2, BLSKey: []byte{2}, Weight: 1}, + {NodeID: node3, BLSKey: []byte{3}, Weight: 1}, + } + validatorSet2 := NodeBLSMappings{ + {NodeID: node1, BLSKey: []byte{1}, Weight: 1}, + {NodeID: node2, BLSKey: []byte{4}, Weight: 1}, + {NodeID: node3, BLSKey: []byte{5}, Weight: 1}, + } + + pChainHeight1 := uint64(100) + pChainHeight2 := uint64(200) + + startTime := time.Now() + + nextBlock := func(height uint64) *InnerBlock { + return &InnerBlock{ + TS: startTime.Add(time.Duration(height) * time.Millisecond), + BlockHeight: height, + Bytes: []byte{byte(height)}, + } + } + + // ----- Step 0: Building on top of genesis or upgrading to Simplex----- + genesis := StateMachineBlock{ + InnerBlock: &InnerBlock{ + BlockHeight: 0, // Genesis block has height 0 + TS: startTime, + Bytes: []byte{0}, + }, + } + + notGenesis := StateMachineBlock{ + InnerBlock: &InnerBlock{ + BlockHeight: 42, + TS: startTime, + Bytes: []byte{0}, + }, + } + for _, testCase := range []struct { + name string + firstBlockBeforeSimplex StateMachineBlock + }{ + { + name: "building on top of genesis", + firstBlockBeforeSimplex: genesis, + }, + { + name: "upgrading to Simplex from pre-Simplex blocks", + firstBlockBeforeSimplex: notGenesis, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + + currentPChainHeight := pChainHeight1 + + getValidatorSet := func(height uint64) (NodeBLSMappings, error) { + if height >= pChainHeight2 { + return validatorSet2, nil + } + return validatorSet1, nil + } + getPChainHeight := func() uint64 { + return currentPChainHeight + } + + // Create fresh state machine instances for each iteration. + sm, tc := newStateMachine(t) + sm.GetValidatorSet = getValidatorSet + sm.GetPChainHeight = getPChainHeight + + smVerify, tcVerify := newStateMachine(t) + smVerify.GetValidatorSet = getValidatorSet + smVerify.GetPChainHeight = getPChainHeight + + // addBlock adds a block to both block stores so builder and verifier stay in sync. + addBlock := func(seq uint64, block StateMachineBlock, fin *simplex.Finalization) { + tc.blockStore[seq] = &outerBlock{block: block, finalization: fin} + tcVerify.blockStore[seq] = &outerBlock{block: block, finalization: fin} + } + + baseSeq := testCase.firstBlockBeforeSimplex.InnerBlock.Height() + addBlock(baseSeq, testCase.firstBlockBeforeSimplex, nil) + + aggr := &signatureAggregator{} + + // ----- Step 1: Build zero epoch block (first simplex block) ----- + tc.blockBuilder.block = nextBlock(1) + md := simplex.ProtocolMetadata{ + Seq: baseSeq + 1, + Round: 0, + Epoch: 1, + Prev: testCase.firstBlockBeforeSimplex.Digest(), + } + + block1, err := sm.BuildBlock(context.Background(), testCase.firstBlockBeforeSimplex, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(1), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(1 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight1, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq, + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: validatorSet1, + }, + }, + }, + }, + }, block1) + addBlock(md.Seq, *block1, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block1)) + + // After we build the first block, the StateMachine should consider it as the latest persisted height. + sm.LatestPersistedHeight = baseSeq + 1 + smVerify.LatestPersistedHeight = baseSeq + 1 + + // ----- Step 2: Build a normal block (no validator set change) ----- + tc.blockBuilder.block = nextBlock(2) + md = simplex.ProtocolMetadata{Seq: baseSeq + 2, Round: 1, Epoch: 1, Prev: block1.Digest()} + block2, err := sm.BuildBlock(context.Background(), *block1, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(2), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(2 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight1, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 1, + }, + }, + }, block2) + addBlock(md.Seq, *block2, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block2)) + + // ----- Step 3: Build a normal block that detects a validator set change ----- + // Advance P-chain height so that GetValidatorSet returns a different set. + currentPChainHeight = pChainHeight2 + + tc.blockBuilder.block = nextBlock(3) + md = simplex.ProtocolMetadata{Seq: baseSeq + 3, Round: 2, Epoch: 1, Prev: block2.Digest()} + block3, err := sm.BuildBlock(context.Background(), *block2, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(3), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(3 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 2, + NextPChainReferenceHeight: pChainHeight2, + }, + }, + }, block3) + addBlock(md.Seq, *block3, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block3)) + + // ----- Step 4: First collecting block (1/3 approvals, not enough to seal) ----- + + // Override ApprovalsRetriever to use our dynamic approvals. + var approvalsResult ValidatorSetApprovals + sm.ApprovalsRetriever = &dynamicApprovalsRetriever{approvals: &approvalsResult} + + approvalsResult = ValidatorSetApprovals{ + { + NodeID: node1, + PChainHeight: pChainHeight2, + Signature: []byte("sig1"), + }, + } + + // node1 is at index 0 in validatorSet2 → bitmask bit 0 → {1} + bitmask := []byte{1} + sig, err := aggr.AggregateSignatures([]byte("sig1")) + require.NoError(t, err) + + tc.blockBuilder.block = nextBlock(4) + md = simplex.ProtocolMetadata{Seq: baseSeq + 4, Round: 3, Epoch: 1, Prev: block3.Digest()} + block4, err := sm.BuildBlock(context.Background(), *block3, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(4), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(4 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 3, + NextPChainReferenceHeight: pChainHeight2, + NextEpochApprovals: &NextEpochApprovals{ + NodeIDs: bitmask, + Signature: sig, + }, + }, + AuxiliaryInfo: &AuxiliaryInfo{}, + }, + }, block4) + addBlock(md.Seq, *block4, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block4)) + + // ----- Step 5: Second collecting block (2/3 approvals, still not enough since threshold is strictly > 2/3) ----- + // Once block4 carries AuxiliaryInfo (with empty Info bytes), peers must agree on + // the digest of those bytes — sha256(nil) — instead of the all-zero digest. + emptyAuxDigest := sha256.Sum256(nil) + approvalsResult = ValidatorSetApprovals{ + { + NodeID: node2, + PChainHeight: pChainHeight2, + Signature: []byte("sig2"), + AuxInfoSeqDigest: emptyAuxDigest, + }, + } + + // node2 is at index 1 → bitmask bits 0,1 → {3} + sig, err = aggr.AggregateSignatures([]byte("sig2"), sig) + require.NoError(t, err) + bitmask = []byte{3} + + tc.blockBuilder.block = nextBlock(5) + md = simplex.ProtocolMetadata{Seq: baseSeq + 5, Round: 4, Epoch: 1, Prev: block4.Digest()} + block5, err := sm.BuildBlock(context.Background(), *block4, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(5), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(5 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 4, + NextPChainReferenceHeight: pChainHeight2, + NextEpochApprovals: &NextEpochApprovals{ + NodeIDs: bitmask, + Signature: sig, + }, + }, + AuxiliaryInfo: &AuxiliaryInfo{PrevAuxInfoSeq: baseSeq + 4}, + }, + }, block5) + addBlock(md.Seq, *block5, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block5)) + + // ----- Step 6: Sealing block (3/3 approvals, enough to seal) ----- + approvalsResult = ValidatorSetApprovals{ + { + NodeID: node3, + PChainHeight: pChainHeight2, + Signature: []byte("sig3"), + AuxInfoSeqDigest: emptyAuxDigest, + }, + } + + // node3 is at index 2 → bitmask bits 0,1,2 → {7} + sig6, err := aggr.AggregateSignatures([]byte("sig3"), sig) + require.NoError(t, err) + bitmask = []byte{7} + + tc.blockBuilder.block = nextBlock(6) + md = simplex.ProtocolMetadata{Seq: baseSeq + 6, Round: 5, Epoch: 1, Prev: block5.Digest()} + block6, err := sm.BuildBlock(context.Background(), *block5, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(6), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 5, + NextPChainReferenceHeight: pChainHeight2, + SealingBlockSeq: 0, + PrevSealingBlockHash: block1.Digest(), + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: validatorSet2, + }, + }, + NextEpochApprovals: &NextEpochApprovals{ + NodeIDs: bitmask, + Signature: sig6, + }, + }, + }, + }, block6) + addBlock(md.Seq, *block6, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block6)) + + sealingSeq := baseSeq + 6 // The sealing block's sequence (md.Seq from step 6) + + backupStoreTC := tc.blockStore.clone() + backupStoreTCVerify := tcVerify.blockStore.clone() + + for _, subTestCase := range []struct { + name string + setup func() + }{ + { + name: "sealing block not finalized yet", + setup: func() { + addBlock(sealingSeq, tc.blockStore[sealingSeq].block, nil) + }, + }, + { + name: "sealing block immediately finalized", + setup: func() { + addBlock(sealingSeq, tc.blockStore[sealingSeq].block, &simplex.Finalization{}) + }, + }, + } { + testName := fmt.Sprintf("%s-%s", testCase.name, subTestCase.name) + t.Run(testName, func(t *testing.T) { + tc.blockStore = backupStoreTC.clone() + sm.GetBlock = tc.blockStore.getBlock + tcVerify.blockStore = backupStoreTCVerify.clone() + smVerify.GetBlock = tcVerify.blockStore.getBlock + + subTestCase.setup() + + tc.blockBuilder.block = nextBlock(7) + md = simplex.ProtocolMetadata{Seq: baseSeq + 7, Round: 6, Epoch: 1, Prev: block6.Digest()} + + // If the sealing block isn't finalized yet, we expect to build a Telock. + // However, despite the fact that the block builder is willing to build a new block, + // a Telock shouldn't contain an inner block. + if tc.blockStore[sealingSeq].finalization == nil { + telock, err := sm.BuildBlock(context.Background(), *block6, md, simplex.Blacklist{}) + require.NoError(t, err) + + require.Equal(t, &StateMachineBlock{ + InnerBlock: nil, + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + NextPChainReferenceHeight: pChainHeight2, + PrevVMBlockSeq: baseSeq + 6, + SealingBlockSeq: sealingSeq, + }, + }, + }, telock) + + // Next, finalize the sealing block after we have built a Telock. + addBlock(sealingSeq, tc.blockStore[sealingSeq].block, &simplex.Finalization{}) + } + + // ----- Step 7: Build a new epoch block (sealing block is finalized) ----- + + block7, err := sm.BuildBlock(context.Background(), *block6, md, simplex.Blacklist{}) + require.NoError(t, err) + require.Equal(t, &StateMachineBlock{ + InnerBlock: nextBlock(7), + Metadata: StateMachineMetadata{ + Timestamp: uint64(startTime.Add(7 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexBlacklist: emptyBlacklistBytes, + SimplexEpochInfo: SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight2, + EpochNumber: sealingSeq, + PrevVMBlockSeq: baseSeq + 6, + }, + }, + }, block7) + addBlock(md.Seq, *block7, nil) + + require.NoError(t, smVerify.VerifyBlock(context.Background(), block7)) + }) + } + }) + } +} + +type dynamicApprovalsRetriever struct { + approvals *ValidatorSetApprovals +} + +func (d *dynamicApprovalsRetriever) RetrieveApprovals() ValidatorSetApprovals { + return *d.approvals +} + +func makeChain(t *testing.T, simplexStartHeight uint64, endHeight uint64) []StateMachineBlock { + startTime := time.Now().Add(-time.Duration(endHeight+2) * time.Second) + blocks := make([]StateMachineBlock, 0, endHeight+1) + var round, seq uint64 + for h := uint64(0); h <= endHeight; h++ { + index := len(blocks) + + if h == 0 { + blocks = append(blocks, genesisBlock) + continue + } + + if h < simplexStartHeight { + blocks = append(blocks, makeNonSimplexBlock(t, simplexStartHeight, startTime, h)) + continue + } + + seq = uint64(index) + + blocks = append(blocks, makeNormalSimplexBlock(t, index, blocks, startTime, h, round, seq)) + round++ + } + return blocks +} + +func makeNormalSimplexBlock(t *testing.T, index int, blocks []StateMachineBlock, start time.Time, h uint64, round uint64, seq uint64) StateMachineBlock { + content := make([]byte, 10) + _, err := rand.Read(content) + require.NoError(t, err) + + prev := genesisBlock.Digest() + if index > 0 { + prev = blocks[index-1].Digest() + } + + return StateMachineBlock{ + InnerBlock: &InnerBlock{ + TS: start.Add(time.Duration(h) * time.Second), + BlockHeight: h, + Bytes: []byte{1, 2, 3}, + }, + Metadata: StateMachineMetadata{ + PChainHeight: 100, + SimplexProtocolMetadata: (&simplex.ProtocolMetadata{ + Round: round, + Seq: seq, + Epoch: 1, + Prev: prev, + }).Bytes(), + SimplexEpochInfo: SimplexEpochInfo{ + PrevSealingBlockHash: [32]byte{}, + PChainReferenceHeight: 100, + EpochNumber: 1, + PrevVMBlockSeq: uint64(index), + }, + }, + } +} + +func makeNonSimplexBlock(t *testing.T, startHeight uint64, start time.Time, h uint64) StateMachineBlock { + content := make([]byte, 10) + _, err := rand.Read(content) + require.NoError(t, err) + + return StateMachineBlock{ + InnerBlock: &InnerBlock{ + TS: start.Add(time.Duration(h-startHeight) * time.Second), + BlockHeight: h, + Bytes: []byte{1, 2, 3}, + }, + } +} + +func TestIdentifyCurrentState(t *testing.T) { + bvd := &BlockValidationDescriptor{} + for _, tc := range []struct { + name string + input SimplexEpochInfo + expected state + }{ + { + name: "epoch 0 is first simplex block", + input: SimplexEpochInfo{EpochNumber: 0}, + expected: stateFirstSimplexBlock, + }, + { + name: "no next p-chain ref height means normal op", + input: SimplexEpochInfo{EpochNumber: 1, NextPChainReferenceHeight: 0}, + expected: stateBuildBlockNormalOp, + }, + { + name: "has sealing block seq means epoch sealed", + input: SimplexEpochInfo{EpochNumber: 1, NextPChainReferenceHeight: 100, SealingBlockSeq: 5}, + expected: stateBuildBlockEpochSealed, + }, + { + name: "has block validation descriptor means epoch sealed", + input: SimplexEpochInfo{EpochNumber: 1, NextPChainReferenceHeight: 100, BlockValidationDescriptor: bvd}, + expected: stateBuildBlockEpochSealed, + }, + { + name: "next p-chain ref height > 0 without sealing means collecting approvals", + input: SimplexEpochInfo{EpochNumber: 1, NextPChainReferenceHeight: 100}, + expected: stateBuildCollectingApprovals, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.input.CurrentState()) + }) + } +} + +func TestAreNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(t *testing.T) { + for _, tc := range []struct { + name string + prev SimplexEpochInfo + next SimplexEpochInfo + err string + }{ + { + name: "prev has nil approvals", + prev: SimplexEpochInfo{}, + next: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}}}, + }, + { + name: "next is superset of prev", + prev: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}}}, + next: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}}}, + }, + { + name: "next equals prev", + prev: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}}}, + next: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}}}, + }, + { + name: "next is missing a signer from prev", + prev: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}}}, + next: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}}}, + err: "some signers from parent block are missing", + }, + { + name: "prev has approvals but next has nil approvals", + prev: SimplexEpochInfo{NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}}}, + next: SimplexEpochInfo{}, + err: "previous block has next epoch approvals but proposed block doesn't have next epoch approvals", + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := ensureNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(tc.prev, tc.next) + if tc.err != "" { + require.ErrorContains(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestComputePrevVMBlockSeq(t *testing.T) { + t.Run("parent has no inner block", func(t *testing.T) { + parent := StateMachineBlock{ + InnerBlock: nil, + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{PrevVMBlockSeq: 42}}, + } + require.Equal(t, uint64(42), computePrevVMBlockSeq(parent, 100)) + }) + + t.Run("parent has inner block", func(t *testing.T) { + parent := StateMachineBlock{ + InnerBlock: &fakeVMBlock{height: 10}, + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{PrevVMBlockSeq: 42}}, + } + require.Equal(t, uint64(100), computePrevVMBlockSeq(parent, 100)) + }) +} + +func TestFindFirstSimplexBlock(t *testing.T) { + t.Run("found at height 3", func(t *testing.T) { + getBlock := func(seq uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + if seq < 3 { + return StateMachineBlock{}, nil, nil + } + return StateMachineBlock{ + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1}}, + }, nil, nil + } + result, err := findFirstSimplexBlock(getBlock, 5) + require.NoError(t, err) + require.Equal(t, uint64(3), result) + }) + + t.Run("no simplex blocks found", func(t *testing.T) { + getBlock := func(_ uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + return StateMachineBlock{}, nil, nil + } + _, err := findFirstSimplexBlock(getBlock, 5) + require.ErrorContains(t, err, "no simplex blocks found") + }) + + t.Run("block not found errors are skipped", func(t *testing.T) { + getBlock := func(seq uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + if seq < 2 { + return StateMachineBlock{}, nil, simplex.ErrBlockNotFound + } + return StateMachineBlock{ + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1}}, + }, nil, nil + } + result, err := findFirstSimplexBlock(getBlock, 5) + require.NoError(t, err) + require.Equal(t, uint64(2), result) + }) + + t.Run("retrieval error propagated", func(t *testing.T) { + getBlock := func(_ uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + return StateMachineBlock{}, nil, fmt.Errorf("disk error") + } + _, err := findFirstSimplexBlock(getBlock, 5) + require.ErrorContains(t, err, "disk error") + }) +} + +func TestComputeTotalWeight(t *testing.T) { + t.Run("valid weights", func(t *testing.T) { + validators := NodeBLSMappings{ + {Weight: 100}, + {Weight: 200}, + {Weight: 300}, + } + total, err := computeTotalWeight(validators) + require.NoError(t, err) + require.Equal(t, int64(600), total) + }) + + t.Run("zero total weight", func(t *testing.T) { + validators := NodeBLSMappings{{Weight: 0}} + _, err := computeTotalWeight(validators) + require.ErrorContains(t, err, "total weight of validators is 0") + }) + + t.Run("empty validators", func(t *testing.T) { + _, err := computeTotalWeight(NodeBLSMappings{}) + require.ErrorContains(t, err, "total weight of validators is 0") + }) +} + +func TestComputeApprovingWeight(t *testing.T) { + validators := NodeBLSMappings{ + {Weight: 100}, + {Weight: 200}, + {Weight: 300}, + } + + t.Run("all approving", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{7}) + weight, err := computeApprovingWeight(validators, &bm) + require.NoError(t, err) + require.Equal(t, int64(600), weight) + }) + + t.Run("partial approving", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{5}) + weight, err := computeApprovingWeight(validators, &bm) + require.NoError(t, err) + require.Equal(t, int64(400), weight) + }) + + t.Run("none approving", func(t *testing.T) { + bm := bitmaskFromBytes(nil) + weight, err := computeApprovingWeight(validators, &bm) + require.NoError(t, err) + require.Equal(t, int64(0), weight) + }) + + t.Run("single validator approving", func(t *testing.T) { + bm := bitmaskFromBytes([]byte{2}) + weight, err := computeApprovingWeight(validators, &bm) + require.NoError(t, err) + require.Equal(t, int64(200), weight) + }) +} + +func TestSanitizeApprovals(t *testing.T) { + node0 := nodeID{0} + node1 := nodeID{1} + node2 := nodeID{2} + node3 := nodeID{3} + + nodeID2Index := map[nodeID]int{ + node0: 0, + node1: 1, + node2: 2, + } + + t.Run("filters by p-chain height", func(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: node0, PChainHeight: 100}, + {NodeID: node1, PChainHeight: 200}, + } + oldApproving := bitmaskFromBytes(nil) + result := sanitizeApprovals(approvals, 100, nodeID2Index, oldApproving) + require.Len(t, result, 1) + require.Equal(t, node0, result[0].NodeID) + }) + + t.Run("filters out already approved", func(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: node0, PChainHeight: 100}, + {NodeID: node1, PChainHeight: 100}, + } + oldApproving := bitmaskFromBytes([]byte{1}) + result := sanitizeApprovals(approvals, 100, nodeID2Index, oldApproving) + require.Len(t, result, 1) + require.Equal(t, node1, result[0].NodeID) + }) + + t.Run("filters out nodes not in validator set", func(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: node3, PChainHeight: 100}, + {NodeID: node2, PChainHeight: 100}, + } + oldApproving := bitmaskFromBytes(nil) + result := sanitizeApprovals(approvals, 100, nodeID2Index, oldApproving) + require.Len(t, result, 1) + require.Equal(t, node2, result[0].NodeID) + }) + + t.Run("deduplicates by node ID", func(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: node0, PChainHeight: 100}, + {NodeID: node0, PChainHeight: 100}, + } + oldApproving := bitmaskFromBytes(nil) + result := sanitizeApprovals(approvals, 100, nodeID2Index, oldApproving) + require.Len(t, result, 1) + }) +} + +func TestIdentifyBlockType(t *testing.T) { + bvd := &BlockValidationDescriptor{} + + for _, tc := range []struct { + name string + nextMD StateMachineMetadata + prevMD StateMachineMetadata + prevSeq uint64 + expected BlockType + }{ + { + name: "next block has BlockValidationDescriptor", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{BlockValidationDescriptor: bvd}}, + prevMD: StateMachineMetadata{}, + expected: BlockTypeSealing, + }, + { + name: "prev is zero-epoch block (epoch 1, NextPChainReferenceHeight 0)", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + EpochNumber: 1, + NextPChainReferenceHeight: 0, + }}, + expected: BlockTypeNormal, + }, + { + name: "prev is sealing block and next epoch matches prevSeq", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 10}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + BlockValidationDescriptor: bvd, + EpochNumber: 1, + NextPChainReferenceHeight: 200, + }}, + prevSeq: 10, + expected: BlockTypeNewEpoch, + }, + { + name: "prev is sealing block and next epoch does not match prevSeq (Telock)", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + BlockValidationDescriptor: bvd, + EpochNumber: 1, + NextPChainReferenceHeight: 200, + }}, + prevSeq: 10, + expected: BlockTypeTelock, + }, + { + name: "same epoch with non-zero SealingBlockSeq (Telock)", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 5}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + EpochNumber: 5, + SealingBlockSeq: 8, + }}, + expected: BlockTypeTelock, + }, + { + name: "epoch number matches prev SealingBlockSeq (NewEpoch)", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 8}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + EpochNumber: 5, + SealingBlockSeq: 8, + }}, + expected: BlockTypeNewEpoch, + }, + { + name: "normal block in the middle of an epoch", + nextMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 5}}, + prevMD: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{ + EpochNumber: 5, + }}, + expected: BlockTypeNormal, + }, + } { + t.Run(tc.name, func(t *testing.T) { + result := IdentifyBlockType(tc.nextMD, tc.prevMD, tc.prevSeq) + require.Equal(t, tc.expected, result) + }) + } +} + +// concatAggregator concatenates signatures for easy verification in tests. +type concatAggregator struct{} + +func (concatAggregator) AggregateSignatures(sigs ...[]byte) ([]byte, error) { + return bytes.Join(sigs, nil), nil +} + +type failingAggregator struct{} + +func (failingAggregator) AggregateSignatures(sigs ...[]byte) ([]byte, error) { + return nil, fmt.Errorf("aggregation failed") +} + +func TestComputeNewApproverSignaturesAndSigners(t *testing.T) { + node0 := nodeID{0} + node1 := nodeID{1} + node2 := nodeID{2} + + nodeID2Index := map[nodeID]int{ + node0: 0, + node1: 1, + node2: 2, + } + + t.Run("new approvals with no previous", func(t *testing.T) { + prevApprovals := &NextEpochApprovals{} + oldApproving := bitmaskFromBytes(nil) + + peers := ValidatorSetApprovals{ + {NodeID: node0, Signature: []byte("sig0")}, + {NodeID: node1, Signature: []byte("sig1")}, + } + + aggSig, newApproving, err := computeNewApproverSignaturesAndSigners(prevApprovals, peers, oldApproving, nodeID2Index, concatAggregator{}) + require.NoError(t, err) + require.True(t, newApproving.Contains(0)) + require.True(t, newApproving.Contains(1)) + require.False(t, newApproving.Contains(2)) + require.Equal(t, []byte("sig0sig1"), aggSig) + }) + + t.Run("new approvals added to existing", func(t *testing.T) { + prevApprovals := &NextEpochApprovals{ + NodeIDs: []byte{1}, // bit 0 + Signature: []byte("existing"), + } + oldApproving := bitmaskFromBytes([]byte{1}) // node0 already approved + + peers := ValidatorSetApprovals{ + {NodeID: node2, Signature: []byte("sig2")}, + } + + aggSig, newApproving, err := computeNewApproverSignaturesAndSigners(prevApprovals, peers, oldApproving, nodeID2Index, concatAggregator{}) + require.NoError(t, err) + require.True(t, newApproving.Contains(0)) // preserved from old + require.True(t, newApproving.Contains(2)) // newly added + require.False(t, newApproving.Contains(1)) // not approved + require.Equal(t, []byte("sig2existing"), aggSig) + }) + + t.Run("no new approvals with existing signature", func(t *testing.T) { + prevApprovals := &NextEpochApprovals{ + NodeIDs: []byte{1}, + Signature: []byte("existing"), + } + oldApproving := bitmaskFromBytes([]byte{1}) + + aggSig, newApproving, err := computeNewApproverSignaturesAndSigners(prevApprovals, nil, oldApproving, nodeID2Index, concatAggregator{}) + require.NoError(t, err) + require.True(t, newApproving.Contains(0)) + require.Equal(t, []byte("existing"), aggSig) + }) + + t.Run("peer not in validator set is skipped", func(t *testing.T) { + prevApprovals := &NextEpochApprovals{} + oldApproving := bitmaskFromBytes(nil) + unknownNode := nodeID{99} + + peers := ValidatorSetApprovals{ + {NodeID: unknownNode, Signature: []byte("unknown")}, + {NodeID: node0, Signature: []byte("sig0")}, + } + + aggSig, newApproving, err := computeNewApproverSignaturesAndSigners(prevApprovals, peers, oldApproving, nodeID2Index, concatAggregator{}) + require.NoError(t, err) + require.True(t, newApproving.Contains(0)) + require.Equal(t, 1, newApproving.Len()) + require.Equal(t, []byte("sig0"), aggSig) + }) + + t.Run("aggregation error propagated", func(t *testing.T) { + prevApprovals := &NextEpochApprovals{} + oldApproving := bitmaskFromBytes(nil) + peers := ValidatorSetApprovals{ + {NodeID: node0, Signature: []byte("sig0")}, + } + + _, _, err := computeNewApproverSignaturesAndSigners(prevApprovals, peers, oldApproving, nodeID2Index, failingAggregator{}) + require.ErrorContains(t, err, "aggregation failed") + }) +} diff --git a/msm/multi_epoch_node_test.go b/msm/multi_epoch_node_test.go new file mode 100644 index 00000000..6b4ec955 --- /dev/null +++ b/msm/multi_epoch_node_test.go @@ -0,0 +1,442 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/ava-labs/simplex" + "github.com/stretchr/testify/require" +) + +// emptyAuxInfoDigest is the digest peers must agree with once a parent block +// carries AuxiliaryInfo whose Info bytes are empty/nil — which is what the +// test's auxiliaryInfoGenerator produces. +var emptyAuxInfoDigest = sha256.Sum256(nil) + +func TestStateMachineEpochTransition(t *testing.T) { + validatorSetRetriever := validatorSetRetriever{ + resultMap: map[uint64]NodeBLSMappings{ + 100: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 1, NodeID: [20]byte{2}}}, + 200: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 2, NodeID: [20]byte{2}}, + {BLSKey: []byte{3}, Weight: 1, NodeID: [20]byte{3}}}, + 300: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 2, NodeID: [20]byte{2}}, + {BLSKey: []byte{3}, Weight: 3, NodeID: [20]byte{3}}, {BLSKey: []byte{4}, Weight: 1, NodeID: [20]byte{4}}}, + }, + } + + var pChainHeight atomic.Uint64 + pChainHeight.Store(100) + node := newMultiEpochNode(t) + node.sm.GetValidatorSet = validatorSetRetriever.getValidatorSet + node.sm.GetPChainHeight = func() uint64 { + return pChainHeight.Load() + } + + // Create some blocks and finalize them, until we reach height 10 + for node.Height() < 10 { + node.act() + } + + // Next, we increase the P-Chain height, which should cause the node to update its validator set and move to the new epoch. + pChainHeight.Store(200) + + epoch := node.Epoch() + for node.Epoch() == epoch { + node.act() + if flipCoin() { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{1}, PChainHeight: 200, Signature: []byte{1}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 200, Signature: []byte{2}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } + } + + t.Log("Epoch:", node.Epoch()) + require.Greater(t, node.Epoch(), uint64(1)) + + // Finally, we increase the P-Chain height again, which should cause the node to update its validator set and move to the new epoch. + + pChainHeight.Store(300) + + epoch = node.Epoch() + for node.Epoch() == epoch { + node.act() + if flipCoin() { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 300, Signature: []byte{2}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{3}, PChainHeight: 300, Signature: []byte{3}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } + } + + t.Log("Epoch:", node.Epoch()) + require.Greater(t, node.Epoch(), epoch) +} + +func TestStateMachineEpochTransitionEmptyMempool(t *testing.T) { + validatorSetRetriever := validatorSetRetriever{ + resultMap: map[uint64]NodeBLSMappings{ + 100: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 1, NodeID: [20]byte{2}}}, + 200: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 2, NodeID: [20]byte{2}}, + {BLSKey: []byte{3}, Weight: 1, NodeID: [20]byte{3}}}, + 300: {{BLSKey: []byte{1}, Weight: 1, NodeID: [20]byte{1}}, {BLSKey: []byte{2}, Weight: 2, NodeID: [20]byte{2}}, + {BLSKey: []byte{3}, Weight: 3, NodeID: [20]byte{3}}, {BLSKey: []byte{4}, Weight: 1, NodeID: [20]byte{4}}}, + }, + } + + var pChainHeight uint64 = 100 + node := newMultiEpochNode(t) + node.sm.MaxBlockBuildingWaitTime = 100 * time.Millisecond + node.sm.GetValidatorSet = validatorSetRetriever.getValidatorSet + node.sm.GetPChainHeight = func() uint64 { + return pChainHeight + } + + // Create some blocks and finalize them, until we reach height 10 + for node.Height() < 10 { + node.act() + } + + // Next, we increase the P-Chain height, which should cause the node to update its validator set and move to the new epoch. + pChainHeight = 200 + + // However, we mark the mempool as empty, which should cause the node to wait until it sees a change in the P-Chain height, rather than building blocks on top of the old epoch. + node.mempoolEmpty = true + + // We build blocks until the sealing block is finalized. + for node.lastFinalizedBlock().Metadata.SimplexEpochInfo.BlockValidationDescriptor == nil { + node.act() + if flipCoin() { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{1}, PChainHeight: 200, Signature: []byte{1}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 200, Signature: []byte{2}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } + } + + node.mempoolEmpty = false + + // Build a new block and check that the node has transitioned to the new epoch, + // rather than building a block on top of the old epoch. + height := node.Height() + + for node.Height() == height { + node.act() + } + require.Greater(t, node.Epoch(), uint64(1)) + + t.Log("Epoch:", node.Epoch()) + + epoch := node.Epoch() + require.Greater(t, epoch, uint64(1)) + + // Finally, we increase the P-Chain height again, which should cause the node to update its validator set and move to the new epoch. + + pChainHeight = 300 + + for node.Height() < 30 { + node.act() + if flipCoin() { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 300, Signature: []byte{2}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []ValidatorSetApproval{{NodeID: [20]byte{3}, PChainHeight: 300, Signature: []byte{3}, AuxInfoSeqDigest: emptyAuxInfoDigest}}, + } + } + } + + t.Log("Epoch:", node.Epoch()) + require.Greater(t, node.Epoch(), epoch) + require.Equal(t, node.Height(), uint64(30)) +} + +type innerBlock struct { + InnerBlock + Prev [32]byte +} + +type blockState struct { + block StateMachineBlock + finalized bool +} + +type multiEpochNode struct { + t *testing.T + sm *StateMachine + mempoolEmpty bool + // blocks holds notarized blocks in order. Finalized blocks always form a + // prefix: all finalized entries precede all non-finalized entries. + blocks []blockState +} + +func (fn *multiEpochNode) WaitForProgress(ctx context.Context, pChainHeight uint64) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(10 * time.Millisecond): + if fn.sm.GetPChainHeight() != pChainHeight { + return nil + } + } + } +} + +func (fn *multiEpochNode) WaitForPendingBlock(ctx context.Context) { + if fn.mempoolEmpty { + <-ctx.Done() + return + } +} + +func newMultiEpochNode(t *testing.T) *multiEpochNode { + sm, _ := newStateMachine(t) + + fn := &multiEpochNode{ + t: t, + sm: sm, + } + + fn.sm.BlockBuilder = fn + fn.sm.PChainProgressListener = fn + + fn.sm.GetBlock = func(seq uint64, digest [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + if seq == 0 { + return genesisBlock, nil, nil + } + for _, bs := range fn.blocks { + match := bs.block.Digest() == digest + if !match { + md, err := simplex.ProtocolMetadataFromBytes(bs.block.Metadata.SimplexProtocolMetadata) + if err != nil { + return StateMachineBlock{}, nil, err + } + match = md.Seq == seq + } + if match { + var fin *simplex.Finalization + if bs.finalized { + fin = &simplex.Finalization{} + } + return bs.block, fin, nil + } + } + + require.Failf(t, "not found block", "height: %d", seq) + return StateMachineBlock{}, nil, fmt.Errorf("block not found") + } + + return fn +} + +// lastFinalizedBlock returns the most recently finalized block. +// Panics if nothing has been finalized. +func (fn *multiEpochNode) lastFinalizedBlock() StateMachineBlock { + for i := len(fn.blocks) - 1; i >= 0; i-- { + if fn.blocks[i].finalized { + return fn.blocks[i].block + } + } + panic("no finalized block") +} + +func (fn *multiEpochNode) Height() uint64 { + var count uint64 + for _, bs := range fn.blocks { + if bs.finalized { + count++ + } + } + return count +} + +func (fn *multiEpochNode) Epoch() uint64 { + return fn.blocks[len(fn.blocks)-1].block.Metadata.SimplexEpochInfo.EpochNumber +} + +// act randomly either finalizes a notarized block, builds and notarizes a new block, or does nothing. +func (fn *multiEpochNode) act() { + if fn.canFinalize() && flipCoin() { + fn.tryFinalizeNextBlock() + return + } + + if flipCoin() { + return + } + + fn.buildAndNotarizeBlock() +} + +func (fn *multiEpochNode) canFinalize() bool { + return fn.nextUnfinalizedIndex() < len(fn.blocks) +} + +func (fn *multiEpochNode) nextUnfinalizedIndex() int { + for i, bs := range fn.blocks { + if !bs.finalized { + return i + } + } + return len(fn.blocks) +} + +func (fn *multiEpochNode) tryFinalizeNextBlock() { + nextIndex := fn.nextUnfinalizedIndex() + + if fn.isNextBlockTelock(nextIndex) { + return + } + + fn.blocks[nextIndex].finalized = true + block := fn.blocks[nextIndex].block + + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + require.NoError(fn.t, err) + + fn.sm.LatestPersistedHeight = md.Seq + fn.t.Logf("Finalized block at height %d with epoch %d", md.Seq, block.Metadata.SimplexEpochInfo.EpochNumber) + + // If we just finalized a sealing block, trim trailing Telock blocks. + if block.Metadata.SimplexEpochInfo.BlockValidationDescriptor != nil { + fn.blocks = fn.blocks[:nextIndex+1] + fn.t.Logf("Trimmed notarized blocks, new length: %d", len(fn.blocks)) + } +} + +func (fn *multiEpochNode) isNextBlockTelock(nextIndex int) bool { + if nextIndex == 0 { + return false + } + return fn.blocks[nextIndex].block.Metadata.SimplexEpochInfo.SealingBlockSeq > 0 +} + +func (fn *multiEpochNode) buildAndNotarizeBlock() { + block := fn.buildBlock() + require.NoError(fn.t, fn.sm.VerifyBlock(context.Background(), block)) + + fn.blocks = append(fn.blocks, blockState{block: *block}) +} + +func (fn *multiEpochNode) buildBlock() *StateMachineBlock { + parentBlock := fn.getParentBlock() + + lastMD, prevBlockDigest := fn.prepareMetadataAndPrevBlockDigest() + + _, finalization, err := fn.sm.GetBlock(lastMD.Seq, prevBlockDigest) + require.NoError(fn.t, err) + + finalizedString := "not finalized" + if finalization != nil { + finalizedString = "finalized" + } + + fn.t.Logf("Building a block on top of %s parent with epoch %d", finalizedString, parentBlock.Metadata.SimplexEpochInfo.EpochNumber) + + block, err := fn.sm.BuildBlock(context.Background(), parentBlock, simplex.ProtocolMetadata{ + Seq: lastMD.Seq + 1, + Round: lastMD.Round + 1, + Prev: prevBlockDigest, + }, simplex.Blacklist{}) + require.NoError(fn.t, err) + + return block +} + +func (fn *multiEpochNode) prepareMetadataAndPrevBlockDigest() (*simplex.ProtocolMetadata, [32]byte) { + var lastMD *simplex.ProtocolMetadata + var err error + lastBlockDigest := genesisBlock.Digest() + if len(fn.blocks) > 0 { + lastBlock := fn.blocks[len(fn.blocks)-1].block + lastBlockDigest = lastBlock.Digest() + lastMD, err = simplex.ProtocolMetadataFromBytes(lastBlock.Metadata.SimplexProtocolMetadata) + require.NoError(fn.t, err) + } else { + lastMD = &simplex.ProtocolMetadata{ + Prev: lastBlockDigest, + } + } + return lastMD, lastBlockDigest +} + +func (fn *multiEpochNode) BuildBlock(context.Context, uint64) (VMBlock, error) { + // Count the number of inner blocks in the chain + var count int + for _, bs := range fn.blocks { + if bs.block.InnerBlock != nil { + count++ + } + } + + vmBlock := &innerBlock{ + Prev: fn.getLastVMBlockDigest(), + InnerBlock: InnerBlock{ + Bytes: randomBuff(10), + TS: time.Now(), + BlockHeight: uint64(count), + }, + } + return vmBlock, nil +} + +func (fn *multiEpochNode) getParentBlock() StateMachineBlock { + if len(fn.blocks) > 0 { + return fn.blocks[len(fn.blocks)-1].block + } + gb := genesisBlock.InnerBlock.(*InnerBlock) + return StateMachineBlock{ + InnerBlock: &innerBlock{ + InnerBlock: *gb, + }, + } +} + +func (fn *multiEpochNode) getLastVMBlockDigest() [32]byte { + for i := len(fn.blocks) - 1; i >= 0; i-- { + if fn.blocks[i].block.InnerBlock != nil { + return fn.blocks[i].block.Digest() + } + } + return genesisBlock.Digest() +} + +func randomBuff(n int) []byte { + buff := make([]byte, n) + _, err := rand.Read(buff) + if err != nil { + panic(err) + } + return buff +} + +func flipCoin() bool { + buff := make([]byte, 1) + _, err := rand.Read(buff) + if err != nil { + panic(err) + } + + lsb := buff[0] & 1 + + return lsb == 1 +} diff --git a/msm/verification.go b/msm/verification.go new file mode 100644 index 00000000..4510f0ac --- /dev/null +++ b/msm/verification.go @@ -0,0 +1,555 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "encoding/binary" + "fmt" + "time" + + "github.com/ava-labs/simplex" +) + +type verificationInput struct { + prevMD StateMachineMetadata + proposedBlockMD StateMachineMetadata + hasInnerBlock bool + innerBlockTimestamp time.Time // only set when hasInnerBlock is true + prevBlockSeq uint64 + prevBlockHash [32]byte + nextBlockType BlockType + state state +} + +type verifier interface { + Verify(in verificationInput) error +} +type validationDescriptorVerifier struct { + getValidatorSet ValidatorSetRetriever +} + +func (vd *validationDescriptorVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + switch in.nextBlockType { + case BlockTypeSealing: + return vd.verifySealingBlock(prev, next) + default: + return vd.verifyEmptyValidationDescriptor(prev, next) + } +} + +func (vd *validationDescriptorVerifier) verifySealingBlock(prev SimplexEpochInfo, next SimplexEpochInfo) error { + validators, err := vd.getValidatorSet(prev.NextPChainReferenceHeight) + if err != nil { + return err + } + + if next.BlockValidationDescriptor == nil { + return fmt.Errorf("validation descriptor should not be nil for a sealing block") + } + + if !validators.Equal(next.BlockValidationDescriptor.AggregatedMembership.Members) { + return fmt.Errorf("expected validator set specified at P-chain height %d does not match validator set encoded in new block", next.NextPChainReferenceHeight) + } + + return nil +} + +func (vd *validationDescriptorVerifier) verifyEmptyValidationDescriptor(_ SimplexEpochInfo, next SimplexEpochInfo) error { + if next.BlockValidationDescriptor != nil { + return fmt.Errorf("block validation descriptor should be nil but got %v", next.BlockValidationDescriptor) + } + return nil +} + +type nextEpochApprovalsVerifier struct { + sigVerifier SignatureVerifier + getValidatorSet ValidatorSetRetriever + keyAggregator KeyAggregator +} + +func (nv *nextEpochApprovalsVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + switch in.nextBlockType { + case BlockTypeSealing: + return nv.verifySealingBlock(prev, next, in.proposedBlockMD.AuxiliaryInfo) + case BlockTypeNormal: + return nv.verifyNormal(prev, next, in.proposedBlockMD.AuxiliaryInfo) + default: + return nv.verifyEmptyNextEpochApprovals(prev, next) + } +} + +func (nv *nextEpochApprovalsVerifier) verifySealingBlock(prev SimplexEpochInfo, next SimplexEpochInfo, auxInfo *AuxiliaryInfo) error { + if next.NextEpochApprovals == nil { + return fmt.Errorf("next epoch approvals should not be nil for a sealing block") + } + + validators, err := nv.getValidatorSet(prev.NextPChainReferenceHeight) + if err != nil { + return err + } + + err = nv.verifySignature(prev, next, auxInfo, validators) + if err != nil { + return err + } + + approvingNodes := bitmaskFromBytes(next.NextEpochApprovals.NodeIDs) + canSeal, err := canSealBlock(validators, approvingNodes) + if err != nil { + return err + } + + if !canSeal { + return fmt.Errorf("not enough approvals to seal block") + } + + return nil +} + +func (nv *nextEpochApprovalsVerifier) verifyNormal(prev SimplexEpochInfo, next SimplexEpochInfo, auxInfo *AuxiliaryInfo) error { + if prev.NextPChainReferenceHeight == 0 { + return nil + } + + // Otherwise, prev.NextPChainReferenceHeight > 0, so this means we're collecting approvals + + if next.NextEpochApprovals == nil { + // The node that proposed the block should have included at least its own approval. + return fmt.Errorf("next epoch approvals should not be nil when collecting approvals") + } + + validators, err := nv.getValidatorSet(prev.NextPChainReferenceHeight) + if err != nil { + return err + } + + err = nv.verifySignature(prev, next, auxInfo, validators) + if err != nil { + return err + } + + // A node cannot remove other nodes' approvals, only add its own approval if it wasn't included in the previous block. + // So the set of signers in next.NextEpochApprovals should be a superset of the set of signers in prev.NextEpochApprovals. + if err := areNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(prev, next); err != nil { + return err + } + + return nil +} + +func (nv *nextEpochApprovalsVerifier) verifyEmptyNextEpochApprovals(_ SimplexEpochInfo, next SimplexEpochInfo) error { + if next.NextEpochApprovals != nil { + return fmt.Errorf("next epoch approvals should be nil but got %v", next.NextEpochApprovals) + } + return nil +} + +func (nv *nextEpochApprovalsVerifier) verifySignature(prev SimplexEpochInfo, next SimplexEpochInfo, auxinfo *AuxiliaryInfo, validators NodeBLSMappings) error { + // First figure out which validators are approving the next epoch by looking at the bitmask of approving nodes, + // and then aggregate their public keys together to verify the signature. + + nodeIDsBitmask := next.NextEpochApprovals.NodeIDs + aggPK, err := nv.aggregatePubKeysForBitmask(nodeIDsBitmask, validators) + if err != nil { + return err + } + + message := pChainNextReferenceHeightAsBytes(prev) + + if err := nv.sigVerifier.VerifySignature(next.NextEpochApprovals.Signature, message, aggPK); err != nil { + return fmt.Errorf("failed to verify signature: %w", err) + } + return nil +} + +func (nv *nextEpochApprovalsVerifier) aggregatePubKeysForBitmask(nodeIDsBitmask []byte, validators NodeBLSMappings) ([]byte, error) { + approvingNodes := bitmaskFromBytes(nodeIDsBitmask) + publicKeys := make([][]byte, 0, len(validators)) + for i, nbm := range validators { + if !approvingNodes.Contains(i) { + continue + } + publicKeys = append(publicKeys, nbm.BLSKey) + } + + aggPK, err := nv.keyAggregator.AggregateKeys(publicKeys...) + if err != nil { + return nil, fmt.Errorf("failed to aggregate public keys: %w", err) + } + return aggPK, nil +} + +func pChainNextReferenceHeightAsBytes(prev SimplexEpochInfo) []byte { + pChainHeight := prev.NextPChainReferenceHeight + pChainHeightBuff := make([]byte, 8) + binary.BigEndian.PutUint64(pChainHeightBuff, pChainHeight) + return pChainHeightBuff +} + +type nextPChainReferenceHeightVerifier struct { + getValidatorSet ValidatorSetRetriever + getPChainHeight func() uint64 +} + +func (n *nextPChainReferenceHeightVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + switch in.nextBlockType { + case BlockTypeTelock, BlockTypeSealing: + if prev.NextPChainReferenceHeight != next.NextPChainReferenceHeight { + return fmt.Errorf("expected P-chain reference height to be %d but got %d", prev.NextPChainReferenceHeight, next.NextPChainReferenceHeight) + } + case BlockTypeNormal: + return n.verifyNextPChainRefHeightNormal(in.prevMD, prev, next) + case BlockTypeNewEpoch: + if next.NextPChainReferenceHeight != 0 { + return fmt.Errorf("expected P-chain reference height to be 0 but got %d", next.NextPChainReferenceHeight) + } + default: + return fmt.Errorf("unknown block type: %d", in.nextBlockType) + } + return nil +} + +func (n *nextPChainReferenceHeightVerifier) verifyNextPChainRefHeightNormal(prevMD StateMachineMetadata, prev SimplexEpochInfo, next SimplexEpochInfo) error { + // Next P-chain height can only increase, not decrease. + if next.NextPChainReferenceHeight > 0 && prev.PChainReferenceHeight > next.NextPChainReferenceHeight { + return fmt.Errorf("expected P-chain reference height to be non-decreasing, "+ + "but the previous P-chain reference height is %d and the proposed P-chain reference height is %d", prev.PChainReferenceHeight, next.NextPChainReferenceHeight) + } + + // If the previous block already has a next P-chain reference height, + // we should keep the same next P-chain reference height until we reach it. + if prev.NextPChainReferenceHeight > 0 { + if next.NextPChainReferenceHeight != prev.NextPChainReferenceHeight { + return fmt.Errorf("expected P-chain reference height to be %d but got %d", prev.NextPChainReferenceHeight, next.NextPChainReferenceHeight) + } + return nil + } + + // If we reached here, then prev.NextPChainReferenceHeight == 0. + // It might be that this block is the first block that has set the next P-chain reference height for the epoch, + // so check if it has done so correctly by observing whether the validator set has indeed changed. + + currentValidatorSet, err := n.getValidatorSet(prevMD.SimplexEpochInfo.PChainReferenceHeight) + if err != nil { + return err + } + + newValidatorSet, err := n.getValidatorSet(next.NextPChainReferenceHeight) + if err != nil { + return err + } + + // If the validator set doesn't change, we shouldn't have increased the next P-chain reference height. + if currentValidatorSet.Equal(newValidatorSet) && next.NextPChainReferenceHeight > 0 { + return fmt.Errorf("validator set at proposed next P-chain reference height %d is the same as "+ + "validator set at previous block's P-chain reference height %d,"+ + "so expected next P-chain reference height to remain the same but got %d", + next.NextPChainReferenceHeight, prev.PChainReferenceHeight, next.NextPChainReferenceHeight) + } + + // Else, either the validator set has changed, or the next P-chain reference height is still 0. + // Both of these cases are fine, but we should verify that we have observed the next P-chain reference height if it is > 0. + + pChainHeight := n.getPChainHeight() + + if pChainHeight < next.NextPChainReferenceHeight { + return fmt.Errorf("haven't reached P-chain height %d yet, current P-chain height is only %d", next.NextPChainReferenceHeight, pChainHeight) + } + + return nil +} + +type epochNumberVerifier struct{} + +func (e *epochNumberVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + // An epoch number of 0 means this is not a Simplex block, so the next block should be the first Simplex block with epoch number 1. + if in.prevMD.SimplexEpochInfo.EpochNumber == 0 && in.proposedBlockMD.SimplexEpochInfo.EpochNumber != 1 { + return fmt.Errorf("expected epoch number of the first block created to be 1 but got %d", next.EpochNumber) + } + + // The only time in which we should increase the epoch number is when we have a block that marks the start of a new epoch. + switch in.nextBlockType { + case BlockTypeNewEpoch: + // TODO: we have to make sure that Telocks are pruned before moving to a new epoch, otherwise we hit a false negative below. + if in.prevBlockSeq != next.EpochNumber { + return fmt.Errorf("expected epoch number to be %d but got %d", in.prevBlockSeq, next.EpochNumber) + } + default: + if prev.EpochNumber != next.EpochNumber { + return fmt.Errorf("expected epoch number to be %d but got %d", prev.EpochNumber, next.EpochNumber) + } + } + return nil +} + +type sealingBlockSeqVerifier struct{} + +func (s *sealingBlockSeqVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + // A block should only have a sealing block if it is a Telock. + switch in.nextBlockType { + case BlockTypeNewEpoch, BlockTypeNormal, BlockTypeSealing: + if next.SealingBlockSeq != 0 { + return fmt.Errorf("expected sealing block sequence number to be 0 but got %d", next.SealingBlockSeq) + } + case BlockTypeTelock: + // This is not the first Telock, make sure the sealing block sequence number doesn't change. + + // prev.SealingBlockSeq > 0 means the previous block is a Telock. + if prev.SealingBlockSeq > 0 && next.SealingBlockSeq != prev.SealingBlockSeq { + return fmt.Errorf("expected sealing block sequence number to be %d but got %d", prev.SealingBlockSeq, next.SealingBlockSeq) + } + + // Else, either this is the first Telock, or the previous block's sealing block sequence is equal to this block's sealing block sequence. + + // We need to check the first case has a valid sealing block sequence, as the second case is fine by definition. + if prev.BlockValidationDescriptor != nil { + md, err := simplex.ProtocolMetadataFromBytes(in.prevMD.SimplexProtocolMetadata) + if err != nil { + return fmt.Errorf("failed parsing protocol metadata: %w", err) + } + if next.SealingBlockSeq != md.Seq { + return fmt.Errorf("expected sealing block sequence number to be %d but got %d", md.Seq, next.SealingBlockSeq) + } + } + default: + return fmt.Errorf("unknown block type: %d", in.nextBlockType) + } + + return nil +} + +type pChainHeightVerifier struct { + getPChainHeight func() uint64 +} + +func (p *pChainHeightVerifier) Verify(in verificationInput) error { + currentPChainHeight := p.getPChainHeight() + + if in.proposedBlockMD.PChainHeight > currentPChainHeight { + return fmt.Errorf("invalid P-chain height (%d) is too big, expected to be ≤ %d", + in.proposedBlockMD.PChainHeight, currentPChainHeight) + } + + if in.prevMD.PChainHeight > in.proposedBlockMD.PChainHeight { + return fmt.Errorf("invalid P-chain height (%d) is smaller than parent block's P-chain height (%d)", + in.proposedBlockMD.PChainHeight, in.prevMD.PChainHeight) + } + + return nil +} + +type pChainReferenceHeightVerifier struct{} + +func (p *pChainReferenceHeightVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + switch in.nextBlockType { + case BlockTypeNewEpoch: + if prev.NextPChainReferenceHeight != next.PChainReferenceHeight { + return fmt.Errorf("expected P-chain reference height of the first block of epoch %d to be %d but got %d", + prev.SealingBlockSeq, prev.NextPChainReferenceHeight, next.PChainReferenceHeight) + } + default: + if prev.PChainReferenceHeight != next.PChainReferenceHeight { + return fmt.Errorf("expected P-chain reference height to be %d but got %d", prev.PChainReferenceHeight, next.PChainReferenceHeight) + } + } + + return nil +} + +type icmEpochInfoVerifier struct { + getUpdates func() UpgradeConfig + computeICMEpoch ICMEpochTransition +} + +func (i *icmEpochInfoVerifier) Verify(in verificationInput) error { + prevMD, nextMD := in.prevMD, in.proposedBlockMD + + timestamp := time.UnixMilli(int64(in.prevMD.Timestamp)) + if in.hasInnerBlock { + timestamp = in.innerBlockTimestamp + } + + expectedICMInfo := nextICMEpochInfo(prevMD, in.hasInnerBlock, i.getUpdates, i.computeICMEpoch, timestamp) + + if !expectedICMInfo.Equal(&nextMD.ICMEpochInfo) { + return fmt.Errorf("expected ICM epoch info to be %v but got %v", expectedICMInfo, nextMD.ICMEpochInfo) + } + + return nil +} + +type timestampVerifier struct { + getTime func() time.Time + timeSkewLimit time.Duration +} + +func (t *timestampVerifier) Verify(in verificationInput) error { + if !in.hasInnerBlock { + // If no inner block, the timestamp is inherited from the parent block. + if in.proposedBlockMD.Timestamp != in.prevMD.Timestamp { + return fmt.Errorf("block without inner block should inherit parent timestamp %d but got %d", in.prevMD.Timestamp, in.proposedBlockMD.Timestamp) + } + } else { + // If there is an inner block, the timestamp should be the same as the inner block's timestamp. + if in.proposedBlockMD.Timestamp != uint64(in.innerBlockTimestamp.UnixMilli()) { + return fmt.Errorf("block timestamp %d does not match inner block timestamp %d", in.proposedBlockMD.Timestamp, in.innerBlockTimestamp.UnixMilli()) + } + } + + timestamp := time.UnixMilli(int64(in.proposedBlockMD.Timestamp)) + + currentTime := t.getTime() + if currentTime.Add(t.timeSkewLimit).Before(timestamp) { + return fmt.Errorf("proposed block timestamp is too far in the future, current time is %v but got %v", currentTime, timestamp) + } + + if in.prevMD.Timestamp > in.proposedBlockMD.Timestamp { + return fmt.Errorf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", in.prevMD.Timestamp, in.proposedBlockMD.Timestamp) + } + return nil +} + +type prevSealingBlockHashVerifier struct { + getBlock BlockRetriever + latestPersistedHeight *uint64 +} + +func (p *prevSealingBlockHashVerifier) Verify(in verificationInput) error { + prev, _ := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + // Sealing block of the first epoch must point to the first ever Simplex block as the previous sealing block. + if prev.EpochNumber == 1 && in.nextBlockType == BlockTypeSealing { + firstEverSimplexBlockSeq, err := findFirstSimplexBlock(p.getBlock, *p.latestPersistedHeight+1) + if err != nil { + return fmt.Errorf("failed to find first Simplex block: %w", err) + } + + block, _, err := p.getBlock(firstEverSimplexBlockSeq, [32]byte{}) + if err != nil { + return fmt.Errorf("failed retrieving first ever simplex block %d: %w", firstEverSimplexBlockSeq, err) + } + + hash := block.Digest() + if !bytes.Equal(in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash[:], hash[:]) { + return fmt.Errorf("expected prev sealing block hash of the first ever simplex block to be %x but got %x", hash, in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash) + } + + return nil + } + + // Otherwise, we can only have a previous sealing block hash if this is a sealing block, + // and in that case, the previous sealing block hash should match the hash of the sealing block of the previous epoch. + + switch in.nextBlockType { + case BlockTypeSealing: + prevSealingBlock, _, err := p.getBlock(in.prevMD.SimplexEpochInfo.EpochNumber, [32]byte{}) + if err != nil { + return fmt.Errorf("failed retrieving block: %w", err) + } + hash := prevSealingBlock.Digest() + if !bytes.Equal(in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash[:], hash[:]) { + return fmt.Errorf("expected prev sealing block hash to be %x but got %x", hash, in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash) + } + default: // non-sealing blocks should have an empty previous sealing block hash + if in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash != [32]byte{} { + return fmt.Errorf("expected prev sealing block hash of a non sealing block to be empty but got %x", in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash) + } + } + + return nil +} + +type vmBlockSeqVerifier struct { + getBlock BlockRetriever +} + +func (v *vmBlockSeqVerifier) Verify(in verificationInput) error { + prev, next := in.prevMD.SimplexEpochInfo, in.proposedBlockMD.SimplexEpochInfo + + // If this is the first ever Simplex block, the PrevVMBlockSeq is simply the seq of the previous block. + if prev.EpochNumber == 0 { + if next.PrevVMBlockSeq != in.prevBlockSeq { + return fmt.Errorf("expected PrevVMBlockSeq to be %d but got %d", in.prevBlockSeq, next.PrevVMBlockSeq) + } + return nil + } + + // Else, if the previous block has an inner block, we point to it. + // Otherwise, we point to the parent block's previous VM block seq. + prevBlock, _, err := v.getBlock(in.prevBlockSeq, in.prevBlockHash) + if err != nil { + return fmt.Errorf("failed retrieving block: %w", err) + } + + expectedPrevVMBlockSeq := in.prevMD.SimplexEpochInfo.PrevVMBlockSeq + + if prevBlock.InnerBlock != nil { + expectedPrevVMBlockSeq = in.prevBlockSeq + } + + if next.PrevVMBlockSeq != expectedPrevVMBlockSeq { + return fmt.Errorf("expected PrevVMBlockSeq to be %d but got %d", expectedPrevVMBlockSeq, next.PrevVMBlockSeq) + } + + return nil +} + +type AuxInfoVerifier struct { + auxiliaryInfoVerifier AuxiliaryInfoVerifier + getBlock BlockRetriever +} + +func (a *AuxInfoVerifier) Verify(in verificationInput) error { + // We want to have an auxiliary info only if: + // 1) We haven't sealed the block in this block (block type is normal) + // 2) We are collecting approvals for the next epoch + // 3) The previous block has set a next P-chain reference height, + // which means this isn't the first block that did so. + // This is needed to ensure nodes won't contribute auxiliary info in the block that transition + // from stateBuildBlockNormalOp to stateBuildCollectingApprovals, to make the code simpler. + if in.nextBlockType == BlockTypeNormal && in.state == stateBuildCollectingApprovals && in.prevMD.SimplexEpochInfo.NextPChainReferenceHeight > 0 { + if in.proposedBlockMD.AuxiliaryInfo == nil { + return fmt.Errorf("auxiliary info should not be nil when collecting approvals") + } + // Next, verify the auxiliary info is correct. + + // We first need to collect all auxiliary info in this epoch so far. + var prevAuxInfos []*AuxiliaryInfo + prevAuxInf := in.prevMD.AuxiliaryInfo + for prevAuxInf != nil { + prevAuxInfos = append(prevAuxInfos, prevAuxInf) + + } + } else if in.proposedBlockMD.AuxiliaryInfo != nil { + return fmt.Errorf("auxiliary info should be nil when not collecting approvals") + } + return nil +} + +func areNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(prev SimplexEpochInfo, next SimplexEpochInfo) error { + if prev.NextEpochApprovals == nil { + return nil + } + // Make sure that previous signers are still there. + prevSigners := bitmaskFromBytes(prev.NextEpochApprovals.NodeIDs) + nextSigners := bitmaskFromBytes(next.NextEpochApprovals.NodeIDs) + // Remove all bits in nextSigners from prevSigners + prevSigners.Difference(&nextSigners) + // If we have some bits left, it means there was a bit in prevSigners that wasn't in nextSigners + if prevSigners.Len() > 0 { + return fmt.Errorf("some signers from parent block are missing from next epoch approvals of proposed block") + } + return nil +} diff --git a/msm/verification_test.go b/msm/verification_test.go new file mode 100644 index 00000000..e0497681 --- /dev/null +++ b/msm/verification_test.go @@ -0,0 +1,1016 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "crypto/sha256" + "fmt" + "testing" + "time" + + "github.com/ava-labs/simplex" + "github.com/stretchr/testify/require" +) + +func TestPChainHeightVerifier(t *testing.T) { + for _, tc := range []struct { + name string + pChainHeight uint64 + prevHeight uint64 + nextHeight uint64 + err string + }{ + { + name: "valid height", + pChainHeight: 200, + prevHeight: 100, + nextHeight: 150, + }, + { + name: "height equal to current", + pChainHeight: 200, + prevHeight: 100, + nextHeight: 200, + }, + { + name: "height too big", + pChainHeight: 100, + prevHeight: 50, + nextHeight: 150, + err: "invalid P-chain height (150) is too big, expected to be ≤ 100", + }, + { + name: "height smaller than parent", + pChainHeight: 200, + prevHeight: 150, + nextHeight: 100, + err: "invalid P-chain height (100) is smaller than parent block's P-chain height (150)", + }, + { + name: "height equal to parent", + pChainHeight: 200, + prevHeight: 100, + nextHeight: 100, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &pChainHeightVerifier{ + getPChainHeight: func() uint64 { return tc.pChainHeight }, + } + err := v.Verify(verificationInput{ + prevMD: StateMachineMetadata{PChainHeight: tc.prevHeight}, + proposedBlockMD: StateMachineMetadata{PChainHeight: tc.nextHeight}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestTimestampVerifier(t *testing.T) { + now := time.Now() + + timeSkewLimit := 5 * time.Second + + futureTime := now.Add(10 * time.Second) + + for _, tc := range []struct { + name string + hasInnerBlock bool + innerBlockTimestamp time.Time + timestamp uint64 + parentTimestamp uint64 + err string + }{ + { + name: "valid timestamp with inner block", + hasInnerBlock: true, + innerBlockTimestamp: now, + timestamp: uint64(now.UnixMilli()), + }, + { + name: "metadata timestamp does not match inner block", + hasInnerBlock: true, + innerBlockTimestamp: now, + timestamp: uint64(now.UnixMilli()) + 100, + err: fmt.Sprintf("block timestamp %d does not match inner block timestamp %d", uint64(now.UnixMilli())+100, now.UnixMilli()), + }, + { + name: "timestamp too far in the future", + hasInnerBlock: true, + innerBlockTimestamp: futureTime, + timestamp: uint64(futureTime.UnixMilli()), + err: fmt.Sprintf("proposed block timestamp is too far in the future, current time is %v but got %v", now, time.UnixMilli(futureTime.UnixMilli())), + }, + { + name: "timestamp older than parent", + hasInnerBlock: true, + innerBlockTimestamp: now, + timestamp: uint64(now.UnixMilli()), + parentTimestamp: uint64(now.UnixMilli()) + 10, + err: fmt.Sprintf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", uint64(now.UnixMilli())+10, uint64(now.UnixMilli())), + }, + { + name: "no inner block inherits parent timestamp", + hasInnerBlock: false, + timestamp: uint64(now.UnixMilli()), + parentTimestamp: uint64(now.UnixMilli()), + }, + { + name: "no inner block with different timestamp than parent", + hasInnerBlock: false, + timestamp: uint64(now.UnixMilli()) + 100, + parentTimestamp: uint64(now.UnixMilli()), + err: fmt.Sprintf("block without inner block should inherit parent timestamp %d but got %d", uint64(now.UnixMilli()), uint64(now.UnixMilli())+100), + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := ×tampVerifier{ + getTime: func() time.Time { return now }, + timeSkewLimit: timeSkewLimit, + } + err := v.Verify(verificationInput{ + hasInnerBlock: tc.hasInnerBlock, + innerBlockTimestamp: tc.innerBlockTimestamp, + proposedBlockMD: StateMachineMetadata{Timestamp: tc.timestamp}, + prevMD: StateMachineMetadata{Timestamp: tc.parentTimestamp}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestPChainReferenceHeightVerifier(t *testing.T) { + for _, tc := range []struct { + name string + nextBlockType BlockType + prev SimplexEpochInfo + next SimplexEpochInfo + err string + }{ + { + name: "new epoch block matching prev NextPChainReferenceHeight", + nextBlockType: BlockTypeNewEpoch, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200, SealingBlockSeq: 5}, + next: SimplexEpochInfo{PChainReferenceHeight: 200}, + }, + { + name: "new epoch block not matching prev NextPChainReferenceHeight", + nextBlockType: BlockTypeNewEpoch, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200, SealingBlockSeq: 5}, + next: SimplexEpochInfo{PChainReferenceHeight: 100}, + err: "expected P-chain reference height of the first block of epoch 5 to be 200 but got 100", + }, + { + name: "normal block matching prev PChainReferenceHeight", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{PChainReferenceHeight: 100}, + }, + { + name: "normal block not matching prev PChainReferenceHeight", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{PChainReferenceHeight: 200}, + err: "expected P-chain reference height to be 100 but got 200", + }, + { + name: "sealing block matching prev PChainReferenceHeight", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{PChainReferenceHeight: 100}, + }, + { + name: "telock block matching prev PChainReferenceHeight", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{PChainReferenceHeight: 100}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &pChainReferenceHeightVerifier{} + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevMD: StateMachineMetadata{SimplexEpochInfo: tc.prev}, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestEpochNumberVerifier(t *testing.T) { + for _, tc := range []struct { + name string + nextBlockType BlockType + prevBlockSeq uint64 + prev SimplexEpochInfo + next SimplexEpochInfo + err string + }{ + { + name: "prev epoch 0 with wrong next epoch", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{EpochNumber: 0}, + next: SimplexEpochInfo{EpochNumber: 5}, + err: "expected epoch number of the first block created to be 1 but got 5", + }, + { + name: "new epoch block matching sealing seq", + nextBlockType: BlockTypeNewEpoch, + prevBlockSeq: 10, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{EpochNumber: 10}, + }, + { + name: "new epoch block not matching sealing seq", + nextBlockType: BlockTypeNewEpoch, + prevBlockSeq: 10, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{EpochNumber: 5}, + err: "expected epoch number to be 10 but got 5", + }, + { + name: "normal block same epoch", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{EpochNumber: 3}, + next: SimplexEpochInfo{EpochNumber: 3}, + }, + { + name: "normal block different epoch", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{EpochNumber: 3}, + next: SimplexEpochInfo{EpochNumber: 4}, + err: "expected epoch number to be 3 but got 4", + }, + { + name: "sealing block same epoch", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{EpochNumber: 2}, + next: SimplexEpochInfo{EpochNumber: 2}, + }, + { + name: "telock block same epoch", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{EpochNumber: 2}, + next: SimplexEpochInfo{EpochNumber: 2}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &epochNumberVerifier{} + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevBlockSeq: tc.prevBlockSeq, + prevMD: StateMachineMetadata{SimplexEpochInfo: tc.prev}, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestPrevSealingBlockHashVerifier(t *testing.T) { + // A simplex block (EpochNumber > 0) so findFirstSimplexBlock can locate it. + firstSimplexBlock := StateMachineBlock{ + InnerBlock: &testVMBlock{bytes: []byte{1, 2, 3}}, + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1}}, + } + firstSimplexBlockHash := firstSimplexBlock.Digest() + + // A block used for epoch >1 sealing lookups. + prevSealingBlock := StateMachineBlock{ + InnerBlock: &testVMBlock{bytes: []byte{4, 5, 6}}, + Metadata: StateMachineMetadata{SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 5}}, + } + prevSealingBlockHash := prevSealingBlock.Digest() + + bs := make(testBlockStore) + bs[1] = firstSimplexBlock + bs[5] = prevSealingBlock + latestPersisted := uint64(1) + + for _, tc := range []struct { + name string + nextBlockType BlockType + prev SimplexEpochInfo + next SimplexEpochInfo + err string + }{ + { + name: "epoch 1 sealing block with correct hash", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{ + PrevSealingBlockHash: firstSimplexBlockHash, + }, + }, + { + name: "epoch 1 sealing block with wrong hash", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{ + PrevSealingBlockHash: [32]byte{9, 9, 9}, + }, + err: fmt.Sprintf("expected prev sealing block hash of the first ever simplex block to be %x but got %x", firstSimplexBlockHash, [32]byte{9, 9, 9}), + }, + { + name: "epoch >1 sealing block with correct hash", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{EpochNumber: 5}, + next: SimplexEpochInfo{ + PrevSealingBlockHash: prevSealingBlockHash, + }, + }, + { + name: "epoch >1 sealing block with wrong hash", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{EpochNumber: 5}, + next: SimplexEpochInfo{ + PrevSealingBlockHash: [32]byte{9, 9, 9}, + }, + err: fmt.Sprintf("expected prev sealing block hash to be %x but got %x", prevSealingBlockHash, [32]byte{9, 9, 9}), + }, + { + name: "non-sealing block with empty hash", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{}, + }, + { + name: "non-sealing block with non-empty hash", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{EpochNumber: 1}, + next: SimplexEpochInfo{ + PrevSealingBlockHash: [32]byte{1}, + }, + err: fmt.Sprintf("expected prev sealing block hash of a non sealing block to be empty but got %x", [32]byte{1}), + }, + { + name: "telock block with empty hash", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{EpochNumber: 2}, + next: SimplexEpochInfo{}, + }, + { + name: "new epoch block with empty hash", + nextBlockType: BlockTypeNewEpoch, + prev: SimplexEpochInfo{EpochNumber: 2}, + next: SimplexEpochInfo{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &prevSealingBlockHashVerifier{ + getBlock: bs.getBlock, + latestPersistedHeight: &latestPersisted, + } + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevMD: StateMachineMetadata{SimplexEpochInfo: tc.prev}, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNextPChainReferenceHeightVerifier(t *testing.T) { + validators1 := NodeBLSMappings{{BLSKey: []byte{1}, Weight: 1}} + validators2 := NodeBLSMappings{{BLSKey: []byte{2}, Weight: 1}} + + for _, tc := range []struct { + name string + nextBlockType BlockType + prev SimplexEpochInfo + prevPChainRef uint64 + next SimplexEpochInfo + getValidator ValidatorSetRetriever + pChainHeight uint64 + err string + }{ + { + name: "telock block matching height", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + }, + { + name: "telock block mismatched height", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 300}, + err: "expected P-chain reference height to be 200 but got 300", + }, + { + name: "sealing block matching height", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + }, + { + name: "sealing block mismatched height", + nextBlockType: BlockTypeSealing, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 100}, + err: "expected P-chain reference height to be 200 but got 100", + }, + { + name: "normal block prev already has next height set", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + }, + { + name: "normal block prev already has next height set mismatch", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 300}, + err: "expected P-chain reference height to be 200 but got 300", + }, + { + name: "normal block next p-chain reference height less than current", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 200}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 100}, + err: "expected P-chain reference height to be non-decreasing, but the previous P-chain reference height is 200 and the proposed P-chain reference height is 100", + }, + { + name: "normal block same validator set with non-zero next height", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators1, nil }, + err: "validator set at proposed next P-chain reference height 200 is the same as validator set at previous block's P-chain reference height 100,so expected next P-chain reference height to remain the same but got 200", + }, + { + name: "normal block no validator change and next height is zero", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 0}, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators1, nil }, + }, + { + name: "normal block validator change detected and p-chain height reached", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + getValidator: func(h uint64) (NodeBLSMappings, error) { + if h == 200 { + return validators2, nil + } + return validators1, nil + }, + pChainHeight: 200, + }, + { + name: "normal block validator change but p-chain height not reached", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{PChainReferenceHeight: 100}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 200}, + getValidator: func(h uint64) (NodeBLSMappings, error) { + if h == 200 { + return validators2, nil + } + return validators1, nil + }, + pChainHeight: 150, + err: "haven't reached P-chain height 200 yet, current P-chain height is only 150", + }, + { + name: "new epoch block with zero next height", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{NextPChainReferenceHeight: 0}, + }, + { + name: "new epoch block with non-zero next height", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{NextPChainReferenceHeight: 100}, + err: "expected P-chain reference height to be 0 but got 100", + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &nextPChainReferenceHeightVerifier{ + getValidatorSet: tc.getValidator, + getPChainHeight: func() uint64 { return tc.pChainHeight }, + } + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevMD: StateMachineMetadata{SimplexEpochInfo: tc.prev}, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestVMBlockSeqVerifier(t *testing.T) { + prevMDBytes := (&simplex.ProtocolMetadata{Seq: 5, Prev: [32]byte{1}}).Bytes() + proposedMDBytes := (&simplex.ProtocolMetadata{Seq: 6, Prev: [32]byte{2}}).Bytes() + + blockWithInner := StateMachineBlock{ + InnerBlock: &testVMBlock{bytes: []byte{1}}, + } + blockWithoutInner := StateMachineBlock{} + + for _, tc := range []struct { + name string + prev SimplexEpochInfo + prevMD StateMachineMetadata + next SimplexEpochInfo + prevBlockSeq uint64 + block StateMachineBlock + err string + }{ + { + name: "first simplex block matching seq", + prev: SimplexEpochInfo{EpochNumber: 0}, + next: SimplexEpochInfo{PrevVMBlockSeq: 42}, + prevBlockSeq: 42, + }, + { + name: "first simplex block wrong seq", + prev: SimplexEpochInfo{EpochNumber: 0}, + next: SimplexEpochInfo{PrevVMBlockSeq: 10}, + prevBlockSeq: 42, + err: "expected PrevVMBlockSeq to be 42 but got 10", + }, + { + name: "prev block has block", + prev: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevMDBytes, SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}}, + next: SimplexEpochInfo{PrevVMBlockSeq: 4}, + prevBlockSeq: 4, + block: blockWithInner, + }, + { + name: "prev block has block wrong seq", + prev: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevMDBytes, SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}}, + next: SimplexEpochInfo{PrevVMBlockSeq: 99}, + prevBlockSeq: 4, + block: blockWithInner, + err: "expected PrevVMBlockSeq to be 4 but got 99", + }, + { + name: "prev block has no block uses parent PrevVMBlockSeq", + prev: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevMDBytes, SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}}, + next: SimplexEpochInfo{PrevVMBlockSeq: 3}, + prevBlockSeq: 4, + block: blockWithoutInner, + }, + { + name: "prev block has no block wrong seq", + prev: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevMDBytes, SimplexEpochInfo: SimplexEpochInfo{EpochNumber: 1, PrevVMBlockSeq: 3}}, + next: SimplexEpochInfo{PrevVMBlockSeq: 99}, + prevBlockSeq: 4, + block: blockWithoutInner, + err: "expected PrevVMBlockSeq to be 3 but got 99", + }, + } { + t.Run(tc.name, func(t *testing.T) { + bs := make(testBlockStore) + bs[tc.prevBlockSeq] = tc.block + + v := &vmBlockSeqVerifier{ + getBlock: bs.getBlock, + } + + prevMD := tc.prevMD + if prevMD.SimplexEpochInfo.EpochNumber == 0 && tc.prev.EpochNumber == 0 { + prevMD.SimplexEpochInfo = tc.prev + } + + err := v.Verify(verificationInput{ + prevMD: prevMD, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next, SimplexProtocolMetadata: proposedMDBytes}, + prevBlockSeq: tc.prevBlockSeq, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidationDescriptorVerifier(t *testing.T) { + validators := NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, + {BLSKey: []byte{2}, Weight: 1}, + } + + otherValidators := NodeBLSMappings{ + {BLSKey: []byte{3}, Weight: 1}, + } + + for _, tc := range []struct { + name string + nextBlockType BlockType + next SimplexEpochInfo + getValidator ValidatorSetRetriever + err string + }{ + { + name: "sealing block with matching validators", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{Members: validators}, + }, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + }, + { + name: "sealing block with mismatching validators", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{Members: otherValidators}, + }, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + err: "expected validator set specified at P-chain height 100 does not match validator set encoded in new block", + }, + { + name: "sealing block with validator retrieval error", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + BlockValidationDescriptor: &BlockValidationDescriptor{}, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return nil, fmt.Errorf("unavailable") }, + err: "unavailable", + }, + { + name: "normal block with nil descriptor", + nextBlockType: BlockTypeNormal, + next: SimplexEpochInfo{}, + }, + { + name: "normal block with non-nil descriptor", + nextBlockType: BlockTypeNormal, + next: SimplexEpochInfo{ + BlockValidationDescriptor: &BlockValidationDescriptor{}, + }, + err: "block validation descriptor should be nil but got &{{[] {0}} {0}}", + }, + { + name: "telock block with nil descriptor", + nextBlockType: BlockTypeTelock, + next: SimplexEpochInfo{}, + }, + { + name: "new epoch block with nil descriptor", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &validationDescriptorVerifier{ + getValidatorSet: tc.getValidator, + } + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNextEpochApprovalsVerifier(t *testing.T) { + validators := NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, + {BLSKey: []byte{2}, Weight: 1}, + {BLSKey: []byte{3}, Weight: 1}, + } + + for _, tc := range []struct { + name string + nextBlockType BlockType + prev SimplexEpochInfo + next SimplexEpochInfo + getValidator ValidatorSetRetriever + sigVerifier SignatureVerifier + keyAggregator KeyAggregator + err string + }{ + { + name: "sealing block with nil approvals", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{}, + err: "next epoch approvals should not be nil for a sealing block", + }, + { + name: "sealing block with validator retrieval error", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{7}, Signature: []byte("sig")}, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return nil, fmt.Errorf("unavailable") }, + err: "unavailable", + }, + { + name: "sealing block not enough approvals", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}, Signature: []byte("sig")}, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + sigVerifier: &testSigVerifier{}, + keyAggregator: &testKeyAggregator{}, + err: "not enough approvals to seal block", + }, + { + name: "sealing block enough approvals", + nextBlockType: BlockTypeSealing, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{7}, Signature: []byte("sig")}, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + sigVerifier: &testSigVerifier{}, + keyAggregator: &testKeyAggregator{}, + }, + { + name: "normal block no validator change", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 0}, + next: SimplexEpochInfo{}, + }, + { + name: "normal block collecting approvals with nil approvals", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{NextPChainReferenceHeight: 100}, + next: SimplexEpochInfo{NextPChainReferenceHeight: 100}, + err: "next epoch approvals should not be nil when collecting approvals", + }, + { + name: "normal block collecting approvals valid", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + PChainReferenceHeight: 50, + }, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}, Signature: []byte("sig")}, + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + sigVerifier: &testSigVerifier{}, + keyAggregator: &testKeyAggregator{}, + }, + { + name: "normal block collecting approvals signers not superset of prev", + nextBlockType: BlockTypeNormal, + prev: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + PChainReferenceHeight: 50, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{3}, Signature: []byte("sig")}, // bits 0,1 + }, + next: SimplexEpochInfo{ + NextPChainReferenceHeight: 100, + NextEpochApprovals: &NextEpochApprovals{NodeIDs: []byte{1}, Signature: []byte("sig")}, // bit 0 only + }, + getValidator: func(h uint64) (NodeBLSMappings, error) { return validators, nil }, + sigVerifier: &testSigVerifier{}, + keyAggregator: &testKeyAggregator{}, + err: "some signers from parent block are missing from next epoch approvals of proposed block", + }, + { + name: "telock block with nil approvals", + nextBlockType: BlockTypeTelock, + next: SimplexEpochInfo{}, + }, + { + name: "telock block with non-nil approvals", + nextBlockType: BlockTypeTelock, + next: SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{}, + }, + err: "next epoch approvals should be nil but got &{[] [] {0}}", + }, + { + name: "new epoch block with nil approvals", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{}, + }, + { + name: "new epoch block with non-nil approvals", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{ + NextEpochApprovals: &NextEpochApprovals{}, + }, + err: "next epoch approvals should be nil but got &{[] [] {0}}", + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &nextEpochApprovalsVerifier{ + sigVerifier: tc.sigVerifier, + getValidatorSet: tc.getValidator, + keyAggregator: tc.keyAggregator, + } + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevMD: StateMachineMetadata{SimplexEpochInfo: tc.prev}, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSealingBlockSeqVerifier(t *testing.T) { + prevProtocolMD := (&simplex.ProtocolMetadata{Seq: 5}).Bytes() + + for _, tc := range []struct { + name string + nextBlockType BlockType + prev SimplexEpochInfo + prevMD StateMachineMetadata + next SimplexEpochInfo + err string + }{ + { + name: "normal block with zero sealing seq", + nextBlockType: BlockTypeNormal, + next: SimplexEpochInfo{SealingBlockSeq: 0}, + }, + { + name: "normal block with non-zero sealing seq", + nextBlockType: BlockTypeNormal, + next: SimplexEpochInfo{SealingBlockSeq: 5}, + err: "expected sealing block sequence number to be 0 but got 5", + }, + { + name: "new epoch block with zero sealing seq", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{SealingBlockSeq: 0}, + }, + { + name: "new epoch block with non-zero sealing seq", + nextBlockType: BlockTypeNewEpoch, + next: SimplexEpochInfo{SealingBlockSeq: 3}, + err: "expected sealing block sequence number to be 0 but got 3", + }, + { + name: "telock block matching prev sealing seq", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{SealingBlockSeq: 10}, + next: SimplexEpochInfo{SealingBlockSeq: 10}, + }, + { + name: "telock block mismatching prev sealing seq", + nextBlockType: BlockTypeTelock, + prev: SimplexEpochInfo{SealingBlockSeq: 10}, + next: SimplexEpochInfo{SealingBlockSeq: 11}, + err: "expected sealing block sequence number to be 10 but got 11", + }, + { + name: "sealing block with zero seq", + nextBlockType: BlockTypeSealing, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevProtocolMD}, + next: SimplexEpochInfo{SealingBlockSeq: 0}, + }, + { + name: "sealing block with non-zero seq", + nextBlockType: BlockTypeSealing, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevProtocolMD}, + next: SimplexEpochInfo{SealingBlockSeq: 10}, + err: "expected sealing block sequence number to be 0 but got 10", + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &sealingBlockSeqVerifier{} + prevMD := tc.prevMD + prevMD.SimplexEpochInfo = tc.prev + err := v.Verify(verificationInput{ + nextBlockType: tc.nextBlockType, + prevMD: prevMD, + proposedBlockMD: StateMachineMetadata{SimplexEpochInfo: tc.next}, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +// Test helpers + +type testBlockStore map[uint64]StateMachineBlock + +func (bs testBlockStore) getBlock(seq uint64, _ [32]byte) (StateMachineBlock, *simplex.Finalization, error) { + blk, ok := bs[seq] + if !ok { + return StateMachineBlock{}, nil, fmt.Errorf("%w: block %d", simplex.ErrBlockNotFound, seq) + } + return blk, nil, nil +} + +type testVMBlock struct { + bytes []byte + height uint64 +} + +func (b *testVMBlock) Digest() [32]byte { + return sha256.Sum256(b.bytes) +} + +func (b *testVMBlock) Height() uint64 { + return b.height +} + +func (b *testVMBlock) Timestamp() time.Time { + return time.Now() +} + +func (b *testVMBlock) Verify(_ context.Context) error { + return nil +} + +type testSigVerifier struct { + err error +} + +func (sv *testSigVerifier) VerifySignature(_, _, _ []byte) error { + return sv.err +} + +type testKeyAggregator struct { + err error +} + +func (ka *testKeyAggregator) AggregateKeys(keys ...[]byte) ([]byte, error) { + if ka.err != nil { + return nil, ka.err + } + var agg []byte + for _, k := range keys { + agg = append(agg, k...) + } + return agg, nil +} + +type InnerBlock struct { + TS time.Time + BlockHeight uint64 + Bytes []byte +} + +func (i *InnerBlock) Digest() [32]byte { + return sha256.Sum256(i.Bytes) +} + +func (i *InnerBlock) Height() uint64 { + return i.BlockHeight +} + +func (i *InnerBlock) Timestamp() time.Time { + return i.TS +} + +func (i *InnerBlock) Verify(_ context.Context) error { + return nil +}