Skip to content
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

Separate snow package from snow vm refactor #1846

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

aaronbuchwald
Copy link
Collaborator

This PR breaks out the snow package from the Snow VM refactor: #1831

Breaking this out to make it an additive change. You can see how this refactor impacts the HyperVM and MorpheusVM example implementation in the original PR.

The added snow package implements the types required by the AvalancheGo consensus engine and decomposes them so that a VM developer overrides only the components relevant to their VM and implements the minimum Chain interface:

  • Initialize the chain and take in all inputs from AvalancheGo
  • Build
  • Parse
  • Execute
  • Accept

This PR additionally adds unit tests that the snow package correctly maintains the AvalancheGo consensus engine invariants and a fuzz test.

@aaronbuchwald aaronbuchwald marked this pull request as ready for review December 30, 2024 16:52
return &InputCovariantVM[I, O, A]{a.vm.covariantVM}
}

func (a *Application[I, O, A]) WithAcceptedSub(sub ...event.Subscription[A]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With* naming is usually reserved for when a type returns itself which also supports chaining, this is more analagous to a Set* function

chainInput ChainInput,
makeChainIndex MakeChainIndexFunc[I, O, A],
app *Application[I, O, A],
) (BlockChainIndex[I], error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is Chain responsible for creating the index? Can't the snow VM implementation own this because it handles all blocks from consensus?

) (BlockChainIndex[I], error)
BuildBlock(ctx context.Context, parent O) (I, O, error)
ParseBlock(ctx context.Context, bytes []byte) (I, error)
Execute(
Copy link
Contributor

@joshua-kim joshua-kim Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call this Verify?

"github.com/ava-labs/hypersdk/event"
)

type Application[I Block, O Block, A Block] struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could actually probably get rid of Application entirely and just merge this code into VM. If we want to hide stuff from Chain we can do that by un-exporting the corresponding functions

AcceptBlock(ctx context.Context, acceptedParent A, outputBlock O) (A, error)
}

type VM[I Block, O Block, A Block] struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the relationship between VM, *CovariantVM, and Application are confusing, but this is my understanding of them - correct me if I'm wrong on any of these...

  1. snow.VM is an adapter/wrapper pattern to make the hypervm compatible with avalanchego consensus
  2. snow.Application is meant to be a way to expose the VM without too many internal implementation details to snow.Chain
  3. snow.*CovariantVM exists because we want Chain to be able to see a concrete type instead of an opaque interface
  4. snow.Chain exists because we want developers to be able to customize a set of vm-level behavior

Some ideas to simplify the relationship between these types:

  1. snow.VM + the related consensus adapter types (e.g StatefulBlock) I don't think are relevant to the developer and bloat the package. I think we should move these to internal to internal/snow.
  2. *CovariantVM is awkward because it's purpose is to implement the VM with type information for a better DX, but has a circular dependency with the VM, because *CovariantVM needs a reference to information owned by VM (example, because VM and *CovariantVM share the responsibility of implementing the "entire" VM, but divide the implementation between typed/untyped responsibilities. I think we can simplify by implementing the entire vm in CovariantVM and just wrapping it with snow.VM to erase type information where needed to conform to avalanchego's interface, so that snow.VM is just a simple wrapper.
  3. I also think that instead of having two separate covariant wrappers for the input/output blocks that we can just have a single covariant type like Block{} that contain both the input/output type to simplify the Chain DX.

Comment on lines +183 to +202
func PrefixBlockKey(height uint64) []byte {
k := make([]byte, 1+consts.Uint64Len)
k[0] = blockPrefix
binary.BigEndian.PutUint64(k[1:], height)
return k
}

func PrefixBlockIDHeightKey(id ids.ID) []byte {
k := make([]byte, 1+ids.IDLen)
k[0] = blockIDHeightPrefix
copy(k[1:], id[:])
return k
}

func PrefixBlockHeightIDKey(height uint64) []byte {
k := make([]byte, 1+consts.Uint64Len)
k[0] = blockHeightIDPrefix
binary.BigEndian.PutUint64(k[1:], height)
return k
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be un-exported?

}

func (c *ChainStore[T]) GetLastAcceptedHeight(_ context.Context) (uint64, error) {
lastAcceptedHeightBytes, err := c.db.Get([]byte{lastAcceptedByte})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it might make sense to just make this byte array a var since we're frequently re-using it

Register(name string, gatherer prometheus.Gatherer) error
}

type Context struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally dislike these Context patterns (we do this in avalanchego... but I still dislike snow.Context because I feel like it makes it too easy to pass around global state/dependencies that a type really doesn't care about. It's also unclear to me why some dependencies in Initialize exist/don't exist in snow.Context, like the database... I feel like we should be consistent). I would probably prefer us just passing dependencies around individually as parameters.

Comment on lines +20 to +38
func (c Config) Get(key string) ([]byte, bool) {
if val, ok := c[key]; ok {
return val, true
}
return nil, false
}

func GetConfig[T any](c Config, key string, defaultConfig T) (T, error) {
val, ok := c[key]
if !ok {
return defaultConfig, nil
}

var emptyConfig T
if err := json.Unmarshal(val, &defaultConfig); err != nil {
return emptyConfig, err
}
return defaultConfig, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just unexport Get? I feel like we're not going to use it and will probably always be using GetConfig.

MinFee uint64 `json:"minFee"`
}

func TestConfigC(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is this a typo in the test name *ConfigC?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants