Skip to content

docs: snow package #2035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ linters:
- "**/proto/**/*.go"
- "**/pubsub/**/*.go"
- "**/requester/**/*.go"
- "**/snow/**/*.go"
- "**/state/**/*.go"
- "**/statesync/**/*.go"
- "**/storage/**/*.go"
Expand Down
106 changes: 88 additions & 18 deletions snow/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,36 @@ var (
errMismatchedPChainContext = errors.New("mismatched P-Chain context")
)

// Block is a union of methods required by snowman.Block and block.WithVerifyContext
type Block interface {
fmt.Stringer
// GetID returns the ID of the block
GetID() ids.ID

// GetParent returns the ID of the parent block
GetParent() ids.ID

// GetTimestamp returns the timestamp of the block
GetTimestamp() int64

// GetBytes returns the bytes of the block
GetBytes() []byte

// GetHeight returns the height of the block
GetHeight() uint64

// GetContext returns the P-Chain context of the block.
// May return nil if there is no P-Chain context, which
// should only occur prior to ProposerVM activation.
// This will be verified from the snow package, so that the
// inner chain can simply use its embedded context.
GetContext() *block.Context

fmt.Stringer
}

// StatefulBlock implements snowman.Block and abstracts away the caching
// and block pinning required by the AvalancheGo Consensus engine.
// This converts the VM DevX from implementing the consensus engine specific invariants
// StatefulBlock implements snowman.Block.
// It abstracts caching and block pinning required by the AvalancheGo Consensus engine.
// This converts the VM DevX from implementing the consensus engine-specific invariants
// to implementing an input/output/accepted block type and handling the state transitions
// between these types.
// In conjunction with the AvalancheGo Consensus engine, this code guarantees that
Expand All @@ -56,6 +68,8 @@ type Block interface {
// verified/accepted to update a moving state sync target.
// After FinishStateSync is called, the snow package guarantees the same invariants
// as applied during normal consensus.
//
// [snowman.Block]: https://github.com/ava-labs/avalanchego/blob/abb1a9a6a21c3dbce6dff5cdcea03173119a5f46/snow/consensus/snowman/block.go#L24
type StatefulBlock[I Block, O Block, A Block] struct {
Input I
Output O
Expand All @@ -66,16 +80,30 @@ type StatefulBlock[I Block, O Block, A Block] struct {
vm *VM[I, O, A]
}

// NewInputBlock creates a new unverified StatefulBlock.
//
// Returns:
// - A new StatefulBlock containing only the input block
//
// This function emulates the initial state of a block that has been built
// but not verified
func NewInputBlock[I Block, O Block, A Block](
vm *VM[I, O, A],
input I,
) *StatefulBlock[I, O, A] {
return &StatefulBlock[I, O, A]{
Input: input,
vm: vm,
Input: input,
}
}

// NewVerifiedBlock creates a StatefulBlock after a block has been built and verified but prior to being accepted/rejected by consensus.
//
// Returns:
// - A new verified StatefulBlock containing the input block, output block with state transitions
//
// This function emulates the state of a block that has passed verification
// but has not yet been accepted into the blockchain by consensus
func NewVerifiedBlock[I Block, O Block, A Block](
vm *VM[I, O, A],
input I,
Expand All @@ -89,6 +117,14 @@ func NewVerifiedBlock[I Block, O Block, A Block](
}
}

// NewAcceptedBlock creates a new StatefulBlock accepted by consensus and committed to the chain.
//
// Returns:
// - A new StatefulBlock containing the input block, output block, accepted block, and both verification
// and acceptance status set to true.
//
// This function emulates the final state of a block that has been fully processed, verified,
// and accepted into the blockchain by the consensus mechanism.
func NewAcceptedBlock[I Block, O Block, A Block](
vm *VM[I, O, A],
input I,
Expand Down Expand Up @@ -136,14 +172,18 @@ func (b *StatefulBlock[I, O, A]) accept(ctx context.Context, parentAccepted A) e
return nil
}

// ShouldVerifyWithContext returns true if the block should be verified with the provided context.
// Always returns true
func (*StatefulBlock[I, O, A]) ShouldVerifyWithContext(context.Context) (bool, error) {
return true, nil
}

// VerifyWithContext verifies the block with P-Chain context
func (b *StatefulBlock[I, O, A]) VerifyWithContext(ctx context.Context, pChainCtx *block.Context) error {
return b.verifyWithContext(ctx, pChainCtx)
}

// Verify block
func (b *StatefulBlock[I, O, A]) Verify(ctx context.Context) error {
return b.verifyWithContext(ctx, nil)
}
Expand Down Expand Up @@ -291,7 +331,16 @@ func (b *StatefulBlock[I, O, A]) notifyAccepted(ctx context.Context) error {
return event.NotifyAll(ctx, b.Accepted, b.vm.acceptedSubs...)
}

// implements "snowman.Block.choices.Decidable"
// Accept implements the snowman.Block.[Decidable] interface.
// It marks this block as accepted by consensus
// If the VM is ready, it ensures the block is verified and then calls markAccepted with its parent to process state transitions.
//
// If the VM is not ready (during state sync),
// it deletes it from [verifiedBlocks], sets the last accepted block to this block, and notifies subscribers.
// We are guaranteed that the block will eventually be accepted by consensus.
//
// [Decidable]: https://github.com/ava-labs/avalanchego/blob/abb1a9a6a21c3dbce6dff5cdcea03173119a5f46/snow/decidable.go#L16
// [verifiedBlocks]: https://github.com/ava-labs/hypersdk/blob/ae0c960050860ad72468e5c3687966366582ba1a/snow/vm.go#L165
func (b *StatefulBlock[I, O, A]) Accept(ctx context.Context) error {
b.vm.chainLock.Lock()
defer b.vm.chainLock.Unlock()
Expand Down Expand Up @@ -329,7 +378,12 @@ func (b *StatefulBlock[I, O, A]) Accept(ctx context.Context) error {
return b.markAccepted(ctx, parent)
}

// implements "snowman.Block.choices.Decidable"
// Reject implements the snowman.Block.[Decidable] interface.
// It removes the block from the verified blocks map (VM.verifiedBlocks) and notifies subscribers
// that consensus rejected the block. For any particular block, either
// Accept or Reject will be called, never both.
//
// [Decidable]: https://github.com/ava-labs/avalanchego/blob/abb1a9a6a21c3dbce6dff5cdcea03173119a5f46/snow/decidable.go#L16
func (b *StatefulBlock[I, O, A]) Reject(ctx context.Context) error {
ctx, span := b.vm.tracer.Start(ctx, "StatefulBlock.Reject")
defer span.End()
Expand All @@ -346,21 +400,37 @@ func (b *StatefulBlock[I, O, A]) Reject(ctx context.Context) error {
return event.NotifyAll[O](ctx, b.Output, b.vm.rejectedSubs...)
}

// implements "snowman.Block"
func (b *StatefulBlock[I, O, A]) ID() ids.ID { return b.Input.GetID() }
func (b *StatefulBlock[I, O, A]) Parent() ids.ID { return b.Input.GetParent() }
func (b *StatefulBlock[I, O, A]) Height() uint64 { return b.Input.GetHeight() }
// ID returns id of Input block
func (b *StatefulBlock[I, O, A]) ID() ids.ID { return b.Input.GetID() }

// Parent returns parent ID of Input block
func (b *StatefulBlock[I, O, A]) Parent() ids.ID { return b.Input.GetParent() }

// Height returns height of Input block
func (b *StatefulBlock[I, O, A]) Height() uint64 { return b.Input.GetHeight() }

// Timestamp returns timestamp in milliseconds of the Input block
func (b *StatefulBlock[I, O, A]) Timestamp() time.Time { return time.UnixMilli(b.Input.GetTimestamp()) }
func (b *StatefulBlock[I, O, A]) Bytes() []byte { return b.Input.GetBytes() }

// Implements GetXXX for internal consistency
func (b *StatefulBlock[I, O, A]) GetID() ids.ID { return b.Input.GetID() }
func (b *StatefulBlock[I, O, A]) GetParent() ids.ID { return b.Input.GetParent() }
func (b *StatefulBlock[I, O, A]) GetHeight() uint64 { return b.Input.GetHeight() }
// Bytes return the serialized bytes of the Input block
func (b *StatefulBlock[I, O, A]) Bytes() []byte { return b.Input.GetBytes() }

// GetID returns ID of Input block
func (b *StatefulBlock[I, O, A]) GetID() ids.ID { return b.Input.GetID() }

// GetParent returns parent ID of Input block
func (b *StatefulBlock[I, O, A]) GetParent() ids.ID { return b.Input.GetParent() }

// GetHeight returns height of Input block
func (b *StatefulBlock[I, O, A]) GetHeight() uint64 { return b.Input.GetHeight() }

// GetTimestamp returns timestamp in milliseconds of the Input block
func (b *StatefulBlock[I, O, A]) GetTimestamp() int64 { return b.Input.GetTimestamp() }
func (b *StatefulBlock[I, O, A]) GetBytes() []byte { return b.Input.GetBytes() }

// implements "fmt.Stringer"
// GetBytes return the serialized bytes of the Input block
func (b *StatefulBlock[I, O, A]) GetBytes() []byte { return b.Input.GetBytes() }

// String implements fmt.Stringer
func (b *StatefulBlock[I, O, A]) String() string {
return fmt.Sprintf("(%s, verified = %t, accepted = %t)", b.Input, b.verified, b.accepted)
}
42 changes: 36 additions & 6 deletions snow/chain_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,28 @@ import (
// The VM provides a caching layer on top of ChainIndex, so the implementation
// does not need to provide its own caching layer.
type ChainIndex[T Block] interface {
// UpdateLastAccepted updates the chain's last accepted block record and manages block storage.
// It:
// 1. Persists the new last accepted block
// 2. Maintains cross-reference indices between block IDs and heights
// 3. Prunes old blocks beyond the retention window
// 4. Periodically triggers database compaction to reclaim space
// This function is called whenever a block is accepted by consensus
UpdateLastAccepted(ctx context.Context, blk T) error

// GetLastAcceptedHeight returns the height of the last accepted block
GetLastAcceptedHeight(ctx context.Context) (uint64, error)

// GetBlock returns the block with the given ID
GetBlock(ctx context.Context, blkID ids.ID) (T, error)

// GetBlockIDAtHeight returns the ID of the block at the given height
GetBlockIDAtHeight(ctx context.Context, blkHeight uint64) (ids.ID, error)

// GetBlockIDHeight returns the height of the block with the given ID
GetBlockIDHeight(ctx context.Context, blkID ids.ID) (uint64, error)

// GetBlockByHeight returns the block at the given height
GetBlockByHeight(ctx context.Context, blkHeight uint64) (T, error)
}

Expand Down Expand Up @@ -95,16 +112,28 @@ func (v *VM[I, O, A]) reprocessFromOutputToInput(ctx context.Context, targetInpu
return NewAcceptedBlock(v, targetInputBlock, outputBlock, acceptedBlock), nil
}

// ConsensusIndex provides a wrapper around the VM, which enables the chain developer to share the
// caching layer provided by the VM and used in the consensus engine.
// The ConsensusIndex additionally provides access to the accepted/preferred frontier by providing
// accessors to the latest type of the frontier.
// ie. last accepted block is guaranteed to have Accepted type available, whereas the preferred block
// is only guaranteed to have the Output type available.
// ConsensusIndex provides access to the current consensus state while offering
// type-safety for blocks in different stages of processing.
//
// It serves two main purposes:
//
// 1. Provides developers with access to the caching layer built into the VM,
// eliminating the need to implement separate caching.
// 2. Offers specialized accessors to blocks at different points in the consensus frontier:
// - GetLastAccepted() returns the block in its Accepted (A) state, which is guaranteed
// to be fully committed to the chain.
// - GetPreferredBlock() returns the block in its Output (O) state, representing
// the current preference that has been verified but may not yet be accepted.
//
// This type-safe approach ensures developers always have the appropriate block representation
// based on its consensus status. For instance, a preferred block is only guaranteed to have
// reached the Output state, while the last accepted block is guaranteed to have reached
// the Accepted state with all its state transitions finalized.
type ConsensusIndex[I Block, O Block, A Block] struct {
vm *VM[I, O, A]
}

// GetBlockByHeight retrieves the block at the specified height
func (c *ConsensusIndex[I, O, A]) GetBlockByHeight(ctx context.Context, height uint64) (I, error) {
blk, err := c.vm.GetBlockByHeight(ctx, height)
if err != nil {
Expand All @@ -113,6 +142,7 @@ func (c *ConsensusIndex[I, O, A]) GetBlockByHeight(ctx context.Context, height u
return blk.Input, nil
}

// GetBlock fetches the input block of the given block ID
func (c *ConsensusIndex[I, O, A]) GetBlock(ctx context.Context, blkID ids.ID) (I, error) {
blk, err := c.vm.GetBlock(ctx, blkID)
if err != nil {
Expand Down
17 changes: 15 additions & 2 deletions snow/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,44 @@ import (
)

const (
SnowVMConfigKey = "snowvm"
TracerConfigKey = "tracer"
// SnowVMConfigKey is the key for the snow VM config
SnowVMConfigKey = "snowvm"

// TracerConfigKey is the key for the tracer config
TracerConfigKey = "tracer"

// ContinuousProfilerKey is the key for the continuous profiler config
ContinuousProfilerKey = "continuousProfiler"
)

// VMConfig contains configuration for parsed block cache size and accepted block window cache size
type VMConfig struct {
ParsedBlockCacheSize int `json:"parsedBlockCacheSize"`
AcceptedBlockWindowCache int `json:"acceptedBlockWindowCache"`
}

// NewDefaultVMConfig returns a default VMConfig
func NewDefaultVMConfig() VMConfig {
return VMConfig{
ParsedBlockCacheSize: 128,
AcceptedBlockWindowCache: 128,
}
}

// GetVMConfig returns the VMConfig from the context.Config. If the config does not contain a VMConfig,
// it returns a default VMConfig.
func GetVMConfig(config context.Config) (VMConfig, error) {
return context.GetConfig(config, SnowVMConfigKey, NewDefaultVMConfig())
}

// GetProfilerConfig returns the profiler.Config. If the config does not contain a profiler.Config,
// it disables profiling.
func GetProfilerConfig(config context.Config) (profiler.Config, error) {
return context.GetConfig(config, ContinuousProfilerKey, profiler.Config{Enabled: false})
}

// GetTracerConfig returns the trace.Config. If the config does not contain a trace.Config,
// it disables tracing.
func GetTracerConfig(config context.Config) (trace.Config, error) {
return context.GetConfig(config, TracerConfigKey, trace.Config{Enabled: false})
}
28 changes: 23 additions & 5 deletions snow/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ var (
errVMNotReady = errors.New("vm not ready")
)

// HealthCheck is a concrete implementation of health.Checker interface
// that reports the health and readiness of the VM.
//
// The health and readiness of the VM is determined by the health checkers registered with the VM.
//
// The health checkers are registered with the VM using RegisterHealthChecker.
func (v *VM[I, O, A]) HealthCheck(ctx context.Context) (any, error) {
var (
details = make(map[string]any)
Expand All @@ -43,6 +49,7 @@ func (v *VM[I, O, A]) HealthCheck(ctx context.Context) (any, error) {
return details, errors.Join(errs...)
}

// RegisterHealthChecker registers a health checker with the VM
func (v *VM[I, O, A]) RegisterHealthChecker(name string, healthChecker health.Checker) error {
if _, ok := v.healthCheckers.LoadOrStore(name, healthChecker); ok {
return fmt.Errorf("duplicate health checker for %s", name)
Expand Down Expand Up @@ -76,12 +83,19 @@ func (v *vmReadinessHealthCheck) HealthCheck(_ context.Context) (any, error) {
return ready, nil
}

// unresolvedBlockHealthCheck
// unresolvedBlockHealthCheck monitors blocks that require explicit resolution through the consensus process.
//
// During state sync, blocks are vacuously marked as verified because the VM lacks the state required
// to properly verify them.
// Assuming a correct validator set and consensus, any invalid blocks will eventually be rejected by
// the network and this node.
// This check reports unhealthy until any such blocks have been cleared from the processing set.
// to properly verify them. When state sync completes, these blocks must go through proper verification,
// but some may fail.
//
// This health check ensures chain integrity by tracking these blocks until they're explicitly
// rejected by consensus. Without such tracking:
// - The node wouldn't differentiate between properly handled rejections and processing errors
// - Chain state could become inconsistent if invalid blocks disappeared without formal rejection
//
// The health check reports unhealthy as long as any blocks remain unresolved, ensuring that
// the chain doesn't report full health until all blocks have been properly processed.
type unresolvedBlockHealthCheck[I Block] struct {
lock sync.RWMutex
unresolvedBlocks set.Set[ids.ID]
Expand All @@ -93,13 +107,17 @@ func newUnresolvedBlocksHealthCheck[I Block](unresolvedBlkIDs set.Set[ids.ID]) *
}
}

// Resolve marks a block as properly handled by the consensus process.
// This is called when a block is explicitly rejected, removing it from the unresolved set.
func (u *unresolvedBlockHealthCheck[I]) Resolve(blkID ids.ID) {
u.lock.Lock()
defer u.lock.Unlock()

u.unresolvedBlocks.Remove(blkID)
}

// HealthCheck reports error if any blocks remain unresolved.
// Returns the count of unresolved blocks.
func (u *unresolvedBlockHealthCheck[I]) HealthCheck(_ context.Context) (any, error) {
u.lock.RLock()
unresolvedBlocks := u.unresolvedBlocks.Len()
Expand Down
Loading