From c16526e0836a2fd7a1a19e0ecdbe466973db1acf Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Mon, 23 Feb 2026 16:06:07 +0100 Subject: [PATCH 1/5] Metadata State Machine (MSM) for reconfiguration This commit adds an implementation of the Metadata State Machine (MSM). It intercepts block building and verification from the Simplex instance, and performs or verifies metadata state transitions according to the logic defined in the README.md. Contains: - MSM block building and verification logic - Epoch transition (sealing blocks, Telocks, approval collection) - Block encoding with canoto - Unit and integration tests (fake node test and full epoch lifecycle test) Signed-off-by: Yacov Manevich --- go.mod | 7 +- go.sum | 20 +- msm/README.md | 21 +- msm/build_decision.go | 117 ++ msm/build_decision_test.go | 204 +++ msm/encoding.canoto.go | 2764 +++++++++++++++++++++++++++++++++++ msm/encoding.go | 343 +++++ msm/encoding_test.go | 578 ++++++++ msm/fake_node_test.go | 435 ++++++ msm/misc.go | 122 ++ msm/msm.go | 936 ++++++++++++ msm/msm_test.go | 1053 +++++++++++++ msm/readme-discrepancies.md | 97 ++ msm/verification.go | 510 +++++++ msm/verification_test.go | 1028 +++++++++++++ 15 files changed, 8223 insertions(+), 12 deletions(-) create mode 100644 msm/build_decision.go create mode 100644 msm/build_decision_test.go create mode 100644 msm/encoding.canoto.go create mode 100644 msm/encoding.go create mode 100644 msm/encoding_test.go create mode 100644 msm/fake_node_test.go create mode 100644 msm/misc.go create mode 100644 msm/msm.go create mode 100644 msm/msm_test.go create mode 100644 msm/readme-discrepancies.md create mode 100644 msm/verification.go create mode 100644 msm/verification_test.go 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..55449a60 100644 --- a/msm/README.md +++ b/msm/README.md @@ -378,33 +378,35 @@ ____________________ | ```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. +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: ```proto message HashPreImage { bytes h_i = 1; // The inner block hash - OuterBlock outer_block = 2; - bytes protocol_metadata = 3; + bytes h_m = 2; // The metadata hash } ``` @@ -453,5 +455,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..e6939965 --- /dev/null +++ b/msm/build_decision.go @@ -0,0 +1,117 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "context" + "sync" + "time" +) + +type blockBuildingDecision int8 + +const ( + blockBuildingDecisionUndefined blockBuildingDecision = iota + blockBuildingDecisionBuildBlock + blockBuildingDecisionTransitionEpoch + blockBuildingDecisionBuildBlockAndTransitionEpoch + blockBuildingDecisionContextCanceled +) + +// 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) +} + +type blockBuildingDecider struct { + maxBlockBuildingWaitTime time.Duration + pChainlistener PChainProgressListener + waitForPendingBlock func(ctx context.Context) + shouldTransitionEpoch func() (bool, error) + getPChainHeight func() uint64 +} + +func (bbd *blockBuildingDecider) shouldBuildBlock( + ctx context.Context, +) (blockBuildingDecision, uint64, error) { + for { + pChainHeight := bbd.getPChainHeight() + + shouldTransitionEpoch, err := bbd.shouldTransitionEpoch() + 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 + } + + bbd.waitForPChainChangeOrPendingBlock(ctx, pChainHeight) + + // If the context was cancelled in the meantime, abandon evaluation. + if bbd.wasContextCanceled(ctx) { + return blockBuildingDecisionContextCanceled, 0, nil + } + + // 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 waitForPendingBlock returned without the P-chain height changing, + // which means we should build a block. + + return blockBuildingDecisionBuildBlock, pChainHeight, nil + } +} + +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() + bbd.pChainlistener.WaitForProgress(pChainAwareContext, pChainHeight) + cancel() + }() + + bbd.waitForPendingBlock(pChainAwareContext) +} + +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..9fce16fc --- /dev/null +++ b/msm/build_decision_test.go @@ -0,0 +1,204 @@ +// 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) { + f.onListen(ctx, pChainHeight) +} + +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() (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() (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() (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() (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() (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() (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()) + + 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() (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..62dc0f59 --- /dev/null +++ b/msm/encoding.canoto.go @@ -0,0 +1,2764 @@ +// 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 +} + +const ( + canoto__StateMachineBlockPreImage__InnerBlockHash = 1 + canoto__StateMachineBlockPreImage__Metadata = 2 + + canoto__StateMachineBlockPreImage__InnerBlockHash__tag = "\x0a" // canoto.Tag(canoto__StateMachineBlockPreImage__InnerBlockHash, canoto.Len) + canoto__StateMachineBlockPreImage__Metadata__tag = "\x12" // canoto.Tag(canoto__StateMachineBlockPreImage__Metadata, canoto.Len) +) + +type canotoData_StateMachineBlockPreImage struct { + size uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*StateMachineBlockPreImage) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeOf(StateMachineBlockPreImage{})) + var zero StateMachineBlockPreImage + s := &canoto.Spec{ + Name: "StateMachineBlockPreImage", + Fields: []canoto.FieldType{ + { + FieldNumber: canoto__StateMachineBlockPreImage__InnerBlockHash, + Name: "InnerBlockHash", + OneOf: "", + TypeBytes: true, + }, + canoto.FieldTypeFromField( + /*type inference:*/ (&zero.Metadata), + /*FieldNumber: */ canoto__StateMachineBlockPreImage__Metadata, + /*Name: */ "Metadata", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*StateMachineBlockPreImage) MakeCanoto() *StateMachineBlockPreImage { + return new(StateMachineBlockPreImage) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = StateMachineBlockPreImage{} + 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__StateMachineBlockPreImage__InnerBlockHash: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.InnerBlockHash); err != nil { + return err + } + if len(c.InnerBlockHash) == 0 { + return canoto.ErrZeroValue + } + case canoto__StateMachineBlockPreImage__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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if len(c.InnerBlockHash) != 0 { + size += uint64(len(canoto__StateMachineBlockPreImage__InnerBlockHash__tag)) + canoto.SizeBytes(c.InnerBlockHash) + } + (&c.Metadata).CalculateCanotoCache() + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__StateMachineBlockPreImage__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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if len(c.InnerBlockHash) != 0 { + canoto.Append(&w, canoto__StateMachineBlockPreImage__InnerBlockHash__tag) + canoto.AppendBytes(&w, c.InnerBlockHash) + } + if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__StateMachineBlockPreImage__Metadata__tag) + canoto.AppendUint(&w, fieldSize) + w = (&c.Metadata).MarshalCanotoInto(w) + } + return w +} diff --git a/msm/encoding.go b/msm/encoding.go new file mode 100644 index 00000000..b49bffbd --- /dev/null +++ b/msm/encoding.go @@ -0,0 +1,343 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package metadata + +import ( + "bytes" + "slices" +) + +// go:generate go 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 ICMEpochInfo `canoto:"value,1"` + SimplexEpochInfo SimplexEpochInfo `canoto:"value,2"` + SimplexProtocolMetadata []byte `canoto:"bytes,3"` + SimplexBlacklist []byte `canoto:"bytes,4"` + AuxiliaryInfo *AuxiliaryInfo `canoto:"pointer,5"` + PChainHeight uint64 `canoto:"uint,6"` + 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) Compare(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 +} + +type StateMachineBlockPreImage struct { + InnerBlockHash []byte `canoto:"bytes,1"` + Metadata StateMachineMetadata `canoto:"value,2"` + + canotoData canotoData_StateMachineBlockPreImage +} diff --git a/msm/encoding_test.go b/msm/encoding_test.go new file mode 100644 index 00000000..f432222f --- /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.Compare(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..1b113baf --- /dev/null +++ b/msm/fake_node_test.go @@ -0,0 +1,435 @@ +// 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)) + + return + + t.Log("Epoch:", node.Epoch()) + + epoch := node.Epoch() + require.Greater(t, epoch, uint64(1)) + require.Equal(t, node.Height(), uint64(20)) + + // 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) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Millisecond): + if fn.sm.GetPChainHeight() != pChainHeight { + return + } + } + } +} + +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.isTelock(nextIndex) { + 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) isTelock(index int) bool { + if index == 0 { + return false + } + return metadata.IdentifyBlockType( + fn.notarizedBlocks[index].Metadata, + fn.notarizedBlocks[index-1].Metadata, + ) == metadata.BlockTypeTelock +} + +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..5bb189c5 --- /dev/null +++ b/msm/msm.go @@ -0,0 +1,936 @@ +// 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" +) + +// 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 +} + +// 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() + + simplexMetadataBytes := simplexMetadata.Bytes() + var simplexBlacklistBytes []byte + if simplexBlacklist != nil { + simplexBlacklistBytes = simplexBlacklist.Bytes() + } + + currentState, err := sm.identifyCurrentState(parentBlock.Metadata.SimplexEpochInfo) + if err != nil { + return nil, err + } + + if simplexMetadata.Seq == 0 { + return nil, fmt.Errorf("invalid ProtocolMetadata sequence number: should be > 0, got %d", simplexMetadata.Seq) + } + + prevBlockSeq := simplexMetadata.Seq - 1 + + switch currentState { + case stateFirstSimplexBlock: + return sm.buildBlockZeroEpoch(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) + timestamp := time.Unix(int64(prevMD.Timestamp), 0) + + 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(simplexEpochInfo SimplexEpochInfo) (state, error) { + // If this is the first ever epoch, then this is also the first ever block to be built by Simplex. + if simplexEpochInfo.EpochNumber == 0 { + return stateFirstSimplexBlock, nil + } + + if simplexEpochInfo.NextPChainReferenceHeight == 0 { + return stateBuildBlockNormalOp, nil + } + + // Else, NextPChainReferenceHeight > 0, so we're either in stateBuildCollectingApprovals or stateBuildBlockEpochSealed + if simplexEpochInfo.SealingBlockSeq == 0 { + // If we don't have a sealing block sequence yet, we're still collecting approvals for the validator set change. + return stateBuildCollectingApprovals, nil + } + + // Otherwise, we do have a sealing block sequence, so the epoch has been sealed. + return stateBuildBlockEpochSealed, nil +} + +func computePrevVMBlockSeq(parentBlock StateMachineBlock, prevBlockSeq uint64) uint64 { + if parentBlock.InnerBlock == nil { + return parentBlock.Metadata.SimplexEpochInfo.PrevVMBlockSeq + } + return prevBlockSeq +} + +func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte, prevBlockSeq uint64) (*StateMachineBlock, error) { + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), + } + + blockBuildingDecider := blockBuildingDecider{ + maxBlockBuildingWaitTime: sm.MaxBlockBuildingWaitTime, + pChainlistener: sm.PChainProgressListener, + getPChainHeight: sm.GetPChainHeight, + waitForPendingBlock: sm.BlockBuilder.WaitForPendingBlock, + shouldTransitionEpoch: func() (bool, error) { + pChainHeight := sm.GetPChainHeight() + + 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.Compare(newValidatorSet) { + return true, nil + } + return false, nil + }, + } + + decisionToBuildBlock, pChainHeight, err := blockBuildingDecider.shouldBuildBlock(ctx) + if err != nil { + return nil, err + } + + var childBlock VMBlock + + switch decisionToBuildBlock { + case blockBuildingDecisionBuildBlock, blockBuildingDecisionBuildBlockAndTransitionEpoch: + return sm.buildBlockAndMaybeTransitionEpoch(ctx, parentBlock, simplexMetadata, simplexBlacklist, childBlock, decisionToBuildBlock, newSimplexEpochInfo, pChainHeight) + case blockBuildingDecisionTransitionEpoch: + 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) 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 { + newSimplexEpochInfo.NextPChainReferenceHeight = pChainHeight + } + + return sm.wrapBlock(parentBlock, childBlock, newSimplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil +} + +func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachineMetadata) 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 { + if simplexEpochInfo.EpochNumber == prevSimplexEpochInfo.SealingBlockSeq { + return BlockTypeNewEpoch + } + // The zero-epoch block has BlockValidationDescriptor but SealingBlockSeq == 0, + // so the block following it is a normal block, not a Telock. + if prevSimplexEpochInfo.SealingBlockSeq == 0 { + return BlockTypeNormal + } + 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 +} + +func (sm *StateMachine) buildBlockZeroEpoch(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() + } + simplexEpochInfo := constructSimplexEpochInfoForZeroEpoch(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).Compare(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 := constructSimplexEpochInfoForZeroEpoch(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.Unix(int64(prevBlock.Metadata.Timestamp), 0) + } + + expectedTimestamp := proposedTime.Unix() + if expectedTimestamp != int64(block.Metadata.Timestamp) { + return time.Time{}, fmt.Errorf("expected timestamp to be %d but got %d", expectedTimestamp, int64(block.Metadata.Timestamp)) + } + currentTime := sm.GetTime() + if currentTime.Add(sm.TimeSkewLimit).Before(proposedTime) { + return time.Time{}, fmt.Errorf("proposed block timestamp is too far in the future, current time is %s but got %s", currentTime.String(), proposedTime.String()) + } + if prevBlock.Metadata.Timestamp > block.Metadata.Timestamp { + return time.Time{}, fmt.Errorf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", prevBlock.Metadata.Timestamp, block.Metadata.Timestamp) + } + return proposedTime, nil +} + +func constructSimplexEpochInfoForZeroEpoch(pChainHeight uint64, newValidatorSet NodeBLSMappings, prevVMBlockSeq uint64) SimplexEpochInfo { + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight, + EpochNumber: 1, + BlockValidationDescriptor: &BlockValidationDescriptor{ + AggregatedMembership: AggregatedMembership{ + Members: newValidatorSet, + }, + }, + NextEpochApprovals: nil, // We don't need to collect approvals to seal the zero epoch. + PrevVMBlockSeq: prevVMBlockSeq, + SealingBlockSeq: 0, // We don't have a sealing block in the zero epoch. + PrevSealingBlockHash: [32]byte{}, // The zero epoch 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) { + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + NextPChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), + } + + validators, err := sm.GetValidatorSet(parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight) + if err != nil { + return nil, err + } + approvalsFromPeers := sm.ApprovalsRetriever.RetrieveApprovals() + auxInfo := parentBlock.Metadata.AuxiliaryInfo + nextPChainHeight := parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight + prevNextEpochApprovals := parentBlock.Metadata.SimplexEpochInfo.NextEpochApprovals + newApprovals, err := computeNewApprovals(prevNextEpochApprovals, auxInfo, approvalsFromPeers, nextPChainHeight, sm.SignatureAggregator, validators) + if err != nil { + return nil, err + } + + if !newApprovals.canSeal { + if newSimplexEpochInfo.NextEpochApprovals == nil { + newSimplexEpochInfo.NextEpochApprovals = &NextEpochApprovals{} + } + newSimplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs + newSimplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature + pChainHeight := parentBlock.Metadata.PChainHeight + return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight) + } + + // Else, we create the sealing block. + return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, newApprovals, nextPChainHeight) +} + +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() + + ctx = impatientContext + + childBlock, err := sm.BlockBuilder.BuildBlock(ctx, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) + if err != nil && ctx.Err() == nil { + // If we got an error building the block, and we didn't time out, return the error. + // We failed to build the block. + return nil, err + } + // Else, either err == nil, and we've built the block, + // or err != nil but ctx.Err() != nil and we have waited MaxBlockBuildingWaitTime, + // so we need to build a block regardless of whether the inner VM wants to build a block. + 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) { + // Update the approvals and signature in the simplex epoch info for the next block + if simplexEpochInfo.NextEpochApprovals == nil { + simplexEpochInfo.NextEpochApprovals = &NextEpochApprovals{} + } + simplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs + simplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature + + // If this is the sealing block, set the sealing block sequence. + md, err := simplex.ProtocolMetadataFromBytes(parentBlock.Metadata.SimplexProtocolMetadata) + if err != nil { + return nil, err + } + simplexEpochInfo.SealingBlockSeq = md.Seq + 1 + 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 newApprovals.canSeal && simplexEpochInfo.EpochNumber > 1 { + prevSealingBlock, _, err := sm.GetBlock(RetrievingOpts{Height: simplexEpochInfo.EpochNumber}) + if err != nil { + 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) + 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. + + newSimplexEpochInfo := SimplexEpochInfo{ + PChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight, + EpochNumber: parentBlock.Metadata.SimplexEpochInfo.EpochNumber, + NextPChainReferenceHeight: parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight, + SealingBlockSeq: parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq, + PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), + } + + // First, we find the sequence of the sealing block. + seq := parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq + + // Do a sanity check just in case, make sure it's defined + if seq == 0 { + return nil, fmt.Errorf("cannot build epoch sealed inner block: sealing inner block sequence is 0 or undefined") + } + + _, finalization, err := sm.GetBlock(RetrievingOpts{Height: seq}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve sealing inner block at sequence %d: %w", seq, 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: parentBlock.Metadata.SimplexEpochInfo.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 +} + +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.Unix()) + } + + 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.Unix(int64(parentMetadata.Timestamp), 0) + 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..3375b2ba --- /dev/null +++ b/msm/msm_test.go @@ -0,0 +1,1053 @@ +// 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) { + <-ctx.Done() +} + +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().Unix()), + 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 inner 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.Unix()), + 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).Unix()), + 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).Unix()), + 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).Unix()), + 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).Unix()), + 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).Unix()), + 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).Unix()), + PChainHeight: pChainHeight2, + SimplexProtocolMetadata: md.Bytes(), + SimplexEpochInfo: metadata.SimplexEpochInfo{ + PChainReferenceHeight: pChainHeight1, + EpochNumber: 1, + PrevVMBlockSeq: baseSeq + 5, + NextPChainReferenceHeight: pChainHeight2, + SealingBlockSeq: baseSeq + 6, + 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 := block6.Metadata.SimplexEpochInfo.SealingBlockSeq + + 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).Unix()), + 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).Unix()), + 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/readme-discrepancies.md b/msm/readme-discrepancies.md new file mode 100644 index 00000000..88b81b68 --- /dev/null +++ b/msm/readme-discrepancies.md @@ -0,0 +1,97 @@ +# Discrepancies between README.md and Code + +The following discrepancies were found by comparing the MSM README specification against the implementation in the Go code. + +## 1. Sealing block `sealing_block_seq` -- README contradicts code and itself + +**README (line 246):** +> "A sealing block is identified by having the `block_validation_descriptor` defined and `sealing_block_seq` set to 0." + +**Code (`createSealingBlock`, msm.go):** +```go +simplexEpochInfo.SealingBlockSeq = md.Seq + 1 // the sealing block's own sequence, NOT 0 +``` + +The code sets `SealingBlockSeq` to the sealing block's own sequence number (always > 0). The README says it's 0. + +The README also contradicts itself -- line 248 says Telocks have `sealing_block_seq` set to the sealing block's sequence, and line 249 says Telocks are identified by `sealing_block_seq > 0`. If the sealing block itself had `sealing_block_seq == 0`, Telocks would inherit 0, which contradicts the `> 0` identification. + +## 2. Signature message -- `PChainReferenceHeight` vs `next_p_chain_reference_height` + +**README (line 211):** +> "The signature is over the `info` field of the `AuxiliaryInfo` and the **`next_p_chain_reference_height`** fields" + +**Code (`verifySignature`, verification.go):** +```go +pChainHeightBuff := pChainReferenceHeightAsBytes(prev) // prev.PChainReferenceHeight +bb.Write(pChainHeightBuff) +bb.Write(auxinfo.Info) +``` + +The code signs/verifies over `PChainReferenceHeight` (current epoch's height), **not** `NextPChainReferenceHeight`. Either the README or the code is wrong. If the README is correct, the code has a security vulnerability -- the signature doesn't bind to the target transition height. + +## 3. Missing enforcement of `next_p_chain_reference_height > p_chain_reference_height` + +**README (line 140):** +> "`next_p_chain_reference_height > p_chain_reference_height` or `next_p_chain_reference_height == 0`" + +**Code (`verifyNextPChainHeightNormal`, verification.go):** Does not check that `NextPChainReferenceHeight > PChainReferenceHeight`. A proposer could set `NextPChainReferenceHeight` to a value less than `PChainReferenceHeight` if there happens to be a different validator set at that lower height. + +## 4. Missing enforcement that validator sets must differ when `next_p_chain_reference_height > 0` + +**README (line 142):** +> "If `next_p_chain_reference_height > 0`, the validator set derived from the P-chain height is **different** from the validator set derived by `p_chain_reference_height`" + +**Code (verification.go):** +```go +if currentValidatorSet.Compare(newValidatorSet) { + return nil // same validator set -- accepted without error +} +``` + +When validator sets are identical, the block is accepted even with `NextPChainReferenceHeight > 0`. This allows spurious epoch transitions with no actual validator set change. + +## 5. `prev_sealing_block_hash` for the first epoch + +**README (line 88-89):** +> "If there is no previous epoch (i.e., the current epoch is the first ever epoch), then it is nil." + +**Code (`createSealingBlock`, msm.go):** For epoch 1 (the first epoch), `PrevSealingBlockHash` is set to the hash of the first simplex block, not nil/zero. + +## 6. `NodeBLSMapping` missing `Weight` field in README + +**README (line 428-431):** +```proto +message NodeBLSMapping { + bytes node_id = 1; + bytes bls_key = 2; +} +``` + +**Code (encoding.go):** Has an additional `Weight uint64` field used for quorum calculations. The README omits it. + +## 7. Block digest computation + +**README (lines 401-411):** Describes the digest as the hash of a proto message `HashPreImage` with fields `h_i` (field 1) and `h_m` (field 2), which would include protobuf field tags and length prefixes. + +**Code (`Digest()`, msm.go):** +```go +combined := make([]byte, 64) +copy(combined[:32], blockDigest[:]) +copy(combined[32:], mdDigest[:]) +return sha256.Sum256(combined) +``` + +The code does a raw 64-byte concatenation, not a protobuf-encoded message. An implementation following the README would produce different hashes. + +## Summary + +| # | Discrepancy | Impact | +|---|------------|--------| +| 1 | `sealing_block_seq` value for sealing blocks | README self-contradictory | +| 2 | Signature over wrong P-chain height field | Potentially high -- code or README is wrong | +| 3 | No `NextPChainReferenceHeight > PChainReferenceHeight` check | Medium -- allows downward transitions | +| 4 | No enforcement that validator sets differ | Low -- allows spurious transitions | +| 5 | `prev_sealing_block_hash` non-nil for first epoch | Documentation mismatch | +| 6 | Missing `Weight` field in README | Documentation incomplete | +| 7 | Digest computation method | Interoperability risk | \ No newline at end of file diff --git a/msm/verification.go b/msm/verification.go new file mode 100644 index 00000000..0b4e77a5 --- /dev/null +++ b/msm/verification.go @@ -0,0 +1,510 @@ +// 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.Compare(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 := pChainReferenceHeightAsBytes(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 pChainReferenceHeightAsBytes(prev SimplexEpochInfo) []byte { + pChainHeight := prev.PChainReferenceHeight + 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 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 currentValidatorSet.Compare(newValidatorSet) { + return nil + } + + 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 prev.SealingBlockSeq != next.EpochNumber { + return fmt.Errorf("expected epoch number to be %d but got %d", prev.SealingBlockSeq, 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 inner block sequence number to be 0 but got %d", next.SealingBlockSeq) + } + case BlockTypeTelock: + if next.SealingBlockSeq != prev.SealingBlockSeq { + return fmt.Errorf("expected sealing inner block sequence number to be %d but got %d", prev.SealingBlockSeq, next.SealingBlockSeq) + } + case BlockTypeSealing: + md, err := simplex.ProtocolMetadataFromBytes(in.prevMD.SimplexProtocolMetadata) + if err != nil { + return fmt.Errorf("failed parsing protocol metadata: %w", err) + } + if next.SealingBlockSeq != md.Seq+1 { + return fmt.Errorf("expected sealing inner block sequence number to be %d but got %d", md.Seq+1, 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.Unix() + 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) + 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..1256dc83 --- /dev/null +++ b/msm/verification_test.go @@ -0,0 +1,1028 @@ +// 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.Unix()), + }, + { + name: "mismatching timestamp", + blockTime: now, + timestamp: uint64(now.Unix()) + 100, + err: fmt.Sprintf("expected timestamp to be %d but got %d", now.Unix(), int64(uint64(now.Unix())+100)), + }, + { + name: "timestamp too far in the future", + blockTime: now.Add(10 * time.Second), + timestamp: uint64(now.Add(10 * time.Second).Unix()), + 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.Unix()), + parentTimestamp: uint64(now.Unix()) + 10, + err: fmt.Sprintf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", uint64(now.Unix())+10, uint64(now.Unix())), + }, + } { + 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 + 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, + prev: SimplexEpochInfo{EpochNumber: 1, SealingBlockSeq: 10}, + next: SimplexEpochInfo{EpochNumber: 10}, + }, + { + name: "new epoch block not matching sealing seq", + nextBlockType: BlockTypeNewEpoch, + prev: SimplexEpochInfo{EpochNumber: 1, SealingBlockSeq: 10}, + 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, + 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 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 inner 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 inner 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 inner block sequence number to be 10 but got 11", + }, + { + name: "sealing block with correct seq (prev seq + 1)", + nextBlockType: BlockTypeSealing, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevProtocolMD}, + next: SimplexEpochInfo{SealingBlockSeq: 6}, + }, + { + name: "sealing block with wrong seq", + nextBlockType: BlockTypeSealing, + prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevProtocolMD}, + next: SimplexEpochInfo{SealingBlockSeq: 10}, + err: "expected sealing inner block sequence number to be 6 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 +} From d6c8cb080eeaebb48ba292d1db9737301ee42cda Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Mon, 30 Mar 2026 00:02:10 +0200 Subject: [PATCH 2/5] fix alignment to document Signed-off-by: Yacov Manevich --- msm/README.md | 14 ++---- msm/fake_node_test.go | 11 ++--- msm/msm.go | 58 ++++++++++------------ msm/msm_test.go | 6 +-- msm/readme-discrepancies.md | 97 ------------------------------------- msm/verification.go | 47 ++++++++++++------ msm/verification_test.go | 37 ++++++++++---- 7 files changed, 96 insertions(+), 174 deletions(-) delete mode 100644 msm/readme-discrepancies.md diff --git a/msm/README.md b/msm/README.md index 55449a60..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`. @@ -401,14 +401,7 @@ message StateMachineMetadata { The digest of the simplex block is computed as follows: 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: - -```proto -message HashPreImage { - bytes h_i = 1; // The inner block hash - bytes h_m = 2; // The metadata hash -} -``` +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. @@ -428,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 { diff --git a/msm/fake_node_test.go b/msm/fake_node_test.go index 1b113baf..a858f73a 100644 --- a/msm/fake_node_test.go +++ b/msm/fake_node_test.go @@ -276,7 +276,7 @@ func (fn *fakeNode) canFinalize() bool { func (fn *fakeNode) tryFinalizeNextBlock() { nextIndex := len(fn.finalizedBlocks) - if fn.isTelock(nextIndex) { + if fn.isNextBlockTelock() { return } @@ -296,14 +296,11 @@ func (fn *fakeNode) tryFinalizeNextBlock() { } } -func (fn *fakeNode) isTelock(index int) bool { - if index == 0 { +func (fn *fakeNode) isNextBlockTelock() bool { + if len(fn.finalizedBlocks) == 0 { return false } - return metadata.IdentifyBlockType( - fn.notarizedBlocks[index].Metadata, - fn.notarizedBlocks[index-1].Metadata, - ) == metadata.BlockTypeTelock + return fn.notarizedBlocks[len(fn.finalizedBlocks)].Metadata.SimplexEpochInfo.SealingBlockSeq > 0 } func (fn *fakeNode) buildAndNotarizeBlock() { diff --git a/msm/msm.go b/msm/msm.go index 5bb189c5..06eda71a 100644 --- a/msm/msm.go +++ b/msm/msm.go @@ -298,7 +298,7 @@ func (sm *StateMachine) init() { } func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block *StateMachineBlock, prevBlockMD StateMachineMetadata, prevMD StateMachineMetadata, state state, prevSeq uint64) error { - blockType := IdentifyBlockType(block.Metadata, prevBlockMD) + blockType := IdentifyBlockType(block.Metadata, prevBlockMD, prevSeq) timestamp := time.Unix(int64(prevMD.Timestamp), 0) if block.InnerBlock != nil { @@ -336,14 +336,11 @@ func (sm *StateMachine) identifyCurrentState(simplexEpochInfo SimplexEpochInfo) return stateBuildBlockNormalOp, nil } - // Else, NextPChainReferenceHeight > 0, so we're either in stateBuildCollectingApprovals or stateBuildBlockEpochSealed - if simplexEpochInfo.SealingBlockSeq == 0 { - // If we don't have a sealing block sequence yet, we're still collecting approvals for the validator set change. - return stateBuildCollectingApprovals, nil + if simplexEpochInfo.SealingBlockSeq > 0 || simplexEpochInfo.BlockValidationDescriptor != nil { + return stateBuildBlockEpochSealed, nil } - // Otherwise, we do have a sealing block sequence, so the epoch has been sealed. - return stateBuildBlockEpochSealed, nil + return stateBuildCollectingApprovals, nil } func computePrevVMBlockSeq(parentBlock StateMachineBlock, prevBlockSeq uint64) uint64 { @@ -418,9 +415,8 @@ func (sm *StateMachine) buildBlockAndMaybeTransitionEpoch(ctx context.Context, p return sm.wrapBlock(parentBlock, childBlock, newSimplexEpochInfo, pChainHeight, simplexMetadata, simplexBlacklist), nil } -func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachineMetadata) BlockType { +func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachineMetadata, prevSeq uint64) BlockType { simplexEpochInfo := nextBlockMD.SimplexEpochInfo - prevSimplexEpochInfo := prevBlockMD.SimplexEpochInfo // Only sealing blocks carry block validation descriptors @@ -433,14 +429,15 @@ func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachin // 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 { - if simplexEpochInfo.EpochNumber == prevSimplexEpochInfo.SealingBlockSeq { - return BlockTypeNewEpoch - } - // The zero-epoch block has BlockValidationDescriptor but SealingBlockSeq == 0, + // 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.SealingBlockSeq == 0 { + if prevSimplexEpochInfo.EpochNumber == 1 && prevSimplexEpochInfo.NextPChainReferenceHeight == 0 { return BlockTypeNormal } + + if simplexEpochInfo.EpochNumber == prevSeq { + return BlockTypeNewEpoch + } return BlockTypeTelock } @@ -647,12 +644,6 @@ func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock Stat simplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs simplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature - // If this is the sealing block, set the sealing block sequence. - md, err := simplex.ProtocolMetadataFromBytes(parentBlock.Metadata.SimplexProtocolMetadata) - if err != nil { - return nil, err - } - simplexEpochInfo.SealingBlockSeq = md.Seq + 1 validators, err := sm.GetValidatorSet(simplexEpochInfo.NextPChainReferenceHeight) if err != nil { return nil, err @@ -795,25 +786,28 @@ func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock S // 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: parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq, + SealingBlockSeq: sealingBlockSeq, PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), } - // First, we find the sequence of the sealing block. - seq := parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq - - // Do a sanity check just in case, make sure it's defined - if seq == 0 { - return nil, fmt.Errorf("cannot build epoch sealed inner block: sealing inner block sequence is 0 or undefined") - } - - _, finalization, err := sm.GetBlock(RetrievingOpts{Height: seq}) + _, finalization, err := sm.GetBlock(RetrievingOpts{Height: sealingBlockSeq}) if err != nil { - return nil, fmt.Errorf("failed to retrieve sealing inner block at sequence %d: %w", seq, err) + return nil, fmt.Errorf("failed to retrieve sealing block at sequence %d: %w", sealingBlockSeq, err) } isSealingBlockFinalized := finalization != nil @@ -828,7 +822,7 @@ func (sm *StateMachine) buildBlockEpochSealed(ctx context.Context, parentBlock S // 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: parentBlock.Metadata.SimplexEpochInfo.SealingBlockSeq, + EpochNumber: sealingBlockSeq, PrevVMBlockSeq: computePrevVMBlockSeq(parentBlock, prevBlockSeq), } diff --git a/msm/msm_test.go b/msm/msm_test.go index 3375b2ba..91404a35 100644 --- a/msm/msm_test.go +++ b/msm/msm_test.go @@ -403,7 +403,7 @@ func TestMSMNormalOp(t *testing.T) { mutateBlock: func(block *metadata.StateMachineBlock) { block.Metadata.SimplexEpochInfo.SealingBlockSeq = 5 }, - err: "expected sealing inner block sequence number to be 0 but got 5", + err: "expected sealing block sequence number to be 0 but got 5", }, { name: "wrong PChainReferenceHeight", @@ -821,7 +821,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { EpochNumber: 1, PrevVMBlockSeq: baseSeq + 5, NextPChainReferenceHeight: pChainHeight2, - SealingBlockSeq: baseSeq + 6, + SealingBlockSeq: 0, PrevSealingBlockHash: block1.Digest(), BlockValidationDescriptor: &metadata.BlockValidationDescriptor{ AggregatedMembership: metadata.AggregatedMembership{ @@ -839,7 +839,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.NoError(t, smVerify.VerifyBlock(context.Background(), block6)) - sealingSeq := block6.Metadata.SimplexEpochInfo.SealingBlockSeq + sealingSeq := baseSeq + 6 // The sealing block's sequence (md.Seq from step 6) backupStoreTC := tc.blockStore.clone() backupStoreTCVerify := tcVerify.blockStore.clone() diff --git a/msm/readme-discrepancies.md b/msm/readme-discrepancies.md deleted file mode 100644 index 88b81b68..00000000 --- a/msm/readme-discrepancies.md +++ /dev/null @@ -1,97 +0,0 @@ -# Discrepancies between README.md and Code - -The following discrepancies were found by comparing the MSM README specification against the implementation in the Go code. - -## 1. Sealing block `sealing_block_seq` -- README contradicts code and itself - -**README (line 246):** -> "A sealing block is identified by having the `block_validation_descriptor` defined and `sealing_block_seq` set to 0." - -**Code (`createSealingBlock`, msm.go):** -```go -simplexEpochInfo.SealingBlockSeq = md.Seq + 1 // the sealing block's own sequence, NOT 0 -``` - -The code sets `SealingBlockSeq` to the sealing block's own sequence number (always > 0). The README says it's 0. - -The README also contradicts itself -- line 248 says Telocks have `sealing_block_seq` set to the sealing block's sequence, and line 249 says Telocks are identified by `sealing_block_seq > 0`. If the sealing block itself had `sealing_block_seq == 0`, Telocks would inherit 0, which contradicts the `> 0` identification. - -## 2. Signature message -- `PChainReferenceHeight` vs `next_p_chain_reference_height` - -**README (line 211):** -> "The signature is over the `info` field of the `AuxiliaryInfo` and the **`next_p_chain_reference_height`** fields" - -**Code (`verifySignature`, verification.go):** -```go -pChainHeightBuff := pChainReferenceHeightAsBytes(prev) // prev.PChainReferenceHeight -bb.Write(pChainHeightBuff) -bb.Write(auxinfo.Info) -``` - -The code signs/verifies over `PChainReferenceHeight` (current epoch's height), **not** `NextPChainReferenceHeight`. Either the README or the code is wrong. If the README is correct, the code has a security vulnerability -- the signature doesn't bind to the target transition height. - -## 3. Missing enforcement of `next_p_chain_reference_height > p_chain_reference_height` - -**README (line 140):** -> "`next_p_chain_reference_height > p_chain_reference_height` or `next_p_chain_reference_height == 0`" - -**Code (`verifyNextPChainHeightNormal`, verification.go):** Does not check that `NextPChainReferenceHeight > PChainReferenceHeight`. A proposer could set `NextPChainReferenceHeight` to a value less than `PChainReferenceHeight` if there happens to be a different validator set at that lower height. - -## 4. Missing enforcement that validator sets must differ when `next_p_chain_reference_height > 0` - -**README (line 142):** -> "If `next_p_chain_reference_height > 0`, the validator set derived from the P-chain height is **different** from the validator set derived by `p_chain_reference_height`" - -**Code (verification.go):** -```go -if currentValidatorSet.Compare(newValidatorSet) { - return nil // same validator set -- accepted without error -} -``` - -When validator sets are identical, the block is accepted even with `NextPChainReferenceHeight > 0`. This allows spurious epoch transitions with no actual validator set change. - -## 5. `prev_sealing_block_hash` for the first epoch - -**README (line 88-89):** -> "If there is no previous epoch (i.e., the current epoch is the first ever epoch), then it is nil." - -**Code (`createSealingBlock`, msm.go):** For epoch 1 (the first epoch), `PrevSealingBlockHash` is set to the hash of the first simplex block, not nil/zero. - -## 6. `NodeBLSMapping` missing `Weight` field in README - -**README (line 428-431):** -```proto -message NodeBLSMapping { - bytes node_id = 1; - bytes bls_key = 2; -} -``` - -**Code (encoding.go):** Has an additional `Weight uint64` field used for quorum calculations. The README omits it. - -## 7. Block digest computation - -**README (lines 401-411):** Describes the digest as the hash of a proto message `HashPreImage` with fields `h_i` (field 1) and `h_m` (field 2), which would include protobuf field tags and length prefixes. - -**Code (`Digest()`, msm.go):** -```go -combined := make([]byte, 64) -copy(combined[:32], blockDigest[:]) -copy(combined[32:], mdDigest[:]) -return sha256.Sum256(combined) -``` - -The code does a raw 64-byte concatenation, not a protobuf-encoded message. An implementation following the README would produce different hashes. - -## Summary - -| # | Discrepancy | Impact | -|---|------------|--------| -| 1 | `sealing_block_seq` value for sealing blocks | README self-contradictory | -| 2 | Signature over wrong P-chain height field | Potentially high -- code or README is wrong | -| 3 | No `NextPChainReferenceHeight > PChainReferenceHeight` check | Medium -- allows downward transitions | -| 4 | No enforcement that validator sets differ | Low -- allows spurious transitions | -| 5 | `prev_sealing_block_hash` non-nil for first epoch | Documentation mismatch | -| 6 | Missing `Weight` field in README | Documentation incomplete | -| 7 | Digest computation method | Interoperability risk | \ No newline at end of file diff --git a/msm/verification.go b/msm/verification.go index 0b4e77a5..4e088212 100644 --- a/msm/verification.go +++ b/msm/verification.go @@ -221,7 +221,7 @@ func (nv *nextEpochApprovalsVerifier) verifySignature(prev SimplexEpochInfo, nex return fmt.Errorf("failed to aggregate public keys: %w", err) } - pChainHeightBuff := pChainReferenceHeightAsBytes(prev) + pChainHeightBuff := pChainNextReferenceHeightAsBytes(prev) var bb bytes.Buffer bb.Write(pChainHeightBuff) @@ -235,8 +235,8 @@ func (nv *nextEpochApprovalsVerifier) verifySignature(prev SimplexEpochInfo, nex return nil } -func pChainReferenceHeightAsBytes(prev SimplexEpochInfo) []byte { - pChainHeight := prev.PChainReferenceHeight +func pChainNextReferenceHeightAsBytes(prev SimplexEpochInfo) []byte { + pChainHeight := prev.NextPChainReferenceHeight pChainHeightBuff := make([]byte, 8) binary.BigEndian.PutUint64(pChainHeightBuff, pChainHeight) return pChainHeightBuff @@ -267,6 +267,10 @@ func (n *nextPChainReferenceHeightVerifier) Verify(in verificationInput) error { } 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) @@ -283,8 +287,12 @@ func (n *nextPChainReferenceHeightVerifier) verifyNextPChainHeightNormal(prevMD return err } - if currentValidatorSet.Compare(newValidatorSet) { - return nil + // If the validator set doesn't change, we shouldn't have increased the next P-chain reference height. + if currentValidatorSet.Compare(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() @@ -307,8 +315,8 @@ func (e *epochNumberVerifier) Verify(in verificationInput) error { switch in.nextBlockType { case BlockTypeNewEpoch: - if prev.SealingBlockSeq != next.EpochNumber { - return fmt.Errorf("expected epoch number to be %d but got %d", prev.SealingBlockSeq, next.EpochNumber) + 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 { @@ -326,19 +334,26 @@ func (s *sealingBlockSeqVerifier) Verify(in verificationInput) error { switch in.nextBlockType { case BlockTypeNewEpoch, BlockTypeNormal: if next.SealingBlockSeq != 0 { - return fmt.Errorf("expected sealing inner block sequence number to be 0 but got %d", next.SealingBlockSeq) + return fmt.Errorf("expected sealing block sequence number to be 0 but got %d", next.SealingBlockSeq) } case BlockTypeTelock: - if next.SealingBlockSeq != prev.SealingBlockSeq { - return fmt.Errorf("expected sealing inner block sequence number to be %d but got %d", prev.SealingBlockSeq, next.SealingBlockSeq) + // 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) } - case BlockTypeSealing: - md, err := simplex.ProtocolMetadataFromBytes(in.prevMD.SimplexProtocolMetadata) - if err != nil { - return fmt.Errorf("failed parsing protocol metadata: %w", err) + // 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) + } } - if next.SealingBlockSeq != md.Seq+1 { - return fmt.Errorf("expected sealing inner block sequence number to be %d but got %d", md.Seq+1, 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) diff --git a/msm/verification_test.go b/msm/verification_test.go index 1256dc83..7b7975e7 100644 --- a/msm/verification_test.go +++ b/msm/verification_test.go @@ -246,6 +246,7 @@ func TestEpochNumberVerifier(t *testing.T) { for _, tc := range []struct { name string nextBlockType BlockType + prevBlockSeq uint64 prev SimplexEpochInfo next SimplexEpochInfo err string @@ -260,13 +261,15 @@ func TestEpochNumberVerifier(t *testing.T) { { name: "new epoch block matching sealing seq", nextBlockType: BlockTypeNewEpoch, - prev: SimplexEpochInfo{EpochNumber: 1, SealingBlockSeq: 10}, + prevBlockSeq: 10, + prev: SimplexEpochInfo{EpochNumber: 1}, next: SimplexEpochInfo{EpochNumber: 10}, }, { name: "new epoch block not matching sealing seq", nextBlockType: BlockTypeNewEpoch, - prev: SimplexEpochInfo{EpochNumber: 1, SealingBlockSeq: 10}, + prevBlockSeq: 10, + prev: SimplexEpochInfo{EpochNumber: 1}, next: SimplexEpochInfo{EpochNumber: 5}, err: "expected epoch number to be 10 but got 5", }, @@ -300,6 +303,7 @@ func TestEpochNumberVerifier(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}, }) @@ -473,6 +477,21 @@ func TestNextPChainReferenceHeightVerifier(t *testing.T) { 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, @@ -891,7 +910,7 @@ func TestSealingBlockSeqVerifier(t *testing.T) { name: "normal block with non-zero sealing seq", nextBlockType: BlockTypeNormal, next: SimplexEpochInfo{SealingBlockSeq: 5}, - err: "expected sealing inner block sequence number to be 0 but got 5", + err: "expected sealing block sequence number to be 0 but got 5", }, { name: "new epoch block with zero sealing seq", @@ -902,7 +921,7 @@ func TestSealingBlockSeqVerifier(t *testing.T) { name: "new epoch block with non-zero sealing seq", nextBlockType: BlockTypeNewEpoch, next: SimplexEpochInfo{SealingBlockSeq: 3}, - err: "expected sealing inner block sequence number to be 0 but got 3", + err: "expected sealing block sequence number to be 0 but got 3", }, { name: "telock block matching prev sealing seq", @@ -915,20 +934,20 @@ func TestSealingBlockSeqVerifier(t *testing.T) { nextBlockType: BlockTypeTelock, prev: SimplexEpochInfo{SealingBlockSeq: 10}, next: SimplexEpochInfo{SealingBlockSeq: 11}, - err: "expected sealing inner block sequence number to be 10 but got 11", + err: "expected sealing block sequence number to be 10 but got 11", }, { - name: "sealing block with correct seq (prev seq + 1)", + name: "sealing block with zero seq", nextBlockType: BlockTypeSealing, prevMD: StateMachineMetadata{SimplexProtocolMetadata: prevProtocolMD}, - next: SimplexEpochInfo{SealingBlockSeq: 6}, + next: SimplexEpochInfo{SealingBlockSeq: 0}, }, { - name: "sealing block with wrong seq", + 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 6 but got 10", + err: "expected sealing inner block sequence number to be 0 but got 10", }, } { t.Run(tc.name, func(t *testing.T) { From f4b7e0ca1f404e051d4c817e32c82575b2b91d07 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Mon, 30 Mar 2026 16:32:53 +0200 Subject: [PATCH 3/5] Improve build_decision Signed-off-by: Yacov Manevich --- msm/build_decision.go | 40 +++++-- msm/build_decision_test.go | 17 +-- msm/encoding.canoto.go | 210 ------------------------------------- msm/encoding.go | 9 +- msm/fake_node_test.go | 6 +- msm/msm.go | 5 +- msm/msm_test.go | 3 +- 7 files changed, 49 insertions(+), 241 deletions(-) diff --git a/msm/build_decision.go b/msm/build_decision.go index e6939965..312274ea 100644 --- a/msm/build_decision.go +++ b/msm/build_decision.go @@ -7,47 +7,60 @@ 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 - blockBuildingDecisionTransitionEpoch - blockBuildingDecisionBuildBlockAndTransitionEpoch + 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 ) // 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) + WaitForProgress(ctx context.Context, pChainHeight uint64) error } type blockBuildingDecider struct { + logger Logger maxBlockBuildingWaitTime time.Duration pChainlistener PChainProgressListener waitForPendingBlock func(ctx context.Context) - shouldTransitionEpoch func() (bool, error) + 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() + 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. @@ -55,19 +68,23 @@ func (bbd *blockBuildingDecider) shouldBuildBlock( 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 waitForPendingBlock returned without the P-chain height changing, + // 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) @@ -79,13 +96,20 @@ func (bbd *blockBuildingDecider) waitForPChainChangeOrPendingBlock(ctx context.C go func() { defer wg.Done() - bbd.pChainlistener.WaitForProgress(pChainAwareContext, pChainHeight) + 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() diff --git a/msm/build_decision_test.go b/msm/build_decision_test.go index 9fce16fc..28435c91 100644 --- a/msm/build_decision_test.go +++ b/msm/build_decision_test.go @@ -16,8 +16,9 @@ type fakePChainListener struct { onListen func(ctx context.Context, pChainHeight uint64) } -func (f *fakePChainListener) WaitForProgress(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) { @@ -30,7 +31,7 @@ func TestShouldBuildBlock_VMSignalsBlock(t *testing.T) { }, }, waitForPendingBlock: func(ctx context.Context) {}, - shouldTransitionEpoch: func() (bool, error) { return false, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, getPChainHeight: func() uint64 { return 100 }, } @@ -55,7 +56,7 @@ func TestShouldBuildBlock_ContextCanceled(t *testing.T) { cancel() <-ctx.Done() }, - shouldTransitionEpoch: func() (bool, error) { return false, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, getPChainHeight: func() uint64 { return 100 }, } @@ -87,7 +88,7 @@ func TestShouldBuildBlock_PChainHeightChangeTriggersEpochTransition(t *testing.T waitForPendingBlock: func(ctx context.Context) { <-ctx.Done() }, - shouldTransitionEpoch: func() (bool, error) { + shouldTransitionEpoch: func(uint64) (bool, error) { return calls.Add(1) > 1, nil }, getPChainHeight: func() uint64 { return pChainHeight.Load() }, @@ -127,7 +128,7 @@ func TestShouldBuildBlock_PChainHeightChangeButNoEpochTransition(t *testing.T) { return } }, - shouldTransitionEpoch: func() (bool, error) { return false, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return false, nil }, getPChainHeight: func() uint64 { return pChainHeight.Load() }, } @@ -147,7 +148,7 @@ func TestShouldBuildBlock_EpochTransitionWithVMBlock(t *testing.T) { }, }, waitForPendingBlock: func(ctx context.Context) {}, - shouldTransitionEpoch: func() (bool, error) { return true, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, getPChainHeight: func() uint64 { return 100 }, } @@ -169,7 +170,7 @@ func TestShouldBuildBlock_EpochTransitionWithoutVMBlock(t *testing.T) { waitForPendingBlock: func(ctx context.Context) { <-ctx.Done() }, - shouldTransitionEpoch: func() (bool, error) { return true, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, getPChainHeight: func() uint64 { return 100 }, } @@ -194,7 +195,7 @@ func TestShouldBuildBlock_EpochTransitionContextCanceled(t *testing.T) { cancel() <-ctx.Done() }, - shouldTransitionEpoch: func() (bool, error) { return true, nil }, + shouldTransitionEpoch: func(uint64) (bool, error) { return true, nil }, getPChainHeight: func() uint64 { return 100 }, } diff --git a/msm/encoding.canoto.go b/msm/encoding.canoto.go index 62dc0f59..8cf85f44 100644 --- a/msm/encoding.canoto.go +++ b/msm/encoding.canoto.go @@ -2552,213 +2552,3 @@ func (c *ValidatorSetApproval) MarshalCanotoInto(w canoto.Writer) canoto.Writer } return w } - -const ( - canoto__StateMachineBlockPreImage__InnerBlockHash = 1 - canoto__StateMachineBlockPreImage__Metadata = 2 - - canoto__StateMachineBlockPreImage__InnerBlockHash__tag = "\x0a" // canoto.Tag(canoto__StateMachineBlockPreImage__InnerBlockHash, canoto.Len) - canoto__StateMachineBlockPreImage__Metadata__tag = "\x12" // canoto.Tag(canoto__StateMachineBlockPreImage__Metadata, canoto.Len) -) - -type canotoData_StateMachineBlockPreImage struct { - size uint64 -} - -// CanotoSpec returns the specification of this canoto message. -func (*StateMachineBlockPreImage) CanotoSpec(types ...reflect.Type) *canoto.Spec { - types = append(types, reflect.TypeOf(StateMachineBlockPreImage{})) - var zero StateMachineBlockPreImage - s := &canoto.Spec{ - Name: "StateMachineBlockPreImage", - Fields: []canoto.FieldType{ - { - FieldNumber: canoto__StateMachineBlockPreImage__InnerBlockHash, - Name: "InnerBlockHash", - OneOf: "", - TypeBytes: true, - }, - canoto.FieldTypeFromField( - /*type inference:*/ (&zero.Metadata), - /*FieldNumber: */ canoto__StateMachineBlockPreImage__Metadata, - /*Name: */ "Metadata", - /*FixedLength: */ 0, - /*Repeated: */ false, - /*OneOf: */ "", - /*types: */ types, - ), - }, - } - s.CalculateCanotoCache() - return s -} - -// MakeCanoto creates a new empty value. -func (*StateMachineBlockPreImage) MakeCanoto() *StateMachineBlockPreImage { - return new(StateMachineBlockPreImage) -} - -// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. -// -// During parsing, the canoto cache is saved. -func (c *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) UnmarshalCanotoFrom(r canoto.Reader) error { - // Zero the struct before unmarshaling. - *c = StateMachineBlockPreImage{} - 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__StateMachineBlockPreImage__InnerBlockHash: - if wireType != canoto.Len { - return canoto.ErrUnexpectedWireType - } - - if err := canoto.ReadBytes(&r, &c.InnerBlockHash); err != nil { - return err - } - if len(c.InnerBlockHash) == 0 { - return canoto.ErrZeroValue - } - case canoto__StateMachineBlockPreImage__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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) CalculateCanotoCache() { - if c == nil { - return - } - var size uint64 - if len(c.InnerBlockHash) != 0 { - size += uint64(len(canoto__StateMachineBlockPreImage__InnerBlockHash__tag)) + canoto.SizeBytes(c.InnerBlockHash) - } - (&c.Metadata).CalculateCanotoCache() - if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { - size += uint64(len(canoto__StateMachineBlockPreImage__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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) 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 *StateMachineBlockPreImage) MarshalCanotoInto(w canoto.Writer) canoto.Writer { - if c == nil { - return w - } - if len(c.InnerBlockHash) != 0 { - canoto.Append(&w, canoto__StateMachineBlockPreImage__InnerBlockHash__tag) - canoto.AppendBytes(&w, c.InnerBlockHash) - } - if fieldSize := (&c.Metadata).CachedCanotoSize(); fieldSize != 0 { - canoto.Append(&w, canoto__StateMachineBlockPreImage__Metadata__tag) - canoto.AppendUint(&w, fieldSize) - w = (&c.Metadata).MarshalCanotoInto(w) - } - return w -} diff --git a/msm/encoding.go b/msm/encoding.go index b49bffbd..3b3d70d9 100644 --- a/msm/encoding.go +++ b/msm/encoding.go @@ -333,11 +333,4 @@ func (vsa ValidatorSetApprovals) UniqueByNodeID() ValidatorSetApprovals { } }) return result -} - -type StateMachineBlockPreImage struct { - InnerBlockHash []byte `canoto:"bytes,1"` - Metadata StateMachineMetadata `canoto:"value,2"` - - canotoData canotoData_StateMachineBlockPreImage -} +} \ No newline at end of file diff --git a/msm/fake_node_test.go b/msm/fake_node_test.go index a858f73a..e519f1e4 100644 --- a/msm/fake_node_test.go +++ b/msm/fake_node_test.go @@ -181,14 +181,14 @@ type fakeNode struct { innerChain []innerBlock } -func (fn *fakeNode) WaitForProgress(ctx context.Context, pChainHeight uint64) { +func (fn *fakeNode) WaitForProgress(ctx context.Context, pChainHeight uint64) error { for { select { case <-ctx.Done(): - return + return ctx.Err() case <-time.After(10 * time.Millisecond): if fn.sm.GetPChainHeight() != pChainHeight { - return + return nil } } } diff --git a/msm/msm.go b/msm/msm.go index 06eda71a..5172cd04 100644 --- a/msm/msm.go +++ b/msm/msm.go @@ -358,13 +358,12 @@ func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock Stat } blockBuildingDecider := blockBuildingDecider{ + logger: sm.Logger, maxBlockBuildingWaitTime: sm.MaxBlockBuildingWaitTime, pChainlistener: sm.PChainProgressListener, getPChainHeight: sm.GetPChainHeight, waitForPendingBlock: sm.BlockBuilder.WaitForPendingBlock, - shouldTransitionEpoch: func() (bool, error) { - pChainHeight := sm.GetPChainHeight() - + shouldTransitionEpoch: func(pChainHeight uint64) (bool, error) { currentValidatorSet, err := sm.GetValidatorSet(parentBlock.Metadata.SimplexEpochInfo.PChainReferenceHeight) if err != nil { return false, err diff --git a/msm/msm_test.go b/msm/msm_test.go index 91404a35..24cdac5b 100644 --- a/msm/msm_test.go +++ b/msm/msm_test.go @@ -73,8 +73,9 @@ func (sv *signatureAggregator) AggregateSignatures(signatures ...[]byte) ([]byte type noOpPChainListener struct{} -func (n *noOpPChainListener) WaitForProgress(ctx context.Context, _ uint64) { +func (n *noOpPChainListener) WaitForProgress(ctx context.Context, _ uint64) error { <-ctx.Done() + return ctx.Err() } type blockBuilder struct { From ff02e140f6e8223d5618d90afb60b1d7500d71b5 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Mon, 30 Mar 2026 17:52:05 +0200 Subject: [PATCH 4/5] Fix test Signed-off-by: Yacov Manevich --- msm/fake_node_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/msm/fake_node_test.go b/msm/fake_node_test.go index e519f1e4..f8927249 100644 --- a/msm/fake_node_test.go +++ b/msm/fake_node_test.go @@ -137,13 +137,10 @@ func TestFakeNodeEmptyMempool(t *testing.T) { } require.Greater(t, node.Epoch(), uint64(1)) - return - t.Log("Epoch:", node.Epoch()) epoch := node.Epoch() require.Greater(t, epoch, uint64(1)) - require.Equal(t, node.Height(), uint64(20)) // Finally, we increase the P-Chain height again, which should cause the node to update its validator set and move to the new epoch. From eb3e1b2472aa66f5e151f6f622a5a2bc7e1bef2e Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Wed, 1 Apr 2026 18:53:36 +0200 Subject: [PATCH 5/5] Add some comments Signed-off-by: Yacov Manevich --- msm/build_decision.go | 17 +++ msm/build_decision_test.go | 1 + msm/encoding.go | 13 ++- msm/encoding_test.go | 2 +- msm/msm.go | 222 +++++++++++++++++++++++++------------ msm/msm_test.go | 20 ++-- msm/verification.go | 8 +- msm/verification_test.go | 14 +-- 8 files changed, 202 insertions(+), 95 deletions(-) diff --git a/msm/build_decision.go b/msm/build_decision.go index 312274ea..5ca1c1ac 100644 --- a/msm/build_decision.go +++ b/msm/build_decision.go @@ -23,6 +23,23 @@ const ( 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. diff --git a/msm/build_decision_test.go b/msm/build_decision_test.go index 28435c91..ec14a607 100644 --- a/msm/build_decision_test.go +++ b/msm/build_decision_test.go @@ -183,6 +183,7 @@ func TestShouldBuildBlock_EpochTransitionWithoutVMBlock(t *testing.T) { 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, diff --git a/msm/encoding.go b/msm/encoding.go index 3b3d70d9..b3c6d9bf 100644 --- a/msm/encoding.go +++ b/msm/encoding.go @@ -27,12 +27,23 @@ type OuterBlock struct { // 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 @@ -272,7 +283,7 @@ func (nbms NodeBLSMappings) SumWeights(selector func(int, NodeBLSMapping) bool) return total, err } -func (nbms NodeBLSMappings) Compare(other NodeBLSMappings) bool { +func (nbms NodeBLSMappings) Equal(other NodeBLSMappings) bool { if len(nbms) != len(other) { return false } diff --git a/msm/encoding_test.go b/msm/encoding_test.go index f432222f..8e2be3d6 100644 --- a/msm/encoding_test.go +++ b/msm/encoding_test.go @@ -538,7 +538,7 @@ func TestNodeBLSMappingsCompare(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, tt.a.Compare(tt.b)) + require.Equal(t, tt.expected, tt.a.Equal(tt.b)) }) } } diff --git a/msm/msm.go b/msm/msm.go index 5172cd04..79b0c991 100644 --- a/msm/msm.go +++ b/msm/msm.go @@ -14,6 +14,7 @@ import ( "time" "github.com/ava-labs/simplex" + "go.uber.org/zap" ) // ICMEpochInput defines the input for computing the ICM Epoch information for the next block. @@ -97,6 +98,9 @@ type RetrievingOpts struct { 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. @@ -180,26 +184,46 @@ const ( func (sm *StateMachine) BuildBlock(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata simplex.ProtocolMetadata, simplexBlacklist *simplex.Blacklist) (*StateMachineBlock, error) { sm.maybeInit() - simplexMetadataBytes := simplexMetadata.Bytes() + // 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 } - if simplexMetadata.Seq == 0 { - return nil, fmt.Errorf("invalid ProtocolMetadata sequence number: should be > 0, got %d", simplexMetadata.Seq) - } - + simplexMetadataBytes := simplexMetadata.Bytes() prevBlockSeq := simplexMetadata.Seq - 1 switch currentState { case stateFirstSimplexBlock: - return sm.buildBlockZeroEpoch(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes) + return sm.buildBlockZero(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes) case stateBuildBlockNormalOp: return sm.buildBlockNormalOp(ctx, parentBlock, simplexMetadataBytes, simplexBlacklistBytes, prevBlockSeq) case stateBuildCollectingApprovals: @@ -299,7 +323,7 @@ func (sm *StateMachine) init() { 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.Unix(int64(prevMD.Timestamp), 0) + timestamp := time.UnixMilli(int64(prevMD.Timestamp)) if block.InnerBlock != nil { timestamp = block.InnerBlock.Timestamp() @@ -326,44 +350,87 @@ func (sm *StateMachine) verifyNonZeroBlock(ctx context.Context, block *StateMach return block.InnerBlock.Verify(ctx) } -func (sm *StateMachine) identifyCurrentState(simplexEpochInfo SimplexEpochInfo) (state, error) { +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 simplexEpochInfo.EpochNumber == 0 { + if prevBlockSimplexEpochInfo.EpochNumber == 0 { return stateFirstSimplexBlock, nil } - if simplexEpochInfo.NextPChainReferenceHeight == 0 { + // 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 simplexEpochInfo.SealingBlockSeq > 0 || simplexEpochInfo.BlockValidationDescriptor != 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, + 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 @@ -374,31 +441,13 @@ func (sm *StateMachine) buildBlockNormalOp(ctx context.Context, parentBlock Stat return false, err } - if !currentValidatorSet.Compare(newValidatorSet) { + if !currentValidatorSet.Equal(newValidatorSet) { return true, nil } return false, nil }, } - - decisionToBuildBlock, pChainHeight, err := blockBuildingDecider.shouldBuildBlock(ctx) - if err != nil { - return nil, err - } - - var childBlock VMBlock - - switch decisionToBuildBlock { - case blockBuildingDecisionBuildBlock, blockBuildingDecisionBuildBlockAndTransitionEpoch: - return sm.buildBlockAndMaybeTransitionEpoch(ctx, parentBlock, simplexMetadata, simplexBlacklist, childBlock, decisionToBuildBlock, newSimplexEpochInfo, pChainHeight) - case blockBuildingDecisionTransitionEpoch: - 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) - } + 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) { @@ -408,6 +457,8 @@ func (sm *StateMachine) buildBlockAndMaybeTransitionEpoch(ctx context.Context, p } 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 } @@ -457,7 +508,9 @@ func IdentifyBlockType(nextBlockMD StateMachineMetadata, prevBlockMD StateMachin return BlockTypeNormal } -func (sm *StateMachine) buildBlockZeroEpoch(ctx context.Context, parentBlock StateMachineBlock, simplexMetadata, simplexBlacklist []byte) (*StateMachineBlock, error) { +// 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) @@ -468,8 +521,13 @@ func (sm *StateMachine) buildBlockZeroEpoch(ctx context.Context, parentBlock Sta 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 := constructSimplexEpochInfoForZeroEpoch(pChainHeight, newValidatorSet, prevVMBlockSeq) + simplexEpochInfo := constructSimplexZeroBlock(pChainHeight, newValidatorSet, prevVMBlockSeq) return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, simplexEpochInfo, pChainHeight) } @@ -513,12 +571,12 @@ func (sm *StateMachine) verifyBlockZero(ctx context.Context, block *StateMachine } membership := simplexEpochInfo.BlockValidationDescriptor.AggregatedMembership.Members - if !NodeBLSMappings(membership).Compare(expectedValidatorSet) { + 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 := constructSimplexEpochInfoForZeroEpoch(simplexEpochInfo.PChainReferenceHeight, expectedValidatorSet, prevVMBlockSeq) + expectedSimplexEpochInfo := constructSimplexZeroBlock(simplexEpochInfo.PChainReferenceHeight, expectedValidatorSet, prevVMBlockSeq) if !expectedSimplexEpochInfo.Equal(&simplexEpochInfo) { return fmt.Errorf("invalid SimplexEpochInfo: expected %v, got %v", expectedSimplexEpochInfo, simplexEpochInfo) @@ -547,10 +605,10 @@ func (sm *StateMachine) verifyZeroBlockTimestamp(block *StateMachineBlock, prevB if block.InnerBlock != nil { proposedTime = block.InnerBlock.Timestamp() } else { - proposedTime = time.Unix(int64(prevBlock.Metadata.Timestamp), 0) + proposedTime = time.UnixMilli(int64(prevBlock.Metadata.Timestamp)) } - expectedTimestamp := proposedTime.Unix() + 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)) } @@ -564,25 +622,31 @@ func (sm *StateMachine) verifyZeroBlockTimestamp(block *StateMachineBlock, prevB return proposedTime, nil } -func constructSimplexEpochInfoForZeroEpoch(pChainHeight uint64, newValidatorSet NodeBLSMappings, prevVMBlockSeq uint64) SimplexEpochInfo { +// 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 zero epoch. + 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 epoch. - PrevSealingBlockHash: [32]byte{}, // The zero epoch has no previous sealing block. + 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, @@ -590,59 +654,70 @@ func (sm *StateMachine) buildBlockCollectingApprovals(ctx context.Context, paren 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 := parentBlock.Metadata.SimplexEpochInfo.NextPChainReferenceHeight + 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 { - if newSimplexEpochInfo.NextEpochApprovals == nil { - newSimplexEpochInfo.NextEpochApprovals = &NextEpochApprovals{} - } - newSimplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs - newSimplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature - pChainHeight := parentBlock.Metadata.PChainHeight return sm.buildBlockImpatiently(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, pChainHeight) } - // Else, we create the sealing block. - return sm.createSealingBlock(ctx, parentBlock, simplexMetadata, simplexBlacklist, newSimplexEpochInfo, newApprovals, nextPChainHeight) + // 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() - ctx = impatientContext + start := time.Now() - childBlock, err := sm.BlockBuilder.BuildBlock(ctx, parentBlock.Metadata.ICMEpochInfo.PChainEpochHeight) - if err != nil && ctx.Err() == nil { - // If we got an error building the block, and we didn't time out, return the error. - // We failed to build the block. - return nil, err + 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)) } - // Else, either err == nil, and we've built the block, - // or err != nil but ctx.Err() != nil and we have waited MaxBlockBuildingWaitTime, - // so we need to build a block regardless of whether the inner VM wants to build a block. 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) { - // Update the approvals and signature in the simplex epoch info for the next block - if simplexEpochInfo.NextEpochApprovals == nil { - simplexEpochInfo.NextEpochApprovals = &NextEpochApprovals{} - } - simplexEpochInfo.NextEpochApprovals.NodeIDs = newApprovals.nodeIDs - simplexEpochInfo.NextEpochApprovals.Signature = newApprovals.signature - validators, err := sm.GetValidatorSet(simplexEpochInfo.NextPChainReferenceHeight) if err != nil { return nil, err @@ -653,14 +728,16 @@ func (sm *StateMachine) createSealingBlock(ctx context.Context, parentBlock Stat 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 newApprovals.canSeal && simplexEpochInfo.EpochNumber > 1 { - prevSealingBlock, _, err := sm.GetBlock(RetrievingOpts{Height: simplexEpochInfo.EpochNumber}) + 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) + + firstSimplexBlock, err := findFirstSimplexBlock(sm.GetBlock, sm.LatestPersistedHeight+1) if err != nil { return nil, fmt.Errorf("failed to find first simplex block: %w", err) } @@ -849,6 +926,7 @@ func computeICMEpochInfo(getUpgrades func() UpgradeConfig, icmEpochTransition IC 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 @@ -861,7 +939,7 @@ func (sm *StateMachine) wrapBlock(parentBlock StateMachineBlock, childBlock VMBl // nextICMEpoch returns the parent's ICM epoch info if hasChildBlock is false. if hasChildBlock { newTimestamp = childBlock.Timestamp() - timestamp = uint64(newTimestamp.Unix()) + timestamp = uint64(newTimestamp.UnixMilli()) } icmEpochInfo := nextICMEpochInfo(parentMetadata, hasChildBlock, getUpgrades, icmEpochTransition, newTimestamp) @@ -883,7 +961,7 @@ func nextICMEpochInfo(parentMetadata StateMachineMetadata, hasChildBlock bool, g icmEpochInfo := parentMetadata.ICMEpochInfo if hasChildBlock { - parentTimestamp := time.Unix(int64(parentMetadata.Timestamp), 0) + parentTimestamp := time.UnixMilli(int64(parentMetadata.Timestamp)) icmEpoch := computeICMEpochInfo(getUpgrades, icmEpochTransition, parentMetadata, parentTimestamp, newTimestamp) icmEpochInfo = ICMEpochInfo{ EpochStartTime: icmEpoch.EpochStartTime, diff --git a/msm/msm_test.go b/msm/msm_test.go index 24cdac5b..589ce956 100644 --- a/msm/msm_test.go +++ b/msm/msm_test.go @@ -316,7 +316,7 @@ func TestMSMFirstSimplexBlockAfterPreSimplexBlocks(t *testing.T) { Bytes: []byte{7, 8, 9}, }, Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(testConfig1.blockBuilder.block.Timestamp().Unix()), + Timestamp: uint64(testConfig1.blockBuilder.block.Timestamp().UnixMilli()), PChainHeight: 100, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -515,7 +515,7 @@ func TestMSMNormalOp(t *testing.T) { }, Metadata: metadata.StateMachineMetadata{ SimplexBlacklist: blacklist.Bytes(), - Timestamp: uint64(blockTime.Unix()), + Timestamp: uint64(blockTime.UnixMilli()), PChainHeight: testCase.expectedPChainHeight, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -637,7 +637,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(1), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(1 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(1 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight1, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -668,7 +668,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(2), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(2 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(2 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight1, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -693,7 +693,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(3), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(3 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(3 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -734,7 +734,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(4), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(4 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(4 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -774,7 +774,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(5), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(5 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(5 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -814,7 +814,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(6), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(6 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -884,7 +884,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nil, Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(6 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(6 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ @@ -908,7 +908,7 @@ func TestMSMFullEpochLifecycle(t *testing.T) { require.Equal(t, &metadata.StateMachineBlock{ InnerBlock: nextBlock(7), Metadata: metadata.StateMachineMetadata{ - Timestamp: uint64(startTime.Add(7 * time.Millisecond).Unix()), + Timestamp: uint64(startTime.Add(7 * time.Millisecond).UnixMilli()), PChainHeight: pChainHeight2, SimplexProtocolMetadata: md.Bytes(), SimplexEpochInfo: metadata.SimplexEpochInfo{ diff --git a/msm/verification.go b/msm/verification.go index 4e088212..e238426f 100644 --- a/msm/verification.go +++ b/msm/verification.go @@ -47,7 +47,7 @@ func (vd *validationDescriptorVerifier) verifySealingBlock(_ SimplexEpochInfo, n return err } - if !validators.Compare(next.BlockValidationDescriptor.AggregatedMembership.Members) { + 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) } @@ -288,7 +288,7 @@ func (n *nextPChainReferenceHeightVerifier) verifyNextPChainHeightNormal(prevMD } // If the validator set doesn't change, we shouldn't have increased the next P-chain reference height. - if currentValidatorSet.Compare(newValidatorSet) && next.NextPChainReferenceHeight > 0 { + 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", @@ -424,7 +424,7 @@ type timestampVerifier struct { } func (t *timestampVerifier) Verify(in verificationInput) error { - expectedTimestamp := in.proposedBlockTimestamp.Unix() + 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)) } @@ -447,7 +447,7 @@ 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) + firstEverSimplexBlockSeq, err := findFirstSimplexBlock(p.getBlock, *p.latestPersistedHeight+1) if err != nil { return fmt.Errorf("failed to find first Simplex inner block: %w", err) } diff --git a/msm/verification_test.go b/msm/verification_test.go index 7b7975e7..6a9c7302 100644 --- a/msm/verification_test.go +++ b/msm/verification_test.go @@ -138,26 +138,26 @@ func TestTimestampVerifier(t *testing.T) { { name: "matching timestamp", blockTime: now, - timestamp: uint64(now.Unix()), + timestamp: uint64(now.UnixMilli()), }, { name: "mismatching timestamp", blockTime: now, - timestamp: uint64(now.Unix()) + 100, - err: fmt.Sprintf("expected timestamp to be %d but got %d", now.Unix(), int64(uint64(now.Unix())+100)), + 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).Unix()), + 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.Unix()), - parentTimestamp: uint64(now.Unix()) + 10, - err: fmt.Sprintf("proposed block timestamp is older than parent block's timestamp, parent timestamp is %d but got %d", uint64(now.Unix())+10, uint64(now.Unix())), + 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) {