-
Notifications
You must be signed in to change notification settings - Fork 118
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
base: main
Are you sure you want to change the base?
Conversation
snow/application.go
Outdated
return &InputCovariantVM[I, O, A]{a.vm.covariantVM} | ||
} | ||
|
||
func (a *Application[I, O, A]) WithAcceptedSub(sub ...event.Subscription[A]) { |
There was a problem hiding this comment.
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
snow/vm.go
Outdated
chainInput ChainInput, | ||
makeChainIndex MakeChainIndexFunc[I, O, A], | ||
app *Application[I, O, A], | ||
) (BlockChainIndex[I], error) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Initialize
, the chain needs to provide the VM with the last accepted input, output, accepted block type, and whether or not it has a "ready" state ie. is the merkledb in a consistent state or are we potentially mid state sync and therefore do not have a valid last output/accepted block because the state is missing.
The VM depends on the ChainIndex
, which provides the current preference, last accepted block, and fetching a block by height/ID.
The VM needs to handle initializing the last accepted block in each of the cases:
- initializing last accepted block to genesis for first time
- restart with a valid last accepted block (same input/output/accepted block)
- restart with the chain index potentially ahead of the last block that was successfully flushed to disk (ie. committed the merkledb)
- if merkledb switches to commit at a different interval such as every 100MB of total data (which could be a meaningful performance improvement and is already planned), handle a restart when the input block index is an arbitrary number of blocks ahead the last merkledb index
- restart mid state sync when we do not have a full merkle state for any given block
The chain index (input type) could be handled by the VM, but each of these different cases can be fairly specific to the implementation. This is the roughest edge. See here for how this is handled in the VM code:
Line 354 in 8f83c7b
// XXX: the VM requires access to the chainIndex returned by makeChainIndex. Passing a function that must be called this |
snow/vm.go
Outdated
) (BlockChainIndex[I], error) | ||
BuildBlock(ctx context.Context, parent O) (I, O, error) | ||
ParseBlock(ctx context.Context, bytes []byte) (I, error) | ||
Execute( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't want to rename this here since I have these changes full integrated with the vm
package on a separate branch, but for now I'll change this in the snow package
"github.com/ava-labs/hypersdk/event" | ||
) | ||
|
||
type Application[I Block, O Block, A Block] struct { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes back to turning VM into one mega object.
Going to modify this for now so that VM
exports only what is needed to fulfill the AvalancheGo VM interface + convience functions for testing like LastAcceptedBlock
instead of LastAccepted
and Application
defines everything that the underlying chain is allowed to consume.
Lmk if this change makes sense, definitely open to putting everything on the VM, but think it makes sense to separate this into everything that must be exported to AvalancheGo and everything that must be exported for the application.
AcceptBlock(ctx context.Context, acceptedParent A, outputBlock O) (A, error) | ||
} | ||
|
||
type VM[I Block, O Block, A Block] struct { |
There was a problem hiding this comment.
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...
snow.VM
is an adapter/wrapper pattern to make the hypervm compatible with avalanchego consensussnow.Application
is meant to be a way to expose theVM
without too many internal implementation details tosnow.Chain
snow.*CovariantVM
exists because we wantChain
to be able to see a concrete type instead of an opaque interfacesnow.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:
snow.VM
+ the related consensus adapter types (e.gStatefulBlock
) I don't think are relevant to the developer and bloat the package. I think we should move these tointernal
tointernal/snow
.*CovariantVM
is awkward because it's purpose is to implement theVM
with type information for a better DX, but has a circular dependency with theVM
, because*CovariantVM
needs a reference to information owned byVM
(example, becauseVM
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 inCovariantVM
and just wrapping it withsnow.VM
to erase type information where needed to conform to avalanchego's interface, so thatsnow.VM
is just a simple wrapper.- 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 theChain
DX.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you expand on this point?
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e instead of having two covariant types to be able to access the input block and output block, maybe we could just have the VM that Chain
is exposed to return a single covariant type like so:
type WrapperVM[I, O Block] struct {}
func (w WrapperVM[I, O]) GetBlock() Block[I, O] { ... }
type Block[I Block, O Block] struct {
Input I
Output O
}
// Chain impl code
func (c *myChainImpl) Foo() {
blk := c.VM.GetBlock()
c.Bar(blk.Input, blk.Output)
}
So basically wrapping the generic types so you don't need to have a covariant vm wrapper for each generic type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great feedback!
- Agree the adapter types are not relevant to the developer. We should expose a way to test the VM. I currently run my integration tests following the engine's expected call behavior. This has caught bugs in initialization and state sync (which is really part of initialization). I think the right testing approach here is unit tests for the simple functional pieces ie.
ParseBlock(ctx, input) -> (expectedOutput, expectedErr)
and integration + e2e tests for the full flow. Are you proposing that we move the entire snow package to internal or just the block types? I think we should initially expose as little as possible and open up the interface as needed over time, so I'd be fine with moving the wholesnow/
packageto
internal/snow/and devs can initially rely only on
vm/defaultvm/` and a harness for integration/e2e testing that we export. - Yup, will move everything to a
VM
type (replacingCovariantVM
) and add an adapter wrapper that will expose thesnowman.Block
types. This is much better. - Will take another pass at this. There are two types exposed to the VM currently the
ChainIndex
, which provides the preferred/last accepted values with the respective types we should have for them ie.Output
andAccepted
. We only need the input covariant VM in the state syncer. Here, it's easier to parameterize the merkle syncer on a block type that includesGetStateRoot()
hypersdk/statesync/merkledb_adapter.go
Line 27 in 8f83c7b
type MerkleSyncer[T MerkleSyncerBlock] struct {
chainstore/chain_store.go
Outdated
} | ||
|
||
func (c *ChainStore[T]) GetLastAcceptedHeight(_ context.Context) (uint64, error) { | ||
lastAcceptedHeightBytes, err := c.db.Get([]byte{lastAcceptedByte}) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
narrowing to just using this for config. This does mean we go back to using AvalancheGo's snow context for creating metrics, passing the log in some places, and add the tracer to ChainInput
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 minimumChain
interface:This PR additionally adds unit tests that the
snow
package correctly maintains the AvalancheGo consensus engine invariants and a fuzz test.