diff --git a/go.mod b/go.mod index a1145162..e8be0cb2 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,18 @@ module github.com/ava-labs/simplex go 1.23.0 require ( - github.com/stretchr/testify v1.9.0 + github.com/StephenButtolph/canoto v0.17.3 + 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..8a254a4c 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,25 @@ +github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI= +github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= +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..4ef6a845 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`. @@ -137,7 +137,7 @@ it sets `next_p_chain_reference_height` to the sampled P-chain height. When the block $B_{k+1}$ built by the `i`'th node is verified by nodes other than the block proposer, they ensure that: - The `p_chain_reference_height` hasn't changed from the previous block $B_k$. -- `next_p_chain_reference_height > p_chain_reference_height` or `next_p_chain_reference_height == 0`. +- `next_p_chain_reference_height ≥ p_chain_reference_height` or `next_p_chain_reference_height == 0`. - If `next_p_chain_reference_height > 0`, the node has observed the P-chain height exists in the P-chain. - If `next_p_chain_reference_height > 0`, the validator set derived from the P-chain height corresponding to is different from the validator set derived by the P-chain height corresponding to `p_chain_reference_height`. @@ -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/build_decision.go b/msm/build_decision.go new file mode 100644 index 00000000..5ca1c1ac --- /dev/null +++ b/msm/build_decision.go @@ -0,0 +1,158 @@ +// 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 +} + +// 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..ec14a607 --- /dev/null +++ b/msm/build_decision_test.go @@ -0,0 +1,206 @@ +// 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(context.Background()) + 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(context.Background()) + + 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) + + var calls atomic.Int32 + + 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(uint64) (bool, error) { + return calls.Add(1) > 1, nil + }, + getPChainHeight: func() uint64 { return pChainHeight.Load() }, + } + + result, resultPChainHeight, err := bbd.shouldBuildBlock(context.Background()) + require.NoError(t, err) + require.Equal(t, blockBuildingDecisionTransitionEpoch, result) + require.Equal(t, uint64(200), resultPChainHeight) + require.GreaterOrEqual(t, int(calls.Load()), 2) +} + +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(context.Background()) + 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(context.Background()) + 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(context.Background()) + 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(context.Background()) + 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..8cf85f44 --- /dev/null +++ b/msm/encoding.canoto.go @@ -0,0 +1,2554 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.17.3 +// 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 - 0 + _ uint = 0 - canoto.VersionCompatibility +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__OuterBlock__InnerBlock = 1 + canoto__OuterBlock__Metadata = 2 + + canoto__OuterBlock__InnerBlock__tag = "\x0a" // canoto.Tag(canoto__OuterBlock__InnerBlock, canoto.Len) + canoto__OuterBlock__Metadata__tag = "\x12" // canoto.Tag(canoto__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.TypeOf(OuterBlock{})) + var zero OuterBlock + s := &canoto.Spec{ + Name: "OuterBlock", + Fields: []canoto.FieldType{ + { + FieldNumber: canoto__OuterBlock__InnerBlock, + Name: "InnerBlock", + OneOf: "", + TypeBytes: true, + }, + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.Metadata), + /*FieldNumber: */ canoto__OuterBlock__Metadata, + /*Name: */ "Metadata", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*OuterBlock) MakeCanoto() *OuterBlock { + return new(OuterBlock) +} + +// 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 canoto__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 canoto__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 == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if len(c.InnerBlock) != 0 { + size += uint64(len(canoto__OuterBlock__InnerBlock__tag)) + canoto.SizeBytes(c.InnerBlock) + } + (&c.Metadata).CalculateCanotoCache() + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__OuterBlock__Metadata__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if len(c.InnerBlock) != 0 { + canoto.Append(&w, canoto__OuterBlock__InnerBlock__tag) + canoto.AppendBytes(&w, c.InnerBlock) + } + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__OuterBlock__Metadata__tag) + canoto.AppendUint(&w, fieldSize) + w = (&c.Metadata).MarshalCanotoInto(w) + } + return w +} + +const ( + canoto__StateMachineMetadata__ICMEpochInfo = 1 + canoto__StateMachineMetadata__SimplexEpochInfo = 2 + canoto__StateMachineMetadata__SimplexProtocolMetadata = 3 + canoto__StateMachineMetadata__SimplexBlacklist = 4 + canoto__StateMachineMetadata__AuxiliaryInfo = 5 + canoto__StateMachineMetadata__PChainHeight = 6 + canoto__StateMachineMetadata__Timestamp = 7 + + canoto__StateMachineMetadata__ICMEpochInfo__tag = "\x0a" // canoto.Tag(canoto__StateMachineMetadata__ICMEpochInfo, canoto.Len) + canoto__StateMachineMetadata__SimplexEpochInfo__tag = "\x12" // canoto.Tag(canoto__StateMachineMetadata__SimplexEpochInfo, canoto.Len) + canoto__StateMachineMetadata__SimplexProtocolMetadata__tag = "\x1a" // canoto.Tag(canoto__StateMachineMetadata__SimplexProtocolMetadata, canoto.Len) + canoto__StateMachineMetadata__SimplexBlacklist__tag = "\x22" // canoto.Tag(canoto__StateMachineMetadata__SimplexBlacklist, canoto.Len) + canoto__StateMachineMetadata__AuxiliaryInfo__tag = "\x2a" // canoto.Tag(canoto__StateMachineMetadata__AuxiliaryInfo, canoto.Len) + canoto__StateMachineMetadata__PChainHeight__tag = "\x30" // canoto.Tag(canoto__StateMachineMetadata__PChainHeight, canoto.Varint) + canoto__StateMachineMetadata__Timestamp__tag = "\x38" // canoto.Tag(canoto__StateMachineMetadata__Timestamp, canoto.Varint) +) + +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.TypeOf(StateMachineMetadata{})) + var zero StateMachineMetadata + s := &canoto.Spec{ + Name: "StateMachineMetadata", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.ICMEpochInfo), + /*FieldNumber: */ canoto__StateMachineMetadata__ICMEpochInfo, + /*Name: */ "ICMEpochInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.SimplexEpochInfo), + /*FieldNumber: */ canoto__StateMachineMetadata__SimplexEpochInfo, + /*Name: */ "SimplexEpochInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + { + FieldNumber: canoto__StateMachineMetadata__SimplexProtocolMetadata, + Name: "SimplexProtocolMetadata", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canoto__StateMachineMetadata__SimplexBlacklist, + Name: "SimplexBlacklist", + OneOf: "", + TypeBytes: true, + }, + canoto.FieldTypeFromField( + /*type inference:*/ (zero.AuxiliaryInfo), + /*FieldNumber: */ canoto__StateMachineMetadata__AuxiliaryInfo, + /*Name: */ "AuxiliaryInfo", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + { + FieldNumber: canoto__StateMachineMetadata__PChainHeight, + Name: "PChainHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainHeight), + }, + { + FieldNumber: canoto__StateMachineMetadata__Timestamp, + Name: "Timestamp", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Timestamp), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*StateMachineMetadata) MakeCanoto() *StateMachineMetadata { + return new(StateMachineMetadata) +} + +// 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 canoto__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 + case canoto__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 canoto__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 canoto__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 canoto__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 + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + 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 canoto__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 canoto__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 + } + 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 == nil { + return true + } + if !(&c.ICMEpochInfo).ValidCanoto() { + return false + } + if !(&c.SimplexEpochInfo).ValidCanoto() { + return false + } + if c.AuxiliaryInfo != nil && !(c.AuxiliaryInfo).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() { + if c == nil { + return + } + var size uint64 + (&c.ICMEpochInfo).CalculateCanotoCache() + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__StateMachineMetadata__ICMEpochInfo__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + (&c.SimplexEpochInfo).CalculateCanotoCache() + if fieldSize := (&c.SimplexEpochInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__StateMachineMetadata__SimplexEpochInfo__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + if len(c.SimplexProtocolMetadata) != 0 { + size += uint64(len(canoto__StateMachineMetadata__SimplexProtocolMetadata__tag)) + canoto.SizeBytes(c.SimplexProtocolMetadata) + } + if len(c.SimplexBlacklist) != 0 { + size += uint64(len(canoto__StateMachineMetadata__SimplexBlacklist__tag)) + canoto.SizeBytes(c.SimplexBlacklist) + } + if c.AuxiliaryInfo != nil { + (c.AuxiliaryInfo).CalculateCanotoCache() + if fieldSize := (c.AuxiliaryInfo).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__StateMachineMetadata__AuxiliaryInfo__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + if !canoto.IsZero(c.PChainHeight) { + size += uint64(len(canoto__StateMachineMetadata__PChainHeight__tag)) + canoto.SizeUint(c.PChainHeight) + } + if !canoto.IsZero(c.Timestamp) { + size += uint64(len(canoto__StateMachineMetadata__Timestamp__tag)) + canoto.SizeUint(c.Timestamp) + } + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if fieldSize := (&c.ICMEpochInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__StateMachineMetadata__ICMEpochInfo__tag) + canoto.AppendUint(&w, fieldSize) + w = (&c.ICMEpochInfo).MarshalCanotoInto(w) + } + if fieldSize := (&c.SimplexEpochInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__StateMachineMetadata__SimplexEpochInfo__tag) + canoto.AppendUint(&w, fieldSize) + w = (&c.SimplexEpochInfo).MarshalCanotoInto(w) + } + if len(c.SimplexProtocolMetadata) != 0 { + canoto.Append(&w, canoto__StateMachineMetadata__SimplexProtocolMetadata__tag) + canoto.AppendBytes(&w, c.SimplexProtocolMetadata) + } + if len(c.SimplexBlacklist) != 0 { + canoto.Append(&w, canoto__StateMachineMetadata__SimplexBlacklist__tag) + canoto.AppendBytes(&w, c.SimplexBlacklist) + } + if c.AuxiliaryInfo != nil { + if fieldSize := (c.AuxiliaryInfo).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__StateMachineMetadata__AuxiliaryInfo__tag) + canoto.AppendUint(&w, fieldSize) + w = (c.AuxiliaryInfo).MarshalCanotoInto(w) + } + } + if !canoto.IsZero(c.PChainHeight) { + canoto.Append(&w, canoto__StateMachineMetadata__PChainHeight__tag) + canoto.AppendUint(&w, c.PChainHeight) + } + if !canoto.IsZero(c.Timestamp) { + canoto.Append(&w, canoto__StateMachineMetadata__Timestamp__tag) + canoto.AppendUint(&w, c.Timestamp) + } + return w +} + +const ( + canoto__ICMEpochInfo__EpochStartTime = 1 + canoto__ICMEpochInfo__EpochNumber = 2 + canoto__ICMEpochInfo__PChainEpochHeight = 3 + + canoto__ICMEpochInfo__EpochStartTime__tag = "\x08" // canoto.Tag(canoto__ICMEpochInfo__EpochStartTime, canoto.Varint) + canoto__ICMEpochInfo__EpochNumber__tag = "\x10" // canoto.Tag(canoto__ICMEpochInfo__EpochNumber, canoto.Varint) + canoto__ICMEpochInfo__PChainEpochHeight__tag = "\x18" // canoto.Tag(canoto__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: canoto__ICMEpochInfo__EpochStartTime, + Name: "EpochStartTime", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochStartTime), + }, + { + FieldNumber: canoto__ICMEpochInfo__EpochNumber, + Name: "EpochNumber", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochNumber), + }, + { + FieldNumber: canoto__ICMEpochInfo__PChainEpochHeight, + Name: "PChainEpochHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainEpochHeight), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*ICMEpochInfo) MakeCanoto() *ICMEpochInfo { + return new(ICMEpochInfo) +} + +// 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 canoto__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 canoto__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 canoto__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 { + if c == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.EpochStartTime) { + size += uint64(len(canoto__ICMEpochInfo__EpochStartTime__tag)) + canoto.SizeUint(c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + size += uint64(len(canoto__ICMEpochInfo__EpochNumber__tag)) + canoto.SizeUint(c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + size += uint64(len(canoto__ICMEpochInfo__PChainEpochHeight__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if !canoto.IsZero(c.EpochStartTime) { + canoto.Append(&w, canoto__ICMEpochInfo__EpochStartTime__tag) + canoto.AppendUint(&w, c.EpochStartTime) + } + if !canoto.IsZero(c.EpochNumber) { + canoto.Append(&w, canoto__ICMEpochInfo__EpochNumber__tag) + canoto.AppendUint(&w, c.EpochNumber) + } + if !canoto.IsZero(c.PChainEpochHeight) { + canoto.Append(&w, canoto__ICMEpochInfo__PChainEpochHeight__tag) + canoto.AppendUint(&w, c.PChainEpochHeight) + } + return w +} + +const ( + canoto__AuxiliaryInfo__Info = 1 + canoto__AuxiliaryInfo__PrevAuxInfoSeq = 2 + canoto__AuxiliaryInfo__ApplicationID = 3 + + canoto__AuxiliaryInfo__Info__tag = "\x0a" // canoto.Tag(canoto__AuxiliaryInfo__Info, canoto.Len) + canoto__AuxiliaryInfo__PrevAuxInfoSeq__tag = "\x10" // canoto.Tag(canoto__AuxiliaryInfo__PrevAuxInfoSeq, canoto.Varint) + canoto__AuxiliaryInfo__ApplicationID__tag = "\x18" // canoto.Tag(canoto__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: canoto__AuxiliaryInfo__Info, + Name: "Info", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canoto__AuxiliaryInfo__PrevAuxInfoSeq, + Name: "PrevAuxInfoSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PrevAuxInfoSeq), + }, + { + FieldNumber: canoto__AuxiliaryInfo__ApplicationID, + Name: "ApplicationID", + OneOf: "", + TypeUint: canoto.SizeOf(zero.ApplicationID), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*AuxiliaryInfo) MakeCanoto() *AuxiliaryInfo { + return new(AuxiliaryInfo) +} + +// 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 canoto__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 canoto__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 canoto__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 { + if c == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if len(c.Info) != 0 { + size += uint64(len(canoto__AuxiliaryInfo__Info__tag)) + canoto.SizeBytes(c.Info) + } + if !canoto.IsZero(c.PrevAuxInfoSeq) { + size += uint64(len(canoto__AuxiliaryInfo__PrevAuxInfoSeq__tag)) + canoto.SizeUint(c.PrevAuxInfoSeq) + } + if !canoto.IsZero(c.ApplicationID) { + size += uint64(len(canoto__AuxiliaryInfo__ApplicationID__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if len(c.Info) != 0 { + canoto.Append(&w, canoto__AuxiliaryInfo__Info__tag) + canoto.AppendBytes(&w, c.Info) + } + if !canoto.IsZero(c.PrevAuxInfoSeq) { + canoto.Append(&w, canoto__AuxiliaryInfo__PrevAuxInfoSeq__tag) + canoto.AppendUint(&w, c.PrevAuxInfoSeq) + } + if !canoto.IsZero(c.ApplicationID) { + canoto.Append(&w, canoto__AuxiliaryInfo__ApplicationID__tag) + canoto.AppendUint(&w, c.ApplicationID) + } + return w +} + +const ( + canoto__SimplexEpochInfo__PChainReferenceHeight = 1 + canoto__SimplexEpochInfo__EpochNumber = 2 + canoto__SimplexEpochInfo__PrevSealingBlockHash = 3 + canoto__SimplexEpochInfo__NextPChainReferenceHeight = 4 + canoto__SimplexEpochInfo__PrevVMBlockSeq = 5 + canoto__SimplexEpochInfo__BlockValidationDescriptor = 6 + canoto__SimplexEpochInfo__NextEpochApprovals = 7 + canoto__SimplexEpochInfo__SealingBlockSeq = 8 + + canoto__SimplexEpochInfo__PChainReferenceHeight__tag = "\x08" // canoto.Tag(canoto__SimplexEpochInfo__PChainReferenceHeight, canoto.Varint) + canoto__SimplexEpochInfo__EpochNumber__tag = "\x10" // canoto.Tag(canoto__SimplexEpochInfo__EpochNumber, canoto.Varint) + canoto__SimplexEpochInfo__PrevSealingBlockHash__tag = "\x1a" // canoto.Tag(canoto__SimplexEpochInfo__PrevSealingBlockHash, canoto.Len) + canoto__SimplexEpochInfo__NextPChainReferenceHeight__tag = "\x20" // canoto.Tag(canoto__SimplexEpochInfo__NextPChainReferenceHeight, canoto.Varint) + canoto__SimplexEpochInfo__PrevVMBlockSeq__tag = "\x28" // canoto.Tag(canoto__SimplexEpochInfo__PrevVMBlockSeq, canoto.Varint) + canoto__SimplexEpochInfo__BlockValidationDescriptor__tag = "\x32" // canoto.Tag(canoto__SimplexEpochInfo__BlockValidationDescriptor, canoto.Len) + canoto__SimplexEpochInfo__NextEpochApprovals__tag = "\x3a" // canoto.Tag(canoto__SimplexEpochInfo__NextEpochApprovals, canoto.Len) + canoto__SimplexEpochInfo__SealingBlockSeq__tag = "\x40" // canoto.Tag(canoto__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.TypeOf(SimplexEpochInfo{})) + var zero SimplexEpochInfo + s := &canoto.Spec{ + Name: "SimplexEpochInfo", + Fields: []canoto.FieldType{ + { + FieldNumber: canoto__SimplexEpochInfo__PChainReferenceHeight, + Name: "PChainReferenceHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainReferenceHeight), + }, + { + FieldNumber: canoto__SimplexEpochInfo__EpochNumber, + Name: "EpochNumber", + OneOf: "", + TypeUint: canoto.SizeOf(zero.EpochNumber), + }, + { + FieldNumber: canoto__SimplexEpochInfo__PrevSealingBlockHash, + Name: "PrevSealingBlockHash", + OneOf: "", + TypeFixedBytes: uint64(len(zero.PrevSealingBlockHash)), + }, + { + FieldNumber: canoto__SimplexEpochInfo__NextPChainReferenceHeight, + Name: "NextPChainReferenceHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NextPChainReferenceHeight), + }, + { + FieldNumber: canoto__SimplexEpochInfo__PrevVMBlockSeq, + Name: "PrevVMBlockSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PrevVMBlockSeq), + }, + canoto.FieldTypeFromField( + /*type inference:*/ (zero.BlockValidationDescriptor), + /*FieldNumber: */ canoto__SimplexEpochInfo__BlockValidationDescriptor, + /*Name: */ "BlockValidationDescriptor", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (zero.NextEpochApprovals), + /*FieldNumber: */ canoto__SimplexEpochInfo__NextEpochApprovals, + /*Name: */ "NextEpochApprovals", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + { + FieldNumber: canoto__SimplexEpochInfo__SealingBlockSeq, + Name: "SealingBlockSeq", + OneOf: "", + TypeUint: canoto.SizeOf(zero.SealingBlockSeq), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*SimplexEpochInfo) MakeCanoto() *SimplexEpochInfo { + return new(SimplexEpochInfo) +} + +// 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 canoto__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 canoto__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 canoto__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 canoto__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 canoto__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 canoto__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 + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + 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 canoto__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 + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + 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 canoto__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 == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.PChainReferenceHeight) { + size += uint64(len(canoto__SimplexEpochInfo__PChainReferenceHeight__tag)) + canoto.SizeUint(c.PChainReferenceHeight) + } + if !canoto.IsZero(c.EpochNumber) { + size += uint64(len(canoto__SimplexEpochInfo__EpochNumber__tag)) + canoto.SizeUint(c.EpochNumber) + } + if !canoto.IsZero(c.PrevSealingBlockHash) { + size += uint64(len(canoto__SimplexEpochInfo__PrevSealingBlockHash__tag)) + canoto.SizeBytes((&c.PrevSealingBlockHash)[:]) + } + if !canoto.IsZero(c.NextPChainReferenceHeight) { + size += uint64(len(canoto__SimplexEpochInfo__NextPChainReferenceHeight__tag)) + canoto.SizeUint(c.NextPChainReferenceHeight) + } + if !canoto.IsZero(c.PrevVMBlockSeq) { + size += uint64(len(canoto__SimplexEpochInfo__PrevVMBlockSeq__tag)) + canoto.SizeUint(c.PrevVMBlockSeq) + } + if c.BlockValidationDescriptor != nil { + (c.BlockValidationDescriptor).CalculateCanotoCache() + if fieldSize := (c.BlockValidationDescriptor).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__SimplexEpochInfo__BlockValidationDescriptor__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + if c.NextEpochApprovals != nil { + (c.NextEpochApprovals).CalculateCanotoCache() + if fieldSize := (c.NextEpochApprovals).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__SimplexEpochInfo__NextEpochApprovals__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + if !canoto.IsZero(c.SealingBlockSeq) { + size += uint64(len(canoto__SimplexEpochInfo__SealingBlockSeq__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if !canoto.IsZero(c.PChainReferenceHeight) { + canoto.Append(&w, canoto__SimplexEpochInfo__PChainReferenceHeight__tag) + canoto.AppendUint(&w, c.PChainReferenceHeight) + } + if !canoto.IsZero(c.EpochNumber) { + canoto.Append(&w, canoto__SimplexEpochInfo__EpochNumber__tag) + canoto.AppendUint(&w, c.EpochNumber) + } + if !canoto.IsZero(c.PrevSealingBlockHash) { + canoto.Append(&w, canoto__SimplexEpochInfo__PrevSealingBlockHash__tag) + canoto.AppendBytes(&w, (&c.PrevSealingBlockHash)[:]) + } + if !canoto.IsZero(c.NextPChainReferenceHeight) { + canoto.Append(&w, canoto__SimplexEpochInfo__NextPChainReferenceHeight__tag) + canoto.AppendUint(&w, c.NextPChainReferenceHeight) + } + if !canoto.IsZero(c.PrevVMBlockSeq) { + canoto.Append(&w, canoto__SimplexEpochInfo__PrevVMBlockSeq__tag) + canoto.AppendUint(&w, c.PrevVMBlockSeq) + } + if c.BlockValidationDescriptor != nil { + if fieldSize := (c.BlockValidationDescriptor).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__SimplexEpochInfo__BlockValidationDescriptor__tag) + canoto.AppendUint(&w, fieldSize) + w = (c.BlockValidationDescriptor).MarshalCanotoInto(w) + } + } + if c.NextEpochApprovals != nil { + if fieldSize := (c.NextEpochApprovals).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__SimplexEpochInfo__NextEpochApprovals__tag) + canoto.AppendUint(&w, fieldSize) + w = (c.NextEpochApprovals).MarshalCanotoInto(w) + } + } + if !canoto.IsZero(c.SealingBlockSeq) { + canoto.Append(&w, canoto__SimplexEpochInfo__SealingBlockSeq__tag) + canoto.AppendUint(&w, c.SealingBlockSeq) + } + return w +} + +const ( + canoto__NodeBLSMapping__NodeID = 1 + canoto__NodeBLSMapping__BLSKey = 2 + canoto__NodeBLSMapping__Weight = 3 + + canoto__NodeBLSMapping__NodeID__tag = "\x0a" // canoto.Tag(canoto__NodeBLSMapping__NodeID, canoto.Len) + canoto__NodeBLSMapping__BLSKey__tag = "\x12" // canoto.Tag(canoto__NodeBLSMapping__BLSKey, canoto.Len) + canoto__NodeBLSMapping__Weight__tag = "\x18" // canoto.Tag(canoto__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: canoto__NodeBLSMapping__NodeID, + Name: "NodeID", + OneOf: "", + TypeFixedBytes: uint64(len(zero.NodeID)), + }, + { + FieldNumber: canoto__NodeBLSMapping__BLSKey, + Name: "BLSKey", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canoto__NodeBLSMapping__Weight, + Name: "Weight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Weight), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*NodeBLSMapping) MakeCanoto() *NodeBLSMapping { + return new(NodeBLSMapping) +} + +// 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 canoto__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 canoto__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 canoto__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 { + if c == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.NodeID) { + size += uint64(len(canoto__NodeBLSMapping__NodeID__tag)) + canoto.SizeBytes((&c.NodeID)[:]) + } + if len(c.BLSKey) != 0 { + size += uint64(len(canoto__NodeBLSMapping__BLSKey__tag)) + canoto.SizeBytes(c.BLSKey) + } + if !canoto.IsZero(c.Weight) { + size += uint64(len(canoto__NodeBLSMapping__Weight__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if !canoto.IsZero(c.NodeID) { + canoto.Append(&w, canoto__NodeBLSMapping__NodeID__tag) + canoto.AppendBytes(&w, (&c.NodeID)[:]) + } + if len(c.BLSKey) != 0 { + canoto.Append(&w, canoto__NodeBLSMapping__BLSKey__tag) + canoto.AppendBytes(&w, c.BLSKey) + } + if !canoto.IsZero(c.Weight) { + canoto.Append(&w, canoto__NodeBLSMapping__Weight__tag) + canoto.AppendUint(&w, c.Weight) + } + return w +} + +const ( + canoto__BlockValidationDescriptor__AggregatedMembership = 1 + + canoto__BlockValidationDescriptor__AggregatedMembership__tag = "\x0a" // canoto.Tag(canoto__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.TypeOf(BlockValidationDescriptor{})) + var zero BlockValidationDescriptor + s := &canoto.Spec{ + Name: "BlockValidationDescriptor", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.AggregatedMembership), + /*FieldNumber: */ canoto__BlockValidationDescriptor__AggregatedMembership, + /*Name: */ "AggregatedMembership", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*BlockValidationDescriptor) MakeCanoto() *BlockValidationDescriptor { + return new(BlockValidationDescriptor) +} + +// 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 canoto__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 == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + (&c.AggregatedMembership).CalculateCanotoCache() + if fieldSize := (&c.AggregatedMembership).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__BlockValidationDescriptor__AggregatedMembership__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if fieldSize := (&c.AggregatedMembership).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__BlockValidationDescriptor__AggregatedMembership__tag) + canoto.AppendUint(&w, fieldSize) + w = (&c.AggregatedMembership).MarshalCanotoInto(w) + } + return w +} + +const ( + canoto__AggregatedMembership__Members = 1 + + canoto__AggregatedMembership__Members__tag = "\x0a" // canoto.Tag(canoto__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.TypeOf(AggregatedMembership{})) + var zero AggregatedMembership + s := &canoto.Spec{ + Name: "AggregatedMembership", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.Members)), + /*FieldNumber: */ canoto__AggregatedMembership__Members, + /*Name: */ "Members", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*AggregatedMembership) MakeCanoto() *AggregatedMembership { + return new(AggregatedMembership) +} + +// 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 canoto__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, canoto__AggregatedMembership__Members__tag) + 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(canoto__AggregatedMembership__Members__tag):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + continue + } + r.Unsafe = originalUnsafe + + 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 { + if c == nil { + return true + } + { + 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() { + if c == nil { + return + } + var size uint64 + { + field := c.Members + for i := range field { + (&field[i]).CalculateCanotoCache() + fieldSize := (&field[i]).CachedCanotoSize() + size += uint64(len(canoto__AggregatedMembership__Members__tag)) + 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 { + if c == nil { + return 0 + } + 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 { + if c == nil { + return w + } + { + field := c.Members + for i := range field { + canoto.Append(&w, canoto__AggregatedMembership__Members__tag) + canoto.AppendUint(&w, (&field[i]).CachedCanotoSize()) + w = (&field[i]).MarshalCanotoInto(w) + } + } + return w +} + +const ( + canoto__NextEpochApprovals__NodeIDs = 1 + canoto__NextEpochApprovals__Signature = 2 + + canoto__NextEpochApprovals__NodeIDs__tag = "\x0a" // canoto.Tag(canoto__NextEpochApprovals__NodeIDs, canoto.Len) + canoto__NextEpochApprovals__Signature__tag = "\x12" // canoto.Tag(canoto__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: canoto__NextEpochApprovals__NodeIDs, + Name: "NodeIDs", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: canoto__NextEpochApprovals__Signature, + Name: "Signature", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*NextEpochApprovals) MakeCanoto() *NextEpochApprovals { + return new(NextEpochApprovals) +} + +// 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 canoto__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 canoto__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 { + if c == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if len(c.NodeIDs) != 0 { + size += uint64(len(canoto__NextEpochApprovals__NodeIDs__tag)) + canoto.SizeBytes(c.NodeIDs) + } + if len(c.Signature) != 0 { + size += uint64(len(canoto__NextEpochApprovals__Signature__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if len(c.NodeIDs) != 0 { + canoto.Append(&w, canoto__NextEpochApprovals__NodeIDs__tag) + canoto.AppendBytes(&w, c.NodeIDs) + } + if len(c.Signature) != 0 { + canoto.Append(&w, canoto__NextEpochApprovals__Signature__tag) + canoto.AppendBytes(&w, c.Signature) + } + return w +} + +const ( + canoto__ValidatorSetApproval__NodeID = 1 + canoto__ValidatorSetApproval__AuxInfoSeqDigest = 2 + canoto__ValidatorSetApproval__PChainHeight = 3 + canoto__ValidatorSetApproval__Signature = 4 + + canoto__ValidatorSetApproval__NodeID__tag = "\x0a" // canoto.Tag(canoto__ValidatorSetApproval__NodeID, canoto.Len) + canoto__ValidatorSetApproval__AuxInfoSeqDigest__tag = "\x12" // canoto.Tag(canoto__ValidatorSetApproval__AuxInfoSeqDigest, canoto.Len) + canoto__ValidatorSetApproval__PChainHeight__tag = "\x18" // canoto.Tag(canoto__ValidatorSetApproval__PChainHeight, canoto.Varint) + canoto__ValidatorSetApproval__Signature__tag = "\x22" // canoto.Tag(canoto__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: canoto__ValidatorSetApproval__NodeID, + Name: "NodeID", + OneOf: "", + TypeFixedBytes: uint64(len(zero.NodeID)), + }, + { + FieldNumber: canoto__ValidatorSetApproval__AuxInfoSeqDigest, + Name: "AuxInfoSeqDigest", + OneOf: "", + TypeFixedBytes: uint64(len(zero.AuxInfoSeqDigest)), + }, + { + FieldNumber: canoto__ValidatorSetApproval__PChainHeight, + Name: "PChainHeight", + OneOf: "", + TypeUint: canoto.SizeOf(zero.PChainHeight), + }, + { + FieldNumber: canoto__ValidatorSetApproval__Signature, + Name: "Signature", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*ValidatorSetApproval) MakeCanoto() *ValidatorSetApproval { + return new(ValidatorSetApproval) +} + +// 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 canoto__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 canoto__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 canoto__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 canoto__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 { + if c == nil { + return true + } + 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() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.NodeID) { + size += uint64(len(canoto__ValidatorSetApproval__NodeID__tag)) + canoto.SizeBytes((&c.NodeID)[:]) + } + if !canoto.IsZero(c.AuxInfoSeqDigest) { + size += uint64(len(canoto__ValidatorSetApproval__AuxInfoSeqDigest__tag)) + canoto.SizeBytes((&c.AuxInfoSeqDigest)[:]) + } + if !canoto.IsZero(c.PChainHeight) { + size += uint64(len(canoto__ValidatorSetApproval__PChainHeight__tag)) + canoto.SizeUint(c.PChainHeight) + } + if len(c.Signature) != 0 { + size += uint64(len(canoto__ValidatorSetApproval__Signature__tag)) + 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 { + if c == nil { + return 0 + } + 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 c == nil { + return w + } + if !canoto.IsZero(c.NodeID) { + canoto.Append(&w, canoto__ValidatorSetApproval__NodeID__tag) + canoto.AppendBytes(&w, (&c.NodeID)[:]) + } + if !canoto.IsZero(c.AuxInfoSeqDigest) { + canoto.Append(&w, canoto__ValidatorSetApproval__AuxInfoSeqDigest__tag) + canoto.AppendBytes(&w, (&c.AuxInfoSeqDigest)[:]) + } + if !canoto.IsZero(c.PChainHeight) { + canoto.Append(&w, canoto__ValidatorSetApproval__PChainHeight__tag) + canoto.AppendUint(&w, c.PChainHeight) + } + if len(c.Signature) != 0 { + canoto.Append(&w, canoto__ValidatorSetApproval__Signature__tag) + canoto.AppendBytes(&w, c.Signature) + } + return w +} diff --git a/msm/encoding.go b/msm/encoding.go new file mode 100644 index 00000000..b3c6d9bf --- /dev/null +++ b/msm/encoding.go @@ -0,0 +1,347 @@ +// 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 tool 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 { + // ICMEpochInfo is the metadata that the StateMachine uses for ICM epoching. + ICMEpochInfo ICMEpochInfo `canoto:"value,1"` + // SimplexEpochInfo is the metadata that the StateMachine uses for its own epoching. + SimplexEpochInfo SimplexEpochInfo `canoto:"value,2"` + // SimplexProtocolMetadata is the metadata that Simplex uses for its protocol, such as sequence and round number. + SimplexProtocolMetadata []byte `canoto:"bytes,3"` + // SimplexBlacklist is the metadata that Simplex uses to keep track of blacklisted nodes. + // Blacklisted nodes do not become leaders. + SimplexBlacklist []byte `canoto:"bytes,4"` + // 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,5"` + // 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,6"` + // Timestamp is the time when the block is being built, in milliseconds since Unix epoch. + Timestamp uint64 `canoto:"uint,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 +} + +// 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. + PrevAuxInfoSeq uint64 `canoto:"uint,2"` + // ApplicationID is an identifier that identifies the application. + // Can be used for backward-compatibility and upgrade purposes. + ApplicationID uint32 `canoto:"uint,3"` + + canotoData canotoData_AuxiliaryInfo +} + +// 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 sei == nil + } + 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) { + return nbms.SumWeights(func(int, NodeBLSMapping) bool { + return true + }) +} + +func (nbms NodeBLSMappings) ForEach(selector func(int, NodeBLSMapping)) { + for i, nbm := range nbms { + selector(i, nbm) + } +} + +func (nbms NodeBLSMappings) SumWeights(selector func(int, NodeBLSMapping) bool) (uint64, error) { + var total uint64 + var err error + nbms.ForEach(func(i int, nbm NodeBLSMapping) { + if err != nil { + return + } + if selector(i, nbm) { + total, err = safeAdd(total, nbm.Weight) + } + }) + return total, err +} + +func (nbms NodeBLSMappings) Equal(other NodeBLSMappings) bool { + if len(nbms) != len(other) { + return false + } + + nbmsClone := nbms.Clone() + otherClone := other.Clone() + + slices.SortFunc(nbmsClone, func(a, b NodeBLSMapping) int { + return slices.Compare(a.NodeID[:], b.NodeID[:]) + }) + + slices.SortFunc(otherClone, func(a, b NodeBLSMapping) int { + return slices.Compare(a.NodeID[:], b.NodeID[:]) + }) + + for i := range nbmsClone { + if !nbmsClone[i].Equals(&otherClone[i]) { + return false + } + } + return true +} + +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) ForEach(f func(int, ValidatorSetApproval)) { + for i, v := range vsa { + f(i, v) + } +} + +func (vsa ValidatorSetApprovals) Filter(f func(int, ValidatorSetApproval) bool) ValidatorSetApprovals { + result := make(ValidatorSetApprovals, 0, len(vsa)) + vsa.ForEach(func(i int, v ValidatorSetApproval) { + if f(i, v) { + result = append(result, v) + } + }) + return result +} + +func (vsa ValidatorSetApprovals) UniqueByNodeID() ValidatorSetApprovals { + seen := make(map[nodeID]struct{}) + result := make(ValidatorSetApprovals, 0, len(vsa)) + vsa.ForEach(func(i int, v ValidatorSetApproval) { + if _, exists := seen[v.NodeID]; !exists { + seen[v.NodeID] = struct{}{} + result = append(result, v) + } + }) + return result +} \ No newline at end of file diff --git a/msm/encoding_test.go b/msm/encoding_test.go new file mode 100644 index 00000000..8e2be3d6 --- /dev/null +++ b/msm/encoding_test.go @@ -0,0 +1,578 @@ +// 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 TestICMEpochInfoEqual(t *testing.T) { + tests := []struct { + name string + a *ICMEpochInfo + b *ICMEpochInfo + expected bool + }{ + { + name: "both nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "first nil second non-nil", + a: nil, + b: &ICMEpochInfo{}, + expected: false, + }, + { + name: "first non-nil second nil", + a: &ICMEpochInfo{}, + b: nil, + expected: false, + }, + { + name: "both zero", + a: &ICMEpochInfo{}, + b: &ICMEpochInfo{}, + expected: true, + }, + { + name: "equal with values", + a: &ICMEpochInfo{EpochStartTime: 100, EpochNumber: 5, PChainEpochHeight: 200}, + b: &ICMEpochInfo{EpochStartTime: 100, EpochNumber: 5, PChainEpochHeight: 200}, + expected: true, + }, + { + name: "different EpochStartTime", + a: &ICMEpochInfo{EpochStartTime: 100}, + b: &ICMEpochInfo{EpochStartTime: 200}, + expected: false, + }, + { + name: "different EpochNumber", + a: &ICMEpochInfo{EpochNumber: 1}, + b: &ICMEpochInfo{EpochNumber: 2}, + expected: false, + }, + { + name: "different PChainEpochHeight", + a: &ICMEpochInfo{PChainEpochHeight: 1}, + b: &ICMEpochInfo{PChainEpochHeight: 2}, + 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 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 TestNodeBLSMappingsSumWeights(t *testing.T) { + mappings := NodeBLSMappings{ + {NodeID: nodeID{1}, Weight: 10}, + {NodeID: nodeID{2}, Weight: 20}, + {NodeID: nodeID{3}, Weight: 30}, + } + + // Select only even indices + total, err := mappings.SumWeights(func(i int, _ NodeBLSMapping) bool { + return i%2 == 0 + }) + require.NoError(t, err) + require.Equal(t, uint64(40), total) // index 0 (10) + index 2 (30) + + // Select none + total, err = mappings.SumWeights(func(int, NodeBLSMapping) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, uint64(0), total) +} + +func TestNodeBLSMappingsForEach(t *testing.T) { + mappings := NodeBLSMappings{ + {Weight: 1}, + {Weight: 2}, + {Weight: 3}, + } + + var visited []uint64 + mappings.ForEach(func(_ int, nbm NodeBLSMapping) { + visited = append(visited, nbm.Weight) + }) + require.Equal(t, []uint64{1, 2, 3}, visited) +} + +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 TestValidatorSetApprovalsForEach(t *testing.T) { + approvals := ValidatorSetApprovals{ + {NodeID: nodeID{1}, PChainHeight: 10}, + {NodeID: nodeID{2}, PChainHeight: 20}, + } + + var heights []uint64 + approvals.ForEach(func(_ int, v ValidatorSetApproval) { + heights = append(heights, v.PChainHeight) + }) + require.Equal(t, []uint64{10, 20}, heights) +} + +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(_ int, 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(int, ValidatorSetApproval) bool { + return false + }) + require.Empty(t, filtered) +} \ No newline at end of file diff --git a/msm/fake_node_test.go b/msm/fake_node_test.go new file mode 100644 index 00000000..f8927249 --- /dev/null +++ b/msm/fake_node_test.go @@ -0,0 +1,429 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata_test + +import ( + "context" + "crypto/rand" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/ava-labs/simplex" + metadata "github.com/ava-labs/simplex/msm" + "github.com/stretchr/testify/require" +) + +func TestFakeNode(t *testing.T) { + validatorSetRetriever := validatorSetRetriever{ + resultMap: map[uint64]metadata.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 := newFakeNode(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: []metadata.ValidatorSetApproval{{NodeID: [20]byte{1}, PChainHeight: 200, Signature: []byte{1}, AuxInfoSeqDigest: [32]byte{}}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []metadata.ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 200, Signature: []byte{2}, AuxInfoSeqDigest: [32]byte{}}}, + } + } + } + + 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: []metadata.ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 300, Signature: []byte{2}, AuxInfoSeqDigest: [32]byte{}}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []metadata.ValidatorSetApproval{{NodeID: [20]byte{3}, PChainHeight: 300, Signature: []byte{3}, AuxInfoSeqDigest: [32]byte{}}}, + } + } + } + + t.Log("Epoch:", node.Epoch()) + require.Greater(t, node.Epoch(), epoch) +} + +func TestFakeNodeEmptyMempool(t *testing.T) { + validatorSetRetriever := validatorSetRetriever{ + resultMap: map[uint64]metadata.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 := newFakeNode(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.finalizedBlocks[len(node.finalizedBlocks)-1].Metadata.SimplexEpochInfo.BlockValidationDescriptor == nil { + node.act() + if flipCoin() { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []metadata.ValidatorSetApproval{{NodeID: [20]byte{1}, PChainHeight: 200, Signature: []byte{1}, AuxInfoSeqDigest: [32]byte{}}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []metadata.ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 200, Signature: []byte{2}, AuxInfoSeqDigest: [32]byte{}}}, + } + } + } + + 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: []metadata.ValidatorSetApproval{{NodeID: [20]byte{2}, PChainHeight: 300, Signature: []byte{2}, AuxInfoSeqDigest: [32]byte{}}}, + } + } else { + node.sm.ApprovalsRetriever = &approvalsRetriever{ + result: []metadata.ValidatorSetApproval{{NodeID: [20]byte{3}, PChainHeight: 300, Signature: []byte{3}, AuxInfoSeqDigest: [32]byte{}}}, + } + } + } + + t.Log("Epoch:", node.Epoch()) + require.Greater(t, node.Epoch(), epoch) + require.Equal(t, node.Height(), uint64(30)) +} + +type innerBlock struct { + metadata.InnerBlock + Prev [32]byte +} + +type fakeNode struct { + t *testing.T + sm metadata.StateMachine + mempoolEmpty bool + notarizedBlocks []metadata.StateMachineBlock + finalizedBlocks []metadata.StateMachineBlock + innerChain []innerBlock +} + +func (fn *fakeNode) 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 *fakeNode) WaitForPendingBlock(ctx context.Context) { + if fn.mempoolEmpty { + <-ctx.Done() + return + } +} + +func newFakeNode(t *testing.T) *fakeNode { + sm, _ := newStateMachine(t) + + fn := &fakeNode{ + t: t, + sm: sm, + } + + fn.sm.BlockBuilder = fn + fn.sm.PChainProgressListener = fn + + fn.sm.GetBlock = func(opts metadata.RetrievingOpts) (metadata.StateMachineBlock, *simplex.Finalization, error) { + if opts.Height == 0 { + return genesisBlock, nil, nil + } + for _, block := range fn.finalizedBlocks { + if block.Digest() == opts.Digest { + return block, &simplex.Finalization{}, nil + } + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + if err != nil { + return metadata.StateMachineBlock{}, nil, err + } + if md.Seq == opts.Height { + return block, &simplex.Finalization{}, nil + } + } + for _, block := range fn.notarizedBlocks { + if block.Digest() == opts.Digest { + return block, nil, nil + } + md, err := simplex.ProtocolMetadataFromBytes(block.Metadata.SimplexProtocolMetadata) + if err != nil { + return metadata.StateMachineBlock{}, nil, err + } + if md.Seq == opts.Height { + return block, nil, nil + } + } + + require.Failf(t, "not found block", "height: %d", opts.Height) + return metadata.StateMachineBlock{}, nil, fmt.Errorf("block not found") + } + + return fn +} + +func (fn *fakeNode) Height() uint64 { + return uint64(len(fn.finalizedBlocks)) +} + +func (fn *fakeNode) Epoch() uint64 { + return fn.notarizedBlocks[len(fn.notarizedBlocks)-1].Metadata.SimplexEpochInfo.EpochNumber +} + +func (fn *fakeNode) act() { + if fn.canFinalize() && flipCoin() { + fn.tryFinalizeNextBlock() + return + } + + if flipCoin() { + return + } + + fn.buildAndNotarizeBlock() +} + +func (fn *fakeNode) canFinalize() bool { + return len(fn.notarizedBlocks) > len(fn.finalizedBlocks) +} + +func (fn *fakeNode) tryFinalizeNextBlock() { + nextIndex := len(fn.finalizedBlocks) + + if fn.isNextBlockTelock() { + return + } + + block := fn.notarizedBlocks[nextIndex] + fn.finalizedBlocks = append(fn.finalizedBlocks, 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.notarizedBlocks = fn.notarizedBlocks[:len(fn.finalizedBlocks)] + fn.t.Logf("Trimmed notarized blocks, new length: %d", len(fn.notarizedBlocks)) + } +} + +func (fn *fakeNode) isNextBlockTelock() bool { + if len(fn.finalizedBlocks) == 0 { + return false + } + return fn.notarizedBlocks[len(fn.finalizedBlocks)].Metadata.SimplexEpochInfo.SealingBlockSeq > 0 +} + +func (fn *fakeNode) buildAndNotarizeBlock() { + vmBlock, block := fn.buildBlock() + require.NoError(fn.t, fn.sm.VerifyBlock(context.Background(), block)) + + fn.notarizedBlocks = append(fn.notarizedBlocks, *block) + + if vmBlock != nil { + fn.innerChain = append(fn.innerChain, *vmBlock.(*innerBlock)) + } +} + +func (fn *fakeNode) buildBlock() (metadata.VMBlock, *metadata.StateMachineBlock) { + parentBlock := fn.getParentBlock() + + lastMD, prevBlockDigest := fn.prepareMetadataAndPrevBlockDigest() + + _, finalizatin, err := fn.sm.GetBlock(metadata.RetrievingOpts{ + Digest: prevBlockDigest, + Height: lastMD.Seq, + }) + require.NoError(fn.t, err) + + finalizedString := "not finalized" + if finalizatin != 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, + }, nil) + require.NoError(fn.t, err) + + return block.InnerBlock, block +} + +func (fn *fakeNode) prepareMetadataAndPrevBlockDigest() (*simplex.ProtocolMetadata, [32]byte) { + var lastMD *simplex.ProtocolMetadata + var err error + lastBlockDigest := genesisBlock.Digest() + if len(fn.notarizedBlocks) > 0 { + lastBlock := fn.notarizedBlocks[len(fn.notarizedBlocks)-1] + 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 *fakeNode) BuildBlock(context.Context, uint64) (metadata.VMBlock, error) { + // Count the number of inner blocks in the chain + var count int + for _, block := range fn.notarizedBlocks { + if block.InnerBlock != nil { + count++ + } + } + + vmBlock := &innerBlock{ + Prev: fn.getLastVMBlockDigest(), + InnerBlock: metadata.InnerBlock{ + Bytes: randomBuff(10), + TS: time.Now(), + BlockHeight: uint64(count), + }, + } + return vmBlock, nil +} + +func (fn *fakeNode) getParentBlock() metadata.StateMachineBlock { + var parentBlock metadata.StateMachineBlock + if len(fn.notarizedBlocks) > 0 { + parentBlock = fn.notarizedBlocks[len(fn.notarizedBlocks)-1] + } else { + gb := genesisBlock.InnerBlock.(*metadata.InnerBlock) + parentBlock = metadata.StateMachineBlock{ + InnerBlock: &innerBlock{ + InnerBlock: *gb, + }, + } + } + return parentBlock +} + +func (fn *fakeNode) getLastVMBlockDigest() [32]byte { + var lastVMBlockDigest = genesisBlock.Digest() + + notarizedBlocks := fn.notarizedBlocks + for len(notarizedBlocks) > 0 { + lastNotarizedBlock := notarizedBlocks[len(notarizedBlocks)-1] + if lastNotarizedBlock.InnerBlock == nil { + notarizedBlocks = notarizedBlocks[:len(notarizedBlocks)-1] + continue + } + lastVMBlockDigest = lastNotarizedBlock.Digest() + break + } + return lastVMBlockDigest +} + +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/misc.go b/msm/misc.go new file mode 100644 index 00000000..08a4a47d --- /dev/null +++ b/msm/misc.go @@ -0,0 +1,122 @@ +// 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) 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/msm.go b/msm/msm.go new file mode 100644 index 00000000..79b0c991 --- /dev/null +++ b/msm/msm.go @@ -0,0 +1,1007 @@ +// 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) +} + +// 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) + +// RetrievingOpts specifies the options for retrieving a block by height and/or digest. +type RetrievingOpts struct { + // Height is the sequence number of the block to retrieve. + Height uint64 + // Digest is the expected hash of the block, used for validation. + Digest [32]byte + // ShouldBeFinalized indicates whether the block being retrieved is expected to be finalized. + // This optimizes retrieval by allowing the retriever to go directly to persisted storage. + ShouldBeFinalized bool +} + +// BlockRetriever retrieves a block and its finalization status given the retrieval options. +// If the block cannot be found it returns ErrBlockNotFound. +// If an error occurs during retrieval, it returns a non-nil error. +type BlockRetriever func(RetrievingOpts) (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) +} + +// StateMachine manages block building and verification across epoch transitions. +type StateMachine struct { + // 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 + + // initialized tracks whether the state machine has been initialized. + // This is used to lazily initialize the verifiers. + initialized bool + + // 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 +} + +type state uint8 + +const ( + stateFirstSimplexBlock state = iota + stateBuildBlockNormalOp + stateBuildCollectingApprovals + stateBuildBlockEpochSealed +) + +type BlockType uint8 + +const ( + BlockTypeNormal BlockType = iota + BlockTypeTelock + BlockTypeSealing + BlockTypeNewEpoch +) + +// 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, simplexBlacklist *simplex.Blacklist) (*StateMachineBlock, error) { + sm.maybeInit() + + // 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), + ) + }() + + var simplexBlacklistBytes []byte + if simplexBlacklist != nil { + simplexBlacklistBytes = simplexBlacklist.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, err := sm.identifyCurrentState(parentBlock.Metadata.SimplexEpochInfo) + if err != nil { + return nil, err + } + + simplexMetadataBytes := simplexMetadata.Bytes() + prevBlockSeq := simplexMetadata.Seq - 1 + + switch currentState { + case stateFirstSimplexBlock: + return sm.buildBlockZero(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes) + case stateBuildBlockNormalOp: + return sm.buildBlockNormalOp(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + case stateBuildCollectingApprovals: + return sm.buildBlockCollectingApprovals(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + case stateBuildBlockEpochSealed: + return sm.buildBlockEpochSealed(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) + 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 { + sm.maybeInit() + + 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(RetrievingOpts{Digest: pmd.Prev, Height: seq - 1}) + if err != nil { + return fmt.Errorf("failed to retrieve previous (%d) inner block: %w", seq-1, err) + } + + prevMD := prevBlock.Metadata + currentState, err := sm.identifyCurrentState(prevMD.SimplexEpochInfo) + if err != nil { + return fmt.Errorf("failed to identify previous state: %w", err) + } + + switch currentState { + case stateFirstSimplexBlock: + err = sm.verifyBlockZero(ctx, block, prevBlock) + default: + err = sm.verifyNonZeroBlock(ctx, block, prevBlock.Metadata, prevMD, currentState, seq-1) + } + return err +} + +func (sm *StateMachine) maybeInit() { + if sm.initialized { + return + } + sm.init() + sm.initialized = true +} + +func (sm *StateMachine) init() { + sm.verifiers = []verifier{ + &icmEpochInfoVerifier{ + computeICMEpoch: sm.ComputeICMEpoch, + getUpdates: sm.GetUpgrades, + }, + &pChainHeightVerifier{ + getPChainHeight: sm.GetPChainHeight, + }, + ×tampVerifier{ + timeSkewLimit: sm.TimeSkewLimit, + getTime: sm.GetTime, + }, + &pChainReferenceHeightVerifier{}, + &epochNumberVerifier{}, + &prevSealingBlockHashVerifier{ + getBlock: sm.GetBlock, + latestPersistedHeight: &sm.LatestPersistedHeight, + }, + &nextPChainReferenceHeightVerifier{ + getPChainHeight: sm.GetPChainHeight, + getValidatorSet: sm.GetValidatorSet, + }, + &vmBlockSeqVerifier{ + getBlock: sm.GetBlock, + }, + &validationDescriptorVerifier{ + getValidatorSet: sm.GetValidatorSet, + }, + &nextEpochApprovalsVerifier{ + getValidatorSet: sm.GetValidatorSet, + keyAggregator: sm.KeyAggregator, + sigVerifier: sm.SignatureVerifier, + }, + &sealingBlockSeqVerifier{}, + } +} + +func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block *StateMachineBlock, prevBlockMD StateMachineMetadata, prevMD StateMachineMetadata, state state, prevSeq uint64) error { + blockType := IdentifyBlockType(block.Metadata, prevBlockMD, prevSeq) + timestamp := time.UnixMilli(int64(prevMD.Timestamp)) + + if block.InnerBlock != nil { + timestamp = block.InnerBlock.Timestamp() + } + + for _, verifier := range sm.verifiers { + if err := verifier.Verify(verificationInput{ + proposedBlockMD: block.Metadata, + nextBlockType: blockType, + prevMD: prevMD, + state: state, + prevBlockSeq: prevSeq, + hasInnerBlock: block.InnerBlock != nil, + proposedBlockTimestamp: timestamp, + }); err != nil { + return err + } + } + + if block.InnerBlock == nil { + return nil + } + + return block.InnerBlock.Verify(ctx) +} + +func (sm *StateMachine) identifyCurrentState(prevBlockSimplexEpochInfo SimplexEpochInfo) (state, error) { + // If this is the first ever epoch, then this is also the first ever block to be built by Simplex. + if prevBlockSimplexEpochInfo.EpochNumber == 0 { + return stateFirstSimplexBlock, nil + } + + // If we don't have a next P-chain preference height, it means we are not transitioning to a new epoch just yet. + if prevBlockSimplexEpochInfo.NextPChainReferenceHeight == 0 { + return stateBuildBlockNormalOp, nil + } + + // 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. + if prevBlockSimplexEpochInfo.SealingBlockSeq > 0 || prevBlockSimplexEpochInfo.BlockValidationDescriptor != nil { + return stateBuildBlockEpochSealed, nil + } + + // 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, 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 + } + // or it has an inner block, in which case it is the previous block sequence. + return prevBlockSeq +} + +// buildBlockNormalOp builds a block while not trying to transition to a new epoch. +func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*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, prevBlockSeq), + } + + blockBuildingDecider := sm.createBlockBuildingDecider(parentBlock) + decisionToBuildBlock, pChainHeight, err := blockBuildingDecider.shouldBuildBlock(ctx) + if err != nil { + return nil, err + } + + sm.Logger.Debug("Block building decision", zap.Stringer("decision", decisionToBuildBlock)) + + var childBlock VMBlock + + switch decisionToBuildBlock { + case blockBuildingDecisionBuildBlock, blockBuildingDecisionBuildBlockAndTransitionEpoch: + // If we reached here, we need to build a new block, and maybe also transition to a new epoch. + return sm.buildBlockAndMaybeTransitionEpoch(ctx, parentBlock, simplexMetadata, simplexBlacklist, childBlock, decisionToBuildBlock, newSimplexEpochInfo, pChainHeight) + 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, simplexBlacklist), nil + case blockBuildingDecisionContextCanceled: + return nil, ctx.Err() + default: + return nil, fmt.Errorf("unknown block building decision %d", decisionToBuildBlock) + } +} + +func (sm *StateMachine) createBlockBuildingDecider(parentBlock StateMachineBlock) blockBuildingDecider { + blockBuildingDecider := 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 + }, + } + return blockBuildingDecider +} + +func (sm *StateMachine) buildBlockAndMaybeTransitionEpoch(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, childBlock VMBlock, decisionToBuildBlock blockBuildingDecision, newSimplexEpochInfo SimplexEpochInfo, pChainHeight uint64) (*StateMachineBlock, error) { + childBlock, err := sm.BlockBuilder.BuildBlock(ctx, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) + if err != nil { + return nil, err + } + + if decisionToBuildBlock == blockBuildingDecisionBuildBlockAndTransitionEpoch { + // 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, simplexBlacklist), nil +} + +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 { + return BlockTypeNewEpoch + } + 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. + // [ 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 probably a block in the middle of the epoch, + // not in the edges. + return BlockTypeNormal +} + +// 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, simplexBlacklist []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, simplexBlacklist, 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 +} + +// 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 (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*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, prevBlockSeq), + } + + // 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 either don't 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, simplexBlacklist, newSimplexEpochInfo, pChainHeight) + } + + // Else, we have enough approvals to seal the epoch, so we create the sealing block. + return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, newApprovals, 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 []byte, simplexBlacklist []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)) + } + return sm.wrapBlock(parentBlock, childBlock, simplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil +} + +func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata []byte, simplexBlacklist []byte, simplexEpochInfo SimplexEpochInfo, newApprovals *approvals, pChainHeight uint64) (*StateMachineBlock, error) { + 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, _, err := sm.GetBlock(RetrievingOpts{Height: simplexEpochInfo.EpochNumber, ShouldBeFinalized: true}) + 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) + } + 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(RetrievingOpts{Height: firstSimplexBlock}) + 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, simplexBlacklist, simplexEpochInfo, pChainHeight) +} + +func computeNewApprovals( + nextEpochApprovals *NextEpochApprovals, + auxInfo *AuxiliaryInfo, + newApprovals ValidatorSetApprovals, + pChainHeight uint64, + aggregator SignatureAggregator, + validators NodeBLSMappings, +) (*approvals, error) { + nodeID2ValidatorIndex := make(map[nodeID]int) + validators.ForEach(func(i int, nbm NodeBLSMapping) { + nodeID2ValidatorIndex[nbm.NodeID] = i + }) + + var candidateAuxInfoDigest [32]byte + if auxInfo != nil { + candidateAuxInfoDigest = sha256.Sum256(auxInfo.Info) + } + + newApprovals = newApprovals.Filter(func(i int, approval ValidatorSetApproval) bool { + // Pick only approvals that agree with our candidate auxiliary info digest and P-Chain height + return approval.PChainHeight == pChainHeight && approval.AuxInfoSeqDigest == candidateAuxInfoDigest + }) + + // If there are multiple approvals from the same node, we only keep one of them. + newApprovals = newApprovals.UniqueByNodeID() + + if nextEpochApprovals == nil { + nextEpochApprovals = &NextEpochApprovals{} + } + existingApprovingNodes := bitmaskFromBytes(nextEpochApprovals.NodeIDs) + + newApprovals = newApprovals.Filter(func(i int, 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 !existingApprovingNodes.Contains(approvingNodeIndexOfNewApprover) + }) + + newApprovingNodes := existingApprovingNodes + + // 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(newApprovals)+1) + + newApprovals.ForEach(func(i int, approval ValidatorSetApproval) { + approvingNodeIndexOfNewApprover, exits := nodeID2ValidatorIndex[approval.NodeID] + if !exits { + // This should not happen, because we have already filtered approvals that are not in the validator set, but we check just in case. + return + } + // 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) + } + + aggregatedSignature, err := aggregator.AggregateSignatures(newSignatures...) + if err != nil { + return nil, fmt.Errorf("failed to aggregate signatures: %w", err) + } + + approvingWeight, err := computeApprovingWeight(validators, &newApprovingNodes) + if err != nil { + return nil, err + } + + totalWeight, err := computeTotalWeight(validators) + if err != nil { + return nil, err + } + + threshold := big.NewRat(2, 3) + + approvingRatio := big.NewRat(approvingWeight, totalWeight) + + canSeal := approvingRatio.Cmp(threshold) > 0 + + return &approvals{ + canSeal: canSeal, + signature: aggregatedSignature, + nodeIDs: newApprovingNodes.Bytes(), + }, 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 (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*StateMachineBlock, error) { + // We check if the sealing block has already been finalized. + // If not, we build a Telock block. + + 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(RetrievingOpts{Height: sealingBlockSeq}) + 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, simplexBlacklist), 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, simplexBlacklist), 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 +} + +// 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, simplexBlacklist []byte) *StateMachineBlock { + parentMetadata := parentBlock.Metadata + timestamp := parentMetadata.Timestamp + + hasChildBlock := childBlock != nil + getUpgrades := sm.GetUpgrades + icmEpochTransition := sm.ComputeICMEpoch + + var newTimestamp time.Time + // nextICMEpoch returns the parent's ICM epoch info if hasChildBlock is false. + if hasChildBlock { + newTimestamp = childBlock.Timestamp() + timestamp = uint64(newTimestamp.UnixMilli()) + } + + icmEpochInfo := nextICMEpochInfo(parentMetadata, hasChildBlock, getUpgrades, icmEpochTransition, newTimestamp) + + return &StateMachineBlock{ + InnerBlock: childBlock, + Metadata: StateMachineMetadata{ + Timestamp: timestamp, + SimplexProtocolMetadata: simplexMetadata, + SimplexBlacklist: simplexBlacklist, + SimplexEpochInfo: newSimplexEpochInfo, + PChainHeight: pChainHeight, + ICMEpochInfo: icmEpochInfo, + }, + } +} + +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 + firstSimplexBlock := sort.Search(int(endHeight+1), func(i int) bool { + if haltError != nil { + return true + } + block, _, err := getBlock(RetrievingOpts{Height: uint64(i)}) + 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 +} + +type approvals struct { + canSeal bool + nodeIDs []byte + signature []byte +} diff --git a/msm/msm_test.go b/msm/msm_test.go new file mode 100644 index 00000000..589ce956 --- /dev/null +++ b/msm/msm_test.go @@ -0,0 +1,1054 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata_test + +import ( + "context" + "crypto/rand" + "encoding/asn1" + "fmt" + "testing" + "time" + + "github.com/ava-labs/simplex" + metadata "github.com/ava-labs/simplex/msm" + "github.com/ava-labs/simplex/testutil" + "github.com/stretchr/testify/require" +) + +type outerBlock struct { + finalization *simplex.Finalization + block metadata.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(opts metadata.RetrievingOpts) (metadata.StateMachineBlock, *simplex.Finalization, error) { + blk, exits := bs[opts.Height] + if !exits { + return metadata.StateMachineBlock{}, nil, fmt.Errorf("%w: block %d not found", simplex.ErrBlockNotFound, opts.Height) + } + return blk.block, blk.finalization, nil +} + +type approvalsRetriever struct { + result metadata.ValidatorSetApprovals +} + +func (a approvalsRetriever) RetrieveApprovals() metadata.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 metadata.VMBlock + err error +} + +func (bb *blockBuilder) WaitForPendingBlock(_ context.Context) { + // Block is always ready in tests. +} + +func (bb *blockBuilder) BuildBlock(_ context.Context, _ uint64) (metadata.VMBlock, error) { + return bb.block, bb.err +} + +type validatorSetRetriever struct { + result metadata.NodeBLSMappings + resultMap map[uint64]metadata.NodeBLSMappings + err error +} + +func (vsr *validatorSetRetriever) getValidatorSet(height uint64) (metadata.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 +} + +var ( + genesisBlock = metadata.StateMachineBlock{ + // Genesis block metadata has all zero values + InnerBlock: &metadata.InnerBlock{ + TS: time.Now(), + Bytes: []byte{1, 2, 3}, + }, + } +) + +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(*metadata.StateMachine, *testConfig) + mutateBlock func(*metadata.StateMachineBlock) + }{ + { + name: "correct information", + md: validMD, + }, + { + name: "trying to build a genesis block", + md: validMD, + mutateBlock: func(block *metadata.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(_ *metadata.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(_ *metadata.StateMachine, tc *testConfig) { + tc.blockStore[0] = &outerBlock{ + block: metadata.StateMachineBlock{}, + } + }, + err: "parent inner block (", + }, + { + name: "wrong epoch number", + md: validMD, + mutateBlock: func(block *metadata.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 *metadata.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(_ *metadata.StateMachine, tc *testConfig) { + tc.blockStore[0] = &outerBlock{ + block: metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{TS: time.Now(), Bytes: []byte{1, 2, 3}}, + Metadata: 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(_ *metadata.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 *metadata.StateMachineBlock) { + block.Metadata.SimplexEpochInfo.BlockValidationDescriptor = nil + }, + err: "invalid BlockValidationDescriptor: should not be nil", + }, + { + name: "membership mismatch", + md: validMD, + configure: func(_ *metadata.StateMachine, tc *testConfig) { + tc.validatorSetRetriever.result = metadata.NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, + } + }, + err: "invalid BlockValidationDescriptor: should match validator set", + }, + { + name: "SimplexEpochInfo mismatch", + md: validMD, + mutateBlock: func(block *metadata.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, nil) + 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 TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { + preSimplexParent := metadata.StateMachineBlock{ + InnerBlock: &metadata.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: 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 = &metadata.InnerBlock{ + TS: time.Now(), + BlockHeight: 43, + Bytes: []byte{7, 8, 9}, + } + + block, err := sm1.BuildBlock(context.Background(), preSimplexParent, md, nil) + require.NoError(t, err) + require.NotNil(t, block) + + require.NoError(t, sm2.VerifyBlock(context.Background(), block)) + + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + TS: testConfig1.blockBuilder.block.Timestamp(), + BlockHeight: 43, + Bytes: []byte{7, 8, 9}, + }, + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(testConfig1.blockBuilder.block.Timestamp().UnixMilli()), + PChainHeight: 100, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: 100, + EpochNumber: 1, + PrevVMBlockSeq: 42, + BlockValidationDescriptor: &metadata.BlockValidationDescriptor{ + AggregatedMembership: metadata.AggregatedMembership{ + Members: testConfig1.validatorSetRetriever.result, + }, + }, + }, + }, + }, block) +} + +func TestMSMNormalOp(t *testing.T) { + newPChainHeight := uint64(200) + newValidatorSet := metadata.NodeBLSMappings{ + {BLSKey: []byte{5}, Weight: 1}, {BLSKey: []byte{6}, Weight: 1}, {BLSKey: []byte{7}, Weight: 1}, + } + + for _, testCase := range []struct { + name string + setup func(*metadata.StateMachine, *testConfig) + mutateBlock func(*metadata.StateMachineBlock) + err string + expectedPChainHeight uint64 + expectedNextPChainRefHeight uint64 + }{ + { + name: "correct information", + expectedPChainHeight: 100, + }, + { + name: "trying to build a genesis block", + mutateBlock: func(block *metadata.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 *metadata.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 *metadata.StateMachineBlock) { + block.Metadata.PChainHeight = 110 + }, + err: "invalid P-chain reference height (110) is too big", + }, + { + name: "P-chain height smaller than parent", + mutateBlock: func(block *metadata.StateMachineBlock) { + block.Metadata.PChainHeight = 0 + }, + err: "invalid P-chain height (0) is smaller than parent inner block's P-chain height (100)", + }, + { + name: "wrong epoch number", + mutateBlock: func(block *metadata.StateMachineBlock) { + block.Metadata.SimplexEpochInfo.EpochNumber = 2 + }, + err: "expected epoch number to be 1 but got 2", + }, + { + name: "non-nil BlockValidationDescriptor", + mutateBlock: func(block *metadata.StateMachineBlock) { + block.Metadata.SimplexEpochInfo.BlockValidationDescriptor = &metadata.BlockValidationDescriptor{} + }, + err: "failed to find first Simplex inner block", + }, + { + name: "non-zero sealing block seq", + mutateBlock: func(block *metadata.StateMachineBlock) { + block.Metadata.SimplexEpochInfo.SealingBlockSeq = 5 + }, + err: "expected sealing block sequence number to be 0 but got 5", + }, + { + name: "wrong PChainReferenceHeight", + mutateBlock: func(block *metadata.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 *metadata.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 *metadata.StateMachineBlock) { + block.Metadata.SimplexEpochInfo.PrevVMBlockSeq = 999 + }, + err: "expected PrevVMBlockSeq to be", + }, + { + name: "validator set change detected", + setup: func(sm *metadata.StateMachine, tc *testConfig) { + tc.validatorSetRetriever.resultMap = map[uint64]metadata.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) + + var icmEpochInvokeCount int + + sm1.ComputeICMEpoch = func(_ any, input metadata.ICMEpochInput) metadata.ICMEpoch { + icmEpochInvokeCount++ + require.Equal(t, metadata.ICMEpochInput{ + ParentPChainHeight: 100, + ChildTimestamp: blockTime, + ParentTimestamp: time.Unix(int64(lastBlock.Metadata.Timestamp), 0), + ParentEpoch: metadata.ICMEpoch{}, + }, input) + return input.ParentEpoch + } + + content := make([]byte, 10) + _, err = rand.Read(content) + require.NoError(t, err) + + testConfig1.blockBuilder.block = &metadata.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) + + require.Equal(t, 1, icmEpochInvokeCount, "ComputeICMEpoch should have been invoked exactly once") + + 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, &metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + TS: blockTime, + BlockHeight: lastBlock.InnerBlock.Height(), + Bytes: content, + }, + Metadata: metadata.StateMachineMetadata{ + SimplexBlacklist: blacklist.Bytes(), + Timestamp: uint64(blockTime.UnixMilli()), + PChainHeight: testCase.expectedPChainHeight, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.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 := metadata.NodeBLSMappings{ + {NodeID: node1, BLSKey: []byte{1}, Weight: 1}, + {NodeID: node2, BLSKey: []byte{2}, Weight: 1}, + {NodeID: node3, BLSKey: []byte{3}, Weight: 1}, + } + validatorSet2 := metadata.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) *metadata.InnerBlock { + return &metadata.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 := metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + BlockHeight: 0, // Genesis block has height 0 + TS: startTime, + Bytes: []byte{0}, + }, + } + + notGenesis := metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + BlockHeight: 42, + TS: startTime, + Bytes: []byte{0}, + }, + } + for _, testCase := range []struct { + name string + firstBlockBeforeSimplex metadata.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) (metadata.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 metadata.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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(1), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(1 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight1, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq, + BlockValidationDescriptor: &metadata.BlockValidationDescriptor{ + AggregatedMembership: metadata.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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(2), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(2 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight1, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(3), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(3 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.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 metadata.ValidatorSetApprovals + sm.ApprovalsRetriever = &dynamicApprovalsRetriever{approvals: &approvalsResult} + + approvalsResult = metadata.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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(4), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(4 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 3, + NextPChainReferenceHeight: pChainHeight2, + NextEpochApprovals: &metadata.NextEpochApprovals{ + NodeIDs: bitmask, + Signature: sig, + }, + }, + }, + }, 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) ----- + approvalsResult = metadata.ValidatorSetApprovals{ + { + NodeID: node2, + PChainHeight: pChainHeight2, + Signature: []byte("sig2"), + }, + } + + // 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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(5), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(5 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 4, + NextPChainReferenceHeight: pChainHeight2, + NextEpochApprovals: &metadata.NextEpochApprovals{ + NodeIDs: bitmask, + Signature: sig, + }, + }, + }, + }, 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 = metadata.ValidatorSetApprovals{ + { + NodeID: node3, + PChainHeight: pChainHeight2, + Signature: []byte("sig3"), + }, + } + + // 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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(6), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 5, + NextPChainReferenceHeight: pChainHeight2, + SealingBlockSeq: 0, + PrevSealingBlockHash: block1.Digest(), + BlockValidationDescriptor: &metadata.BlockValidationDescriptor{ + AggregatedMembership: metadata.AggregatedMembership{ + Members: validatorSet2, + }, + }, + NextEpochApprovals: &metadata.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, nil) + require.NoError(t, err) + + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nil, + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.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, nil) + require.NoError(t, err) + require.Equal(t, &metadata.StateMachineBlock{ + InnerBlock: nextBlock(7), + Metadata: metadata.StateMachineMetadata{ + Timestamp: uint64(startTime.Add(7 * time.Millisecond).UnixMilli()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.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 *metadata.ValidatorSetApprovals +} + +func (d *dynamicApprovalsRetriever) RetrieveApprovals() metadata.ValidatorSetApprovals { + return *d.approvals +} + +func makeChain(t *testing.T, simplexStartHeight uint64, endHeight uint64) []metadata.StateMachineBlock { + startTime := time.Now().Add(-time.Duration(endHeight+2) * time.Second) + blocks := make([]metadata.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 []metadata.StateMachineBlock, start time.Time, h uint64, round uint64, seq uint64) metadata.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 metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + TS: start.Add(time.Duration(h) * time.Second), + BlockHeight: h, + Bytes: []byte{1, 2, 3}, + }, + Metadata: metadata.StateMachineMetadata{ + PChainHeight: 100, + SimplexProtocolMetadata: (&simplex.ProtocolMetadata{ + Round: round, + Seq: seq, + Epoch: 1, + Prev: prev, + }).Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PrevSealingBlockHash: [32]byte{}, + PChainReferenceHeight: 100, + EpochNumber: 1, + PrevVMBlockSeq: uint64(index), + }, + }, + } +} + +func makeNonSimplexBlock(t *testing.T, startHeight uint64, start time.Time, h uint64) metadata.StateMachineBlock { + content := make([]byte, 10) + _, err := rand.Read(content) + require.NoError(t, err) + + return metadata.StateMachineBlock{ + InnerBlock: &metadata.InnerBlock{ + TS: start.Add(time.Duration(h-startHeight) * time.Second), + BlockHeight: h, + Bytes: []byte{1, 2, 3}, + }, + } +} + +type testConfig struct { + blockStore blockStore + approvalsRetriever approvalsRetriever + signatureVerifier signatureVerifier + signatureAggregator signatureAggregator + blockBuilder blockBuilder + keyAggregator keyAggregator + validatorSetRetriever validatorSetRetriever +} + +func newStateMachine(t *testing.T) (metadata.StateMachine, *testConfig) { + bs := make(blockStore) + + var testConfig testConfig + testConfig.blockStore = bs + testConfig.validatorSetRetriever.result = metadata.NodeBLSMappings{ + {BLSKey: []byte{1}, Weight: 1}, {BLSKey: []byte{2}, Weight: 1}, + } + + sm := metadata.StateMachine{ + 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, + ComputeICMEpoch: func(_ any, input metadata.ICMEpochInput) metadata.ICMEpoch { + return input.ParentEpoch + }, + GetPChainHeight: func() uint64 { + return 100 + }, + GetUpgrades: func() any { + return nil + }, + GetValidatorSet: testConfig.validatorSetRetriever.getValidatorSet, + PChainProgressListener: &noOpPChainListener{}, + } + return sm, &testConfig +} diff --git a/msm/verification.go b/msm/verification.go new file mode 100644 index 00000000..e238426f --- /dev/null +++ b/msm/verification.go @@ -0,0 +1,525 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "math/big" + "time" + + "github.com/ava-labs/simplex" +) + +type verificationInput struct { + prevMD StateMachineMetadata + proposedBlockMD StateMachineMetadata + proposedBlockTimestamp time.Time + hasInnerBlock bool + prevBlockSeq uint64 + 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(_ SimplexEpochInfo, next SimplexEpochInfo) error { + validators, err := vd.getValidatorSet(next.NextPChainReferenceHeight) + if err != nil { + return err + } + + 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 inner block", next.NextPChainReferenceHeight) + } + + return nil +} + +func (vd *validationDescriptorVerifier) verifyEmptyValidationDescriptor(_ SimplexEpochInfo, next SimplexEpochInfo) error { + if next.BlockValidationDescriptor != nil { + return fmt.Errorf("inner 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 inner block") + } + + validators, err := nv.getValidatorSet(next.NextPChainReferenceHeight) + if err != nil { + return err + } + + err = nv.verifySignature(prev, next, auxInfo, validators) + if err != nil { + return err + } + + canSeal, err := canSealBlock(validators, next) + if err != nil { + return err + } + + if !canSeal { + return fmt.Errorf("not enough approvals to seal inner 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 { + return fmt.Errorf("next epoch approvals should not be nil when collecting approvals") + } + + validators, err := nv.getValidatorSet(next.NextPChainReferenceHeight) + if err != nil { + return err + } + + err = nv.verifySignature(prev, next, auxInfo, validators) + if err != nil { + return err + } + + if err := areNextEpochApprovalsSignersSupersetOfApprovalsOfPrevBlock(prev, next); err != nil { + return err + } + + 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 inner block are missing from next epoch approvals of proposed inner block") + } + 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 canSealBlock(validators NodeBLSMappings, next SimplexEpochInfo) (bool, error) { + totalWeight, err := computeTotalWeight(validators) + if err != nil { + return false, fmt.Errorf("failed computing total weight: %w", err) + } + + approvingNodes := bitmaskFromBytes(next.NextEpochApprovals.NodeIDs) + + approvingWeight, err := computeApprovingWeight(validators, &approvingNodes) + if err != nil { + return false, err + } + + if approvingWeight > math.MaxInt64 { + return false, fmt.Errorf("approving weight is too large, overflows int64: %d", approvingWeight) + } + + threshold := big.NewRat(2, 3) + approvingRatio := big.NewRat(approvingWeight, totalWeight) + + canSeal := approvingRatio.Cmp(threshold) > 0 + return canSeal, nil +} + +func computeApprovingWeight(validators NodeBLSMappings, approvingNodes *bitmask) (int64, error) { + var approvingWeight uint64 + var err error + validators.ForEach(func(i int, nbm NodeBLSMapping) { + if err != nil { + return + } + if !approvingNodes.Contains(i) { + return + } + approvingWeight, err = safeAdd(approvingWeight, nbm.Weight) + }) + + if err != nil { + return 0, fmt.Errorf("failed to compute approving weights: %w", err) + } + + if approvingWeight > math.MaxInt64 { + return 0, fmt.Errorf("approving weight of validators is too big, overflows int64: %d", approvingWeight) + } + + return int64(approvingWeight), nil +} + +func (nv *nextEpochApprovalsVerifier) verifySignature(prev SimplexEpochInfo, next SimplexEpochInfo, auxinfo *AuxiliaryInfo, validators NodeBLSMappings) error { + approvingNodes := bitmaskFromBytes(next.NextEpochApprovals.NodeIDs) + publicKeys := make([][]byte, 0, len(validators)) + validators.ForEach(func(i int, nbm NodeBLSMapping) { + if !approvingNodes.Contains(i) { + return + } + publicKeys = append(publicKeys, nbm.BLSKey) + }) + + aggPK, err := nv.keyAggregator.AggregateKeys(publicKeys...) + if err != nil { + return fmt.Errorf("failed to aggregate public keys: %w", err) + } + + pChainHeightBuff := pChainNextReferenceHeightAsBytes(prev) + + var bb bytes.Buffer + bb.Write(pChainHeightBuff) + if auxinfo != nil { + bb.Write(auxinfo.Info) + } + + if err := nv.sigVerifier.VerifySignature(next.NextEpochApprovals.Signature, bb.Bytes(), aggPK); err != nil { + return fmt.Errorf("failed to verify signature: %w", err) + } + return 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.verifyNextPChainHeightNormal(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 inner block type: %d", in.nextBlockType) + } + return nil +} + +func (n *nextPChainReferenceHeightVerifier) verifyNextPChainHeightNormal(prevMD StateMachineMetadata, prev SimplexEpochInfo, next SimplexEpochInfo) error { + 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 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 + } + 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) + } + + 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 + + if in.prevMD.SimplexEpochInfo.EpochNumber == 0 && in.proposedBlockMD.SimplexEpochInfo.EpochNumber != 1 { + return fmt.Errorf("expected epoch number of the first inner block created to be 1 but got %d", next.EpochNumber) + } + + switch in.nextBlockType { + case BlockTypeNewEpoch: + 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 + + switch in.nextBlockType { + case BlockTypeNewEpoch, BlockTypeNormal: + 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. + 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) + } + // Previous block is the sealing block, and this is the first Telock. + if prev.BlockValidationDescriptor != nil && next.SealingBlockSeq != 0 { + 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) + } + } + case BlockTypeSealing: + if next.SealingBlockSeq > 0 { + return fmt.Errorf("expected sealing inner block sequence number to be 0 but got %d", next.SealingBlockSeq) + } + default: + return fmt.Errorf("unknown inner 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 reference 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 inner 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 inner 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 + expectedICMInfo := nextICMEpochInfo(prevMD, in.hasInnerBlock, i.getUpdates, i.computeICMEpoch, in.proposedBlockTimestamp) + + 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 { + expectedTimestamp := in.proposedBlockTimestamp.UnixMilli() + if expectedTimestamp != int64(in.proposedBlockMD.Timestamp) { + return fmt.Errorf("expected timestamp to be %d but got %d", expectedTimestamp, int64(in.proposedBlockMD.Timestamp)) + } + currentTime := t.getTime() + if currentTime.Add(t.timeSkewLimit).Before(in.proposedBlockTimestamp) { + return fmt.Errorf("proposed block timestamp is too far in the future, current time is %s but got %s", currentTime.String(), in.proposedBlockTimestamp.String()) + } + 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 + + 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 inner block: %w", err) + } + + block, _, err := p.getBlock(RetrievingOpts{Height: firstEverSimplexBlockSeq}) + if err != nil { + return fmt.Errorf("failed retrieving first ever simplex inner block %d: %w", firstEverSimplexBlockSeq, err) + } + + hash := block.Digest() + if !bytes.Equal(in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash[:], hash[:]) { + return fmt.Errorf("expected prev sealing inner block hash of the first ever simplex inner block to be %x but got %x", hash, in.proposedBlockMD.SimplexEpochInfo.PrevSealingBlockHash) + } + + return nil + } + + switch in.nextBlockType { + case BlockTypeSealing: + prevSealingBlock, _, err := p.getBlock(RetrievingOpts{Height: in.prevMD.SimplexEpochInfo.EpochNumber}) + if err != nil { + return fmt.Errorf("failed retrieving inner 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: + 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 + } + + md, err := simplex.ProtocolMetadataFromBytes(in.proposedBlockMD.SimplexProtocolMetadata) + if err != nil { + return fmt.Errorf("failed parsing protocol metadata: %w", err) + } + + // 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(RetrievingOpts{Height: in.prevBlockSeq, Digest: md.Prev}) + if err != nil { + return fmt.Errorf("failed retrieving inner 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 +} diff --git a/msm/verification_test.go b/msm/verification_test.go new file mode 100644 index 00000000..6a9c7302 --- /dev/null +++ b/msm/verification_test.go @@ -0,0 +1,1047 @@ +// 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 TestICMEpochInfoVerifier(t *testing.T) { + now := time.Now() + + for _, tc := range []struct { + name string + prevMD StateMachineMetadata + nextMD StateMachineMetadata + blockTime time.Time + err string + }{ + { + name: "matching ICM epoch info without inner block", + prevMD: StateMachineMetadata{}, + nextMD: StateMachineMetadata{}, + }, + { + name: "matching ICM epoch info with inner block", + prevMD: StateMachineMetadata{}, + nextMD: StateMachineMetadata{}, + blockTime: now, + }, + { + name: "mismatching ICM epoch info", + prevMD: StateMachineMetadata{}, + nextMD: StateMachineMetadata{ + ICMEpochInfo: ICMEpochInfo{EpochNumber: 99}, + }, + err: "expected ICM epoch info to be {0 0 0 {0}} but got {0 99 0 {0}}", + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &icmEpochInfoVerifier{ + getUpdates: func() any { return nil }, + computeICMEpoch: func(_ any, input ICMEpochInput) ICMEpoch { + return input.ParentEpoch + }, + } + err := v.Verify(verificationInput{ + prevMD: tc.prevMD, + proposedBlockMD: tc.nextMD, + proposedBlockTimestamp: tc.blockTime, + }) + if tc.err != "" { + require.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} + +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 reference 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 inner 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 + + for _, tc := range []struct { + name string + blockTime time.Time + timestamp uint64 + parentTimestamp uint64 + err string + }{ + { + name: "matching timestamp", + blockTime: now, + timestamp: uint64(now.UnixMilli()), + }, + { + name: "mismatching timestamp", + blockTime: now, + timestamp: uint64(now.UnixMilli()) + 100, + err: fmt.Sprintf("expected timestamp to be %d but got %d", now.UnixMilli(), int64(uint64(now.UnixMilli())+100)), + }, + { + name: "timestamp too far in the future", + blockTime: now.Add(10 * time.Second), + timestamp: uint64(now.Add(10 * time.Second).UnixMilli()), + err: fmt.Sprintf("proposed block timestamp is too far in the future, current time is %s but got %s", now.String(), now.Add(10*time.Second).String()), + }, + { + name: "timestamp older than parent", + blockTime: 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())), + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := ×tampVerifier{ + getTime: func() time.Time { return now }, + timeSkewLimit: timeSkewLimit, + } + err := v.Verify(verificationInput{ + proposedBlockTimestamp: tc.blockTime, + 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 inner 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 inner 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 inner block hash of the first ever simplex inner 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 inner 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 inner 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 inner 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 inner 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 inner 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: "inner 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 + auxInfo *AuxiliaryInfo + 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 inner 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 inner 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 inner block are missing from next epoch approvals of proposed inner 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, AuxiliaryInfo: tc.auxInfo}, + }) + 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 inner 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(opts RetrievingOpts) (StateMachineBlock, *simplex.Finalization, error) { + blk, ok := bs[opts.Height] + if !ok { + return StateMachineBlock{}, nil, fmt.Errorf("%w: block %d", simplex.ErrBlockNotFound, opts.Height) + } + 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 +}