diff --git a/go.mod b/go.mod index c95dd7a260ba..afdd2b2de8fa 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/ava-labs/avalanchego/graft/evm v1.14.2 github.com/ava-labs/avalanchego/graft/subnet-evm v1.14.2 github.com/ava-labs/libevm v1.13.15-0.20260310192938-d71b6cc8513a - github.com/ava-labs/strevm v0.0.0-20260402181257-eca343805711 + github.com/ava-labs/strevm v0.0.0-20260410133722-13fb1cc9bccf github.com/btcsuite/btcd/btcutil v1.1.3 github.com/cespare/xxhash/v2 v2.3.0 github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 diff --git a/go.sum b/go.sum index beba09f6c761..b7f30c81e9ce 100644 --- a/go.sum +++ b/go.sum @@ -339,8 +339,8 @@ github.com/ava-labs/libevm v1.13.15-0.20260310192938-d71b6cc8513a h1:rPtNc8GdAxi github.com/ava-labs/libevm v1.13.15-0.20260310192938-d71b6cc8513a/go.mod h1:6NxGoR1aLABnfLy+fncXRj0W6rUoUrXghnAWZ+Rhr4o= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= -github.com/ava-labs/strevm v0.0.0-20260402181257-eca343805711 h1:ywi93mhfgQ14PoQzGKuUOcu4hXRcWognOQEexH3gaIg= -github.com/ava-labs/strevm v0.0.0-20260402181257-eca343805711/go.mod h1:YaV1d8LZ2K4RM0Rqzx0RWGuNoqUJDhokirwDQCFAe6s= +github.com/ava-labs/strevm v0.0.0-20260410133722-13fb1cc9bccf h1:zL61DvsHc1qzREE0avYMLLiiZzbmTPNoZpRkf2BM5k8= +github.com/ava-labs/strevm v0.0.0-20260410133722-13fb1cc9bccf/go.mod h1:YaV1d8LZ2K4RM0Rqzx0RWGuNoqUJDhokirwDQCFAe6s= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/graft/coreth/plugin/evm/atomic/vm/vm.go b/graft/coreth/plugin/evm/atomic/vm/vm.go index 9aa08bef2d2c..8b2c5a447199 100644 --- a/graft/coreth/plugin/evm/atomic/vm/vm.go +++ b/graft/coreth/plugin/evm/atomic/vm/vm.go @@ -105,6 +105,11 @@ type VM struct { bootstrapped avalancheutils.Atomic[bool] } +// SetPreferenceWithContext implements [transitionvm.Chain]. +func (vm *VM) SetPreferenceWithContext(ctx context.Context, blkID ids.ID, _ *block.Context) error { + return vm.SetPreference(ctx, blkID) +} + func WrapVM(vm extension.InnerVM) *VM { return &VM{InnerVM: vm} } diff --git a/graft/coreth/plugin/evm/wrapped_block.go b/graft/coreth/plugin/evm/wrapped_block.go index 7446b1fe909a..7e07782caee5 100644 --- a/graft/coreth/plugin/evm/wrapped_block.go +++ b/graft/coreth/plugin/evm/wrapped_block.go @@ -346,6 +346,14 @@ func (b *wrappedBlock) semanticVerify(predicateContext *precompileconfig.Predica return err } + headerExtra := customtypes.GetHeaderExtra(header) + if headerExtra.TargetExcess != nil { + return fmt.Errorf("unexpected TargetExcess in header extra: %d", *headerExtra.TargetExcess) + } + if headerExtra.SettledHeight != nil { + return fmt.Errorf("unexpected SettledHeight in header extra: %d", *headerExtra.SettledHeight) + } + // If the VM is not marked as bootstrapped the other chains may also be // bootstrapping and not have populated the required indices. Since // bootstrapping only verifies blocks that have been canonically accepted by @@ -498,13 +506,6 @@ func (b *wrappedBlock) syntacticVerify() error { } } - if headerExtra.TargetExcess != nil { - return fmt.Errorf("unexpected TargetExcess in header extra: %d", *headerExtra.TargetExcess) - } - if headerExtra.SettledHeight != nil { - return fmt.Errorf("unexpected SettledHeight in header extra: %d", *headerExtra.SettledHeight) - } - if b.extension != nil { if err := b.extension.SyntacticVerify(*rulesExtra); err != nil { return err diff --git a/node/node.go b/node/node.go index fd3bd6b4c79e..cfbec6d207ac 100644 --- a/node/node.go +++ b/node/node.go @@ -83,8 +83,10 @@ import ( "github.com/ava-labs/avalanchego/vms/registry" "github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime" "github.com/ava-labs/avalanchego/vms/saevm" + "github.com/ava-labs/avalanchego/vms/transitionvm" databasefactory "github.com/ava-labs/avalanchego/database/factory" + coreth "github.com/ava-labs/avalanchego/graft/coreth/plugin/factory" avmconfig "github.com/ava-labs/avalanchego/vms/avm/config" platformconfig "github.com/ava-labs/avalanchego/vms/platformvm/config" ) @@ -1224,7 +1226,11 @@ func (n *Node) initVMs() error { CreateAssetTxFee: n.Config.CreateAssetTxFee, }, }), - n.VMManager.RegisterFactory(context.TODO(), constants.EVMID, &saevm.Factory{}), + n.VMManager.RegisterFactory(context.TODO(), constants.EVMID, &transitionvm.Factory{ + PreFactory: &coreth.Factory{}, + PostFactory: &saevm.Factory{}, + TransitionTime: time.Date(2026, time.April, 10, 12+5, 34, 0, 0, time.Local), + }), ) if err != nil { return err diff --git a/vms/saevm/hook/block_builder.go b/vms/saevm/hook/block_builder.go index b01d1ad0996e..765097ad8afe 100644 --- a/vms/saevm/hook/block_builder.go +++ b/vms/saevm/hook/block_builder.go @@ -110,9 +110,7 @@ func (b *blockBuilder) BuildHeader(parent *types.Header) (*types.Header, error) ), nil } -func (b *blockBuilder) PotentialEndOfBlockOps(header *types.Header, settledHash common.Hash, source saetypes.BlockSource) iter.Seq[*txpool.Tx] { - ctx := context.TODO() - +func (b *blockBuilder) PotentialEndOfBlockOps(ctx context.Context, header *types.Header, settledHash common.Hash, source saetypes.BlockSource) iter.Seq[*txpool.Tx] { seq := b.potentialTxs() return func(yield func(*txpool.Tx) bool) { // Transactions are verified against the last executed state. We must diff --git a/vms/saevm/state/genesis.go b/vms/saevm/state/genesis.go new file mode 100644 index 000000000000..35d8e268a8c8 --- /dev/null +++ b/vms/saevm/state/genesis.go @@ -0,0 +1,23 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/prefixdb" +) + +var lastSyncKey = prefixdb.MakePrefix([]byte("lastSync")) + +func ReadLastSync(db database.KeyValueReader) ([]byte, error) { + return db.Get(lastSyncKey) +} + +func HasLastSync(db database.KeyValueReader) (bool, error) { + return db.Has(lastSyncKey) +} + +func WriteLastSync(db database.KeyValueWriter, b []byte) error { + return db.Put(lastSyncKey, b) +} diff --git a/vms/saevm/vm.go b/vms/saevm/vm.go index 6a5a69d83120..6f84484d7c04 100644 --- a/vms/saevm/vm.go +++ b/vms/saevm/vm.go @@ -8,20 +8,21 @@ import ( "encoding/json" "errors" "fmt" - "math/big" "net/http" "slices" "sync" "sync/atomic" "time" - "github.com/ava-labs/libevm/core" + "github.com/ava-labs/avalanchego/graft/coreth/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/rlp" "github.com/ava-labs/libevm/triedb" "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/sae" "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/database/prefixdb" @@ -41,6 +42,7 @@ import ( "github.com/ava-labs/avalanchego/vms/saevm/api" "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/saevm/hook/acp176" + "github.com/ava-labs/avalanchego/vms/saevm/state" "github.com/ava-labs/avalanchego/vms/saevm/tx" "github.com/ava-labs/avalanchego/vms/saevm/txpool" @@ -101,55 +103,49 @@ func (vm *SinceGenesis) Initialize( db := rawdb.NewDatabase(database.New(prefixdb.NewNested(ethDBPrefix, avaDB))) tdb := triedb.NewDatabase(db, vm.config.DBConfig.TrieDBConfig) - genesis := new(core.Genesis) - if err := json.Unmarshal(genesisBytes, genesis); err != nil { + snowCtx.Log.Info("parsing genesis") + + genesis, err := parseGenesis(snowCtx, genesisBytes) + if err != nil { return fmt.Errorf("json.Unmarshal(%T): %w", genesis, err) } - { - c := genesis.Config - u := snowCtx.NetworkUpgrades - - c.HomesteadBlock = big.NewInt(0) - c.DAOForkBlock = big.NewInt(0) - c.DAOForkSupport = true - c.EIP150Block = big.NewInt(0) - c.EIP155Block = big.NewInt(0) - c.EIP158Block = big.NewInt(0) - c.ByzantiumBlock = big.NewInt(0) - c.ConstantinopleBlock = big.NewInt(0) - c.PetersburgBlock = big.NewInt(0) - c.IstanbulBlock = big.NewInt(0) - c.MuirGlacierBlock = big.NewInt(0) - c.BerlinBlock = big.NewInt(0) - c.LondonBlock = big.NewInt(0) - c.ShanghaiTime = utils.PointerTo(uint64(u.DurangoTime.Unix())) - c.CancunTime = utils.PointerTo(uint64(u.EtnaTime.Unix())) - - chainConfigExtra := &extras.ChainConfig{ - NetworkUpgrades: extras.GetNetworkUpgrades(u), - AvalancheContext: extras.AvalancheContext{ - SnowCtx: snowCtx, - }, - } - if chainConfigExtra.DurangoBlockTimestamp != nil { - chainConfigExtra.PrecompileUpgrades = append(chainConfigExtra.PrecompileUpgrades, extras.PrecompileUpgrade{ - Config: warpcontract.NewDefaultConfig(chainConfigExtra.DurangoBlockTimestamp), - }) + snowCtx.Log.Info("establishing last synchronous block") + + var lastSync *types.Block + lastSyncBytes, err := state.ReadLastSync(avaDB) + switch { + case err == nil: + lastSync = new(types.Block) + if err := rlp.DecodeBytes(lastSyncBytes, lastSync); err != nil { + return fmt.Errorf("rlp.DecodeBytes(..., %T): %v", lastSync, err) } - corethparams.WithExtra(c, chainConfigExtra) + case errors.Is(err, avadb.ErrNotFound): + lastSync = genesis.ToBlock() + default: + return err } - config, _, err := core.SetupGenesisBlock(db, tdb, genesis) + snowCtx.Log.Info("setting up the genesis", + zap.Stringer("lastID", ids.ID(lastSync.Hash())), + zap.Uint64("lastHeight", lastSync.NumberU64()), + ) + + // TODO: Are these reasonable? + config, _, err := core.SetupGenesisBlock(db, tdb, genesis, lastSync.Hash(), false) if err != nil { return fmt.Errorf("core.SetupGenesisBlock(...): %w", err) } + snowCtx.Log.Info("parsing user config") + userConfig, err := ParseConfig(configBytes) if err != nil { return err } + snowCtx.Log.Info("parsing warp message overrides") + warpMessages, err := userConfig.WarpMessages() if err != nil { return err @@ -177,7 +173,10 @@ func (vm *SinceGenesis) Initialize( txs, warpStorage, ) - inner, err := sae.NewVM(ctx, hooks, vm.config, snowCtx, config, db, genesis.ToBlock(), appSender) + + snowCtx.Log.Info("constructing the sae VM") + + inner, err := sae.NewVM(ctx, hooks, vm.config, snowCtx, config, db, lastSync, appSender) if err != nil { return err } @@ -187,11 +186,15 @@ func (vm *SinceGenesis) Initialize( vm.mempool = txpool.New(txs, snowCtx, inner.GethRPCBackends()) vm.onClose = append(vm.onClose, vm.mempool.Close) + snowCtx.Log.Info("registering coreth metrics") + metrics := prometheus.NewRegistry() if err := snowCtx.Metrics.Register("coreth", metrics); err != nil { return fmt.Errorf("failed to register metrics: %w", err) } + snowCtx.Log.Info("p2p gossip") + { // ========== P2P Gossip ========== gossipSet, err := gossip.NewBloomSet(vm.mempool, gossip.BloomSetConfig{}) if err != nil { @@ -239,6 +242,8 @@ func (vm *SinceGenesis) Initialize( }) } + snowCtx.Log.Info("warp handlers") + { // ========== Warp Handler ========== vm.warpVerifier = saewarp.NewVerifier(&blockClient{vm: inner}, warpStorage) warpHandler := acp118.NewCachedHandler( @@ -251,9 +256,42 @@ func (vm *SinceGenesis) Initialize( } } + snowCtx.Log.Info("initialized saevm") + return nil } +// TODO: copied from coreth +func parseGenesis(ctx *snow.Context, bytes []byte) (*core.Genesis, error) { + g := new(core.Genesis) + if err := json.Unmarshal(bytes, g); err != nil { + return nil, fmt.Errorf("parsing genesis: %w", err) + } + + // Populate the Avalanche config extras. + configExtra := corethparams.GetExtra(g.Config) + configExtra.AvalancheContext = extras.AvalancheContext{ + SnowCtx: ctx, + } + configExtra.NetworkUpgrades = extras.GetNetworkUpgrades(ctx.NetworkUpgrades) + + // If Durango is scheduled, schedule the Warp Precompile at the same time. + if configExtra.DurangoBlockTimestamp != nil { + configExtra.PrecompileUpgrades = append(configExtra.PrecompileUpgrades, extras.PrecompileUpgrade{ + Config: warpcontract.NewDefaultConfig(configExtra.DurangoBlockTimestamp), + }) + } + if err := configExtra.Verify(); err != nil { + return nil, fmt.Errorf("invalid chain config: %w", err) + } + + // Align all the Ethereum upgrades to the Avalanche upgrades + if err := corethparams.SetEthUpgrades(g.Config); err != nil { + return nil, fmt.Errorf("setting eth upgrades: %w", err) + } + return g, nil +} + const ( avaxServiceName = "avax" avaxHTTPExtensionPath = "/" + avaxServiceName diff --git a/vms/transitionvm/factory.go b/vms/transitionvm/factory.go new file mode 100644 index 000000000000..e78490e4b5cf --- /dev/null +++ b/vms/transitionvm/factory.go @@ -0,0 +1,53 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms" +) + +var _ vms.Factory = (*Factory)(nil) + +type Factory struct { + PreFactory vms.Factory + PostFactory vms.Factory + TransitionTime time.Time +} + +var errInvalidVMType = errors.New("invalid VM type") + +func (f *Factory) New(log logging.Logger) (interface{}, error) { + preIntf, err := f.PreFactory.New(log) + if err != nil { + return nil, err + } + pre, ok := preIntf.(Chain) + if !ok { + return nil, fmt.Errorf("pre: %w: %T", errInvalidVMType, preIntf) + } + + postIntf, err := f.PostFactory.New(log) + if err != nil { + return nil, err + } + post, ok := postIntf.(Chain) + if !ok { + return nil, fmt.Errorf("post: %w: %T", errInvalidVMType, postIntf) + } + + return &VM{ + preTransitionChain: pre, + postTransitionChain: post, + transitionTime: f.TransitionTime, + + current: ¤t{ + chain: pre, + }, + }, nil +} diff --git a/vms/transitionvm/requests.go b/vms/transitionvm/requests.go new file mode 100644 index 000000000000..0127fbb330c3 --- /dev/null +++ b/vms/transitionvm/requests.go @@ -0,0 +1,55 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/set" +) + +var _ common.AppSender = &sender{} + +type request struct { + nodeID ids.NodeID + requestID uint32 +} + +type requests struct { + lock sync.Mutex + set set.Set[request] +} + +func (r *requests) add(nodeIDs set.Set[ids.NodeID], requestID uint32) { + r.lock.Lock() + defer r.lock.Unlock() + + for nodeID := range nodeIDs { + r.set.Add(request{nodeID, requestID}) + } +} + +func (r *requests) remove(nodeID ids.NodeID, requestID uint32) bool { + r.lock.Lock() + defer r.lock.Unlock() + + req := request{nodeID, requestID} + had := r.set.Contains(req) + r.set.Remove(req) + return had +} + +type sender struct { + common.AppSender + + requests *requests +} + +func (s *sender) SendAppRequest(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, appRequestBytes []byte) error { + s.requests.add(nodeIDs, requestID) + return s.AppSender.SendAppRequest(ctx, nodeIDs, requestID, appRequestBytes) +} diff --git a/vms/transitionvm/vm.go b/vms/transitionvm/vm.go new file mode 100644 index 000000000000..4d1c32b51bee --- /dev/null +++ b/vms/transitionvm/vm.go @@ -0,0 +1,221 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ava-labs/avalanchego/api/metrics" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/version" + "github.com/ava-labs/avalanchego/vms/saevm/state" + "go.uber.org/zap" +) + +var _ Chain = &VM{} + +type Chain interface { + block.ChainVM + block.BuildBlockWithContextChainVM + block.SetPreferenceWithContextChainVM +} + +type VM struct { + // transition parameters + preTransitionChain Chain + postTransitionChain Chain + transitionTime time.Time + + // chain parameters + chainCtx *snow.Context // Has modified Lock and Metrics fields + db database.Database + genesisBytes []byte + upgradeBytes []byte + configBytes []byte + fxs []*common.Fx + appSender common.AppSender + + // current state + transitionLock sync.RWMutex + transitioned bool + current *current +} + +type current struct { + chain Chain + consensusState snow.State + requests *requests + connections *connections + httpHandlers *httpHandlers + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (v *VM) Initialize( + ctx context.Context, + chainCtx *snow.Context, + db database.Database, + genesisBytes []byte, + upgradeBytes []byte, + configBytes []byte, + fxs []*common.Fx, + appSender common.AppSender, +) error { + gatherer := metrics.NewPrefixGatherer() + if err := chainCtx.Metrics.Register("transition", gatherer); err != nil { + return err + } + + v.chainCtx = &snow.Context{ + NetworkID: chainCtx.NetworkID, + SubnetID: chainCtx.SubnetID, + ChainID: chainCtx.ChainID, + NodeID: chainCtx.NodeID, + PublicKey: chainCtx.PublicKey, + NetworkUpgrades: chainCtx.NetworkUpgrades, + XChainID: chainCtx.XChainID, + CChainID: chainCtx.CChainID, + AVAXAssetID: chainCtx.AVAXAssetID, + Log: chainCtx.Log, + Lock: sync.RWMutex{}, + SharedMemory: chainCtx.SharedMemory, + BCLookup: chainCtx.BCLookup, + Metrics: gatherer, + WarpSigner: chainCtx.WarpSigner, + ValidatorState: chainCtx.ValidatorState, + ChainDataDir: chainCtx.ChainDataDir, + } + v.db = db + v.genesisBytes = genesisBytes + v.upgradeBytes = upgradeBytes + v.configBytes = configBytes + v.fxs = fxs + v.appSender = appSender + + v.current = ¤t{ + chain: v.preTransitionChain, + consensusState: snow.Initializing, + connections: &connections{ + nodes: make(map[ids.NodeID]*version.Application), + }, + httpHandlers: &httpHandlers{ + routes: make(map[string]*httpHandler), + }, + } + + chainCtx.Log.Info("checking for last synchronous block") + has, err := state.HasLastSync(v.db) + if err != nil { + return fmt.Errorf("checking for last synchronous block: %w", err) + } + + if has { + chainCtx.Log.Info("initializing post-transition VM") + if err := v.initChain(ctx, v.postTransitionChain, v.chainCtx); err != nil { + return fmt.Errorf("initializing post-transition VM: %w", err) + } + v.transitioned = true + } else { + chainCtx.Log.Info("initializing pre-transition VM") + if err := v.initChain(ctx, v.preTransitionChain, chainCtx); err != nil { + return fmt.Errorf("initializing pre-transition VM: %w", err) + } + } + return nil +} + +func (v *VM) transition(ctx context.Context, last snowman.Block) error { + // We must cancel the context before grabbing the lock to ensure that + // [VM.WaitForEvent] does not block indefinitely. + v.current.ctxCancel() + + v.transitionLock.Lock() + defer v.transitionLock.Unlock() + + lastID := last.ID() + lastBytes := last.Bytes() + v.chainCtx.Log.Info("transitioning VMs", + zap.Stringer("lastID", lastID), + zap.Uint64("lastHeight", last.Height()), + zap.Time("lastTime", last.Timestamp()), + ) + + v.chainCtx.Log.Info("shutting down pre-transition VM") + if err := v.preTransitionChain.Shutdown(ctx); err != nil { + return fmt.Errorf("closing pre-transition chain: %w", err) + } + + v.chainCtx.Log.Info("writing last synchronous block") + if err := state.WriteLastSync(v.db, lastBytes); err != nil { + return fmt.Errorf("saving last synchronous block: %w", err) + } + + v.chainCtx.Log.Info("initializing post-transition VM") + if err := v.initChain(ctx, v.postTransitionChain, v.chainCtx); err != nil { + return fmt.Errorf("initializing post-transition VM: %w", err) + } + + v.chainCtx.Log.Info("initializing post-transition VM preference", + zap.Stringer("blkID", lastID), + ) + if err := v.postTransitionChain.SetPreference(ctx, lastID); err != nil { + return fmt.Errorf("setting post-transition preference: %w", err) + } + + v.transitioned = true + v.chainCtx.Log.Info("transition finished successfully") + return nil +} + +func (v *VM) initChain(ctx context.Context, chain Chain, chainCtx *snow.Context) error { + var ( + requests requests + sender = sender{ + AppSender: v.appSender, + requests: &requests, + } + ) + err := chain.Initialize( + ctx, + chainCtx, + v.db, + v.genesisBytes, + v.upgradeBytes, + v.configBytes, + v.fxs, + &sender, + ) + if err != nil { + return fmt.Errorf("initializing chain: %w", err) + } + + if v.current.consensusState != snow.Initializing { + if err := chain.SetState(ctx, v.current.consensusState); err != nil { + return fmt.Errorf("setting consensus state: %w", err) + } + } + if err := v.current.connections.reconnect(ctx, chain); err != nil { + return fmt.Errorf("reconnecting to vm: %w", err) + } + + newHandlers, err := chain.CreateHandlers(ctx) + if err != nil { + return fmt.Errorf("creating http handlers", err) + } + v.current.httpHandlers.set(newHandlers) + + v.current.chain = chain + v.current.requests = &requests + v.current.ctx, v.current.ctxCancel = context.WithCancel(context.Background()) + return nil +} diff --git a/vms/transitionvm/vm_block.go b/vms/transitionvm/vm_block.go new file mode 100644 index 000000000000..0c4b4731043d --- /dev/null +++ b/vms/transitionvm/vm_block.go @@ -0,0 +1,116 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +var _ snowman.Block = &preBlock{} + +type preBlock struct { + snowman.Block + v *VM +} + +var errPreTransitionBlockAfterTransition = errors.New("pre-transition block after transition") + +func (p *preBlock) Verify(ctx context.Context) error { + p.v.transitionLock.RLock() + defer p.v.transitionLock.RUnlock() + + parent, err := p.v.current.chain.GetBlock(ctx, p.Parent()) + if err != nil { + return err + } + + // Make sure the parent block is a pre-transition block. + if parentTime := parent.Timestamp(); !parentTime.Before(p.v.transitionTime) { + return errPreTransitionBlockAfterTransition + } + return p.Block.Verify(ctx) +} + +// Accept is basically the only function that does not immediately prevent +// transitions. This is because Accept is the function that actually triggers +// the transition. +func (p *preBlock) Accept(ctx context.Context) error { + if err := p.Block.Accept(ctx); err != nil { + return err + } + if time := p.Timestamp(); time.Before(p.v.transitionTime) { + return nil + } + return p.v.transition(ctx, p.Block) +} + +func (p *preBlock) Reject(ctx context.Context) error { + p.v.transitionLock.RLock() + defer p.v.transitionLock.RUnlock() + + // If the VM transitioned with some blocks still in consensus, they are + // guranteed to all be rejected. However, we don't want to run into a + // situation where we call into the now-closed pre-transition VM. So we just + // do nothing for this block. + if p.v.transitioned { + return nil + } + return p.Block.Reject(ctx) +} + +func (v *VM) BuildBlock(ctx context.Context) (snowman.Block, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + b, err := v.current.chain.BuildBlock(ctx) + if err != nil { + return nil, err + } + return v.wrapBlock(b), nil +} + +func (v *VM) BuildBlockWithContext(ctx context.Context, blockCtx *block.Context) (snowman.Block, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + b, err := v.current.chain.BuildBlockWithContext(ctx, blockCtx) + if err != nil { + return nil, err + } + return v.wrapBlock(b), nil +} + +func (v *VM) ParseBlock(ctx context.Context, blockBytes []byte) (snowman.Block, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + b, err := v.current.chain.ParseBlock(ctx, blockBytes) + if err != nil { + return nil, err + } + return v.wrapBlock(b), nil +} + +func (v *VM) GetBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + b, err := v.current.chain.GetBlock(ctx, blkID) + if err != nil { + return nil, err + } + return v.wrapBlock(b), nil +} + +func (v *VM) wrapBlock(b snowman.Block) snowman.Block { + if v.transitioned { + return b + } + return &preBlock{b, v} +} diff --git a/vms/transitionvm/vm_consensus.go b/vms/transitionvm/vm_consensus.go new file mode 100644 index 000000000000..8c5a57e60c61 --- /dev/null +++ b/vms/transitionvm/vm_consensus.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/common" +) + +func (v *VM) SetState(ctx context.Context, state snow.State) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + v.current.consensusState = state + return v.current.chain.SetState(ctx, state) +} + +func (v *VM) WaitForEvent(ctx context.Context) (common.Message, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // If the VM is being transitioned, we want to stop waiting on an event + // notification. + removeCancel := context.AfterFunc(v.current.ctx, cancel) + + // If the VM isn't transitioned, we must remove the cancellation of the + // context to avoid a memory leak. + defer removeCancel() + + return v.current.chain.WaitForEvent(ctx) +} diff --git a/vms/transitionvm/vm_current.go b/vms/transitionvm/vm_current.go new file mode 100644 index 000000000000..e3e193975bf2 --- /dev/null +++ b/vms/transitionvm/vm_current.go @@ -0,0 +1,75 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +func (v *VM) AppGossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.AppGossip(ctx, nodeID, msg) +} + +func (v *VM) AppRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, request []byte) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.AppRequest(ctx, nodeID, requestID, deadline, request) +} + +func (v *VM) HealthCheck(ctx context.Context) (interface{}, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.HealthCheck(ctx) +} + +func (v *VM) LastAccepted(ctx context.Context) (ids.ID, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.LastAccepted(ctx) +} + +func (v *VM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.GetBlockIDAtHeight(ctx, height) +} + +func (v *VM) SetPreference(ctx context.Context, blkID ids.ID) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.SetPreference(ctx, blkID) +} + +func (v *VM) SetPreferenceWithContext(ctx context.Context, blkID ids.ID, blockCtx *block.Context) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.SetPreferenceWithContext(ctx, blkID, blockCtx) +} + +func (v *VM) Version(ctx context.Context) (string, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.Version(ctx) +} + +func (v *VM) Shutdown(ctx context.Context) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + return v.current.chain.Shutdown(ctx) +} diff --git a/vms/transitionvm/vm_http.go b/vms/transitionvm/vm_http.go new file mode 100644 index 000000000000..574315698aba --- /dev/null +++ b/vms/transitionvm/vm_http.go @@ -0,0 +1,99 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "net/http" + "sync" +) + +// httpHandler that wraps an existing [http.Handler]. If the existing +// [http.Handler] is nil, this handler returns with [http.StatusNotFound]. +// Otherwise, it delegates calls to the inner handler. +type httpHandler struct { + lock sync.RWMutex + handler http.Handler +} + +func (h *httpHandler) set(handler http.Handler) { + h.lock.Lock() + defer h.lock.Unlock() + + h.handler = handler +} + +func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.lock.RLock() + handler := h.handler + h.lock.RUnlock() + + if handler == nil { + http.NotFound(w, r) + return + } + + handler.ServeHTTP(w, r) +} + +// httpHandlers manages an append-only collection of updateable routes. +type httpHandlers struct { + lock sync.RWMutex + routes map[string]*httpHandler +} + +// set adds all the new handlers to the collection of tracked routes. Any +// tracked routes not included in newHandlers will still be tracked, but will +// return 404s. +func (h *httpHandlers) set(newHandlers map[string]http.Handler) { + h.lock.Lock() + defer h.lock.Unlock() + + for path, newHandler := range newHandlers { + handler := &httpHandler{} + if oldHandler, ok := h.routes[path]; ok { + handler = oldHandler + } + handler.set(newHandler) + h.routes[path] = handler + } + for path, oldHandler := range h.routes { + if _, ok := newHandlers[path]; ok { + continue + } + oldHandler.set(nil) + } +} + +// asInterface returns the current collection of tracked handlers as their +// interface type. +func (h *httpHandlers) asInterface() map[string]http.Handler { + h.lock.RLock() + defer h.lock.RUnlock() + + handlers := make(map[string]http.Handler, len(h.routes)) + for path, handler := range h.routes { + handlers[path] = handler + } + return handlers +} + +func (v *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + newHandlers, err := v.current.chain.CreateHandlers(ctx) + if err != nil { + return nil, err + } + + v.current.httpHandlers.set(newHandlers) + return v.current.httpHandlers.asInterface(), nil +} + +// None of Subnet-EVM, Coreth, or SAEVM implement NewHTTPHandler, so it is left +// unimplemented. +func (v *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) { + return nil, nil +} diff --git a/vms/transitionvm/vm_network.go b/vms/transitionvm/vm_network.go new file mode 100644 index 000000000000..355359c29341 --- /dev/null +++ b/vms/transitionvm/vm_network.go @@ -0,0 +1,81 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package transitionvm + +import ( + "context" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/version" +) + +type connections struct { + lock sync.RWMutex + nodes map[ids.NodeID]*version.Application +} + +func (c *connections) add(nodeID ids.NodeID, v *version.Application) { + c.lock.Lock() + defer c.lock.Unlock() + + c.nodes[nodeID] = v +} + +func (c *connections) remove(nodeID ids.NodeID) { + c.lock.Lock() + defer c.lock.Unlock() + + delete(c.nodes, nodeID) +} + +func (c *connections) reconnect(ctx context.Context, connector validators.Connector) error { + c.lock.RLock() + defer c.lock.RUnlock() + + for nodeID, v := range c.nodes { + if err := connector.Connected(ctx, nodeID, v); err != nil { + return err + } + } + return nil +} + +func (v *VM) Connected(ctx context.Context, nodeID ids.NodeID, version *version.Application) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + v.current.connections.add(nodeID, version) + return v.current.chain.Connected(ctx, nodeID, version) +} + +func (v *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + v.current.connections.remove(nodeID) + return v.current.chain.Disconnected(ctx, nodeID) +} + +func (v *VM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + if !v.current.requests.remove(nodeID, requestID) { + return nil + } + return v.current.chain.AppRequestFailed(ctx, nodeID, requestID, appErr) +} + +func (v *VM) AppResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error { + v.transitionLock.RLock() + defer v.transitionLock.RUnlock() + + if !v.current.requests.remove(nodeID, requestID) { + return nil + } + return v.current.chain.AppResponse(ctx, nodeID, requestID, response) +}