Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
20 changes: 18 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
33 changes: 15 additions & 18 deletions msm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -378,35 +378,30 @@ ____________________ |


```proto
message SimplexBlock {
message OuterBlock {
bytes inner_block = 1; // The inner block built by the VM, opaque to Simplex.
OuterBlock outer_block = 2; // The outer block that wraps the inner block without the inner block.
bytes protocol_metadata = 3 // The Simplex protocol metadata, set by the Simplex consensus protocol.
StateMachineMetadata metadata = 2; // The the metadata of the block.
}
```

where `OuterBlock` is a protobuf message that contains the ICM epoch information, the Simplex epoch information, and auxiliary information:
where `StateMachineMetadata` is a protobuf message that contains the ICM epoch information, the Simplex epoch information, and auxiliary information:

```proto
message OuterBlock {
message StateMachineMetadata {
ICMEpochInfo icm_epoch_info = 1; // The ICM epoch information.
SimplexEpochInfo simplex_epoch_info = 2; // The Simplex epoch information.
AuxiliaryInfo auxiliary_info = 3; // The auxiliary information.
bytes protocol_metadata = 3; // The Simplex protocol metadata, set by the Simplex consensus protocol.
bytes blacklist = 4; // The blacklist of the Simplex protocol.
AuxiliaryInfo auxiliary_info = 5; // The auxiliary information.
uint64 p_chain_height = 6; // The P-chain height sampled when building the block.
uint64 timestamp = 7; // The timestamp of the block, set by the block builder.
}
```

The digest of the simplex block is computed as follows:

Let $h_i$ be the hash of the inner block.
The digest of the Simplex block is the hash of the following encoding:

```proto
message HashPreImage {
bytes h_i = 1; // The inner block hash
OuterBlock outer_block = 2;
bytes protocol_metadata = 3;
}
```
Let $h_i$ be the hash of the inner block and $h_m$ be the hash of the metadata.
The digest of the Simplex block is the hash of the following encoding: `h_i || h_m` where `||` denotes concatenation.


This way of hashing the block allows any holder of a finalization certificate for the block to authenticate the block while hiding the content of the inner block.
Expand All @@ -426,6 +421,7 @@ The Simplex epoch information is a canoto encoded message with the following sch
message NodeBLSMapping {
bytes node_id = 1; // The nodeID
bytes bls_key = 2; // The BLS key of the node
uint64 weight = 3; // The weight of the node in the validator set, used for quorum calculations.
}

message BlockValidationDescriptor {
Expand Down Expand Up @@ -453,5 +449,6 @@ message SimplexEpochInfo {
uint64 prev_vm_block_seq = 5; // The sequence of the previous VM block
BlockValidationDescriptor block_validation_descriptor = 6; // Describes how to validate the blocks of the next epoch
NextEpochApprovals next_epoch_approvals = 7; // The epoch change approvals of the next epoch by at least n-f nodes.
uint64 sealing_block_seq = 8; // The sequence number of the sealing block of the current epoch.
}
```
158 changes: 158 additions & 0 deletions msm/build_decision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package metadata

import (
"context"
"sync"
"time"

"go.uber.org/zap"
)

// blockBuildingDecision represents the decision of whether we should build a block at the current time,
// and if so, whether we should also transition to a new epoch along the way.
type blockBuildingDecision int8

const (
blockBuildingDecisionUndefined blockBuildingDecision = iota
blockBuildingDecisionBuildBlock // We should build a block, and we don't need to transition to a new epoch.
blockBuildingDecisionTransitionEpoch // We should transition to a new epoch immediately, but we don't need to build a block.
blockBuildingDecisionBuildBlockAndTransitionEpoch // We should build a block and transition to a new epoch along the way.
blockBuildingDecisionContextCanceled
)

func (bbd blockBuildingDecision) String() string {
switch bbd {
case blockBuildingDecisionUndefined:
return "undefined"
case blockBuildingDecisionBuildBlock:
return "build block"
case blockBuildingDecisionTransitionEpoch:
return "transition epoch"
case blockBuildingDecisionBuildBlockAndTransitionEpoch:
return "build block and transition epoch"
case blockBuildingDecisionContextCanceled:
return "context canceled"
default:
return "unknown"
}
}

// PChainProgressListener listens for changes in the P-chain height.
type PChainProgressListener interface {
// WaitForProgress should block until either the context is cancelled, or the P-chain height has increased from the provided pChainHeight.
WaitForProgress(ctx context.Context, pChainHeight uint64) error
}

type blockBuildingDecider struct {
logger Logger
maxBlockBuildingWaitTime time.Duration
pChainlistener PChainProgressListener
waitForPendingBlock func(ctx context.Context)
shouldTransitionEpoch func(pChainHeight uint64) (bool, error)
getPChainHeight func() uint64
}

// shouldBuildBlock determines whether we should build a block at the current time,
// based on the current P-chain height and whether we should transition to a new epoch.
// It returns a blockBuildingDecision, the current P-chain height sampled at the time of deciding,
// and an error if the decision cannot be made.
// The P-chain height is returned because sampling the P-chain height afterwards might be inconsistent with the decision that was made.
func (bbd *blockBuildingDecider) shouldBuildBlock(
ctx context.Context,
) (blockBuildingDecision, uint64, error) {
for {
pChainHeight := bbd.getPChainHeight()

shouldTransitionEpoch, err := bbd.shouldTransitionEpoch(pChainHeight)
if err != nil {
return blockBuildingDecisionUndefined, 0, err
}

if shouldTransitionEpoch {
// If we should transition to a new epoch, maybe we can also build a block along the way.
return bbd.maybeBuildBlockWithEpochTransition(ctx), pChainHeight, nil
}

// Else, we don't need to transition to a new epoch, but maybe we should build a block.
// We wait for either the P-chain height to change, or for a block to be ready to be built.
bbd.waitForPChainChangeOrPendingBlock(ctx, pChainHeight)

// If the context was cancelled in the meantime, abandon evaluation.
if bbd.wasContextCanceled(ctx) {
return blockBuildingDecisionContextCanceled, 0, nil
}

// If we've reached here, either the P-chain height has changed, or a block is ready to be built.

// If the P-chain height changed, re-evaluate again whether we should transition to a new epoch,
// or continue waiting to build a block.
if bbd.getPChainHeight() != pChainHeight {
continue
}

// Else, we have reached here because a block is ready to be built, and the P-chain height has not changed,
// which means we should build a block.

return blockBuildingDecisionBuildBlock, pChainHeight, nil
}
}

// waitForPChainChangeOrPendingBlock waits until either the given P-chain height changes from the provided pChainHeight,
// or a block is ready to be built.
func (bbd *blockBuildingDecider) waitForPChainChangeOrPendingBlock(ctx context.Context, pChainHeight uint64) {
pChainAwareContext, cancel := context.WithCancel(ctx)

var wg sync.WaitGroup
wg.Add(1)

defer wg.Wait()
defer cancel()

go func() {
defer wg.Done()
err := bbd.pChainlistener.WaitForProgress(pChainAwareContext, pChainHeight)
if err != nil && pChainAwareContext.Err() == nil{
bbd.logger.Warn("error while waiting for P-chain progress", zap.Error(err))
}
cancel()
}()

bbd.waitForPendingBlock(pChainAwareContext)
}

// maybeBuildBlockWithEpochTransition decides if we should build a block while transitioning to a new epoch.
// It waits up to a limited amount of time (bbd.maxBlockBuildingWaitTime) for a block to be ready to be built,
// and if no block is ready by then, it returns the decision to transition epoch without building a block.
// Otherwise, it returns the decision to build a block and transition epoch along the way.
func (bbd *blockBuildingDecider) maybeBuildBlockWithEpochTransition(ctx context.Context) blockBuildingDecision {
impatientContext, cancel := context.WithTimeout(ctx, bbd.maxBlockBuildingWaitTime)
defer cancel()

// We should transition to a new epoch, so we wait some time just in case we can also build a block along the way.
// waitForPendingBlock will return in case a block is ready to be built, or when the context times out.
bbd.waitForPendingBlock(impatientContext)

if impatientContext.Err() != nil {
// Check if we have returned because the parent context was cancelled
if bbd.wasContextCanceled(ctx) {
return blockBuildingDecisionContextCanceled
}
// We have returned from waitForPendingBlock because the context has timed out, which means we don't need to build a block.
return blockBuildingDecisionTransitionEpoch
}

// Block is ready to be built
return blockBuildingDecisionBuildBlockAndTransitionEpoch
}

func (bbd *blockBuildingDecider) wasContextCanceled(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
Loading
Loading