From 734bcca778852cf72a18531632adf9471cf380e8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 20 Jun 2025 16:02:56 -0400 Subject: [PATCH 01/45] wip --- go.mod | 2 + plugin/evm/vm_atomic.go | 140 +++++++++++++++++++++++++++++++++ plugin/evm/vm_sae.go | 168 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 plugin/evm/vm_atomic.go create mode 100644 plugin/evm/vm_sae.go diff --git a/go.mod b/go.mod index 5eef7eb56a..527dfd879b 100644 --- a/go.mod +++ b/go.mod @@ -138,3 +138,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm diff --git a/plugin/evm/vm_atomic.go b/plugin/evm/vm_atomic.go new file mode 100644 index 0000000000..6fb1e9539c --- /dev/null +++ b/plugin/evm/vm_atomic.go @@ -0,0 +1,140 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "net/http" + "sync" + "time" + + "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" +) + +var _ vmInterface = (*AtomicVM)(nil) + +type AtomicVM struct { + lock sync.RWMutex + value vmInterface +} + +func (a *AtomicVM) Get() vmInterface { + a.lock.RLock() + defer a.lock.RUnlock() + + return a.value +} + +func (a *AtomicVM) Set(value vmInterface) { + a.lock.Lock() + defer a.lock.Unlock() + + a.value = value +} + +func (a *AtomicVM) AppGossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error { + return a.Get().AppGossip(ctx, nodeID, msg) +} + +func (a *AtomicVM) AppRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, request []byte) error { + return a.Get().AppRequest(ctx, nodeID, requestID, deadline, request) +} + +func (a *AtomicVM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { + return a.Get().AppRequestFailed(ctx, nodeID, requestID, appErr) +} + +func (a *AtomicVM) AppResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error { + return a.Get().AppResponse(ctx, nodeID, requestID, response) +} + +func (a *AtomicVM) BuildBlock(ctx context.Context) (snowman.Block, error) { + return a.Get().BuildBlock(ctx) +} + +func (a *AtomicVM) BuildBlockWithContext(ctx context.Context, blockCtx *block.Context) (snowman.Block, error) { + return a.Get().BuildBlockWithContext(ctx, blockCtx) +} + +func (a *AtomicVM) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error { + return a.Get().Connected(ctx, nodeID, nodeVersion) +} + +func (a *AtomicVM) CreateHTTP2Handler(ctx context.Context) (http.Handler, error) { + return a.Get().CreateHTTP2Handler(ctx) +} + +func (a *AtomicVM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) { + return a.Get().CreateHandlers(ctx) +} + +func (a *AtomicVM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { + return a.Get().Disconnected(ctx, nodeID) +} + +func (a *AtomicVM) GetBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { + return a.Get().GetBlock(ctx, blkID) +} + +func (a *AtomicVM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) { + return a.Get().GetBlockIDAtHeight(ctx, height) +} + +func (a *AtomicVM) GetLastStateSummary(ctx context.Context) (block.StateSummary, error) { + return a.Get().GetLastStateSummary(ctx) +} + +func (a *AtomicVM) GetOngoingSyncStateSummary(ctx context.Context) (block.StateSummary, error) { + return a.Get().GetOngoingSyncStateSummary(ctx) +} + +func (a *AtomicVM) GetStateSummary(ctx context.Context, summaryHeight uint64) (block.StateSummary, error) { + return a.Get().GetStateSummary(ctx, summaryHeight) +} + +func (a *AtomicVM) HealthCheck(ctx context.Context) (interface{}, error) { + return a.Get().HealthCheck(ctx) +} + +func (a *AtomicVM) Initialize(ctx context.Context, chainCtx *snow.Context, db database.Database, genesisBytes []byte, upgradeBytes []byte, configBytes []byte, toEngine chan<- common.Message, fxs []*common.Fx, appSender common.AppSender) error { + return a.Get().Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, toEngine, fxs, appSender) +} + +func (a *AtomicVM) LastAccepted(ctx context.Context) (ids.ID, error) { + return a.Get().LastAccepted(ctx) +} + +func (a *AtomicVM) ParseBlock(ctx context.Context, blockBytes []byte) (snowman.Block, error) { + return a.Get().ParseBlock(ctx, blockBytes) +} + +func (a *AtomicVM) ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error) { + return a.Get().ParseStateSummary(ctx, summaryBytes) +} + +func (a *AtomicVM) SetPreference(ctx context.Context, blkID ids.ID) error { + return a.Get().SetPreference(ctx, blkID) +} + +func (a *AtomicVM) SetState(ctx context.Context, state snow.State) error { + return a.Get().SetState(ctx, state) +} + +func (a *AtomicVM) Shutdown(ctx context.Context) error { + return a.Get().Shutdown(ctx) +} + +func (a *AtomicVM) StateSyncEnabled(ctx context.Context) (bool, error) { + return a.Get().StateSyncEnabled(ctx) +} + +func (a *AtomicVM) Version(ctx context.Context) (string, error) { + return a.Get().Version(ctx) +} diff --git a/plugin/evm/vm_sae.go b/plugin/evm/vm_sae.go new file mode 100644 index 0000000000..cf51c12733 --- /dev/null +++ b/plugin/evm/vm_sae.go @@ -0,0 +1,168 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/prefixdb" + "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/utils/set" + "github.com/ava-labs/avalanchego/vms/avm/blocks" + "github.com/ava-labs/strevm/adaptor" + + corethdatabase "github.com/ava-labs/coreth/plugin/evm/database" + sae "github.com/ava-labs/strevm" +) + +type vmInterface interface { + block.ChainVM + block.BuildBlockWithContextChainVM + block.WithVerifyContext + block.StateSyncableVM +} + +var _ vmInterface = (*TransitionVM)(nil) + +type TransitionVM struct { + AtomicVM // current vm backend + outstandingAppRequests set.Set[ids.RequestID] // protected by atomicVM lock + + chainCtx *snow.Context + db database.Database + genesisBytes []byte + upgradeBytes []byte + configBytes []byte + toEngine chan<- common.Message + fxs []*common.Fx + appSender common.AppSender + + preFork *VM + + saeVM *sae.VM + postFork vmInterface +} + +type saeWrapper struct { + *sae.VM +} + +func (*saeWrapper) Initialize(context.Context, *snow.Context, database.Database, []byte, []byte, []byte, chan<- common.Message, []*common.Fx, common.AppSender) error { + return errors.New("unexpected call to saeWrapper.Initialize") +} + +func (t *TransitionVM) Initialize( + ctx context.Context, + chainCtx *snow.Context, + db database.Database, + genesisBytes []byte, + upgradeBytes []byte, + configBytes []byte, + toEngine chan<- common.Message, + fxs []*common.Fx, + appSender common.AppSender, +) error { + if err := t.preFork.Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, toEngine, fxs, appSender); err != nil { + return fmt.Errorf("initializing preFork VM: %w", err) + } + t.Set(t.preFork) + + t.chainCtx = chainCtx + t.db = db + t.genesisBytes = genesisBytes + t.upgradeBytes = upgradeBytes + t.configBytes = configBytes + t.toEngine = toEngine + t.fxs = fxs + t.appSender = appSender + + lastAcceptedID, err := t.preFork.LastAccepted(ctx) + if err != nil { + return fmt.Errorf("getting preFork last accepted ID: %w", err) + } + + lastAccepted, err := t.preFork.GetBlock(ctx, lastAcceptedID) + if err != nil { + return fmt.Errorf("getting preFork last accepted %q: %w", lastAcceptedID, err) + } + + if err := t.afterAccept(ctx, lastAccepted); err != nil { + return fmt.Errorf("running post accept hook on %q: %w", lastAcceptedID, err) + } + return nil +} + +func (t *TransitionVM) afterAccept(ctx context.Context, block snowman.Block) error { + lastAcceptedTimestamp := block.Timestamp() + if !t.chainCtx.NetworkUpgrades.IsGraniteActivated(lastAcceptedTimestamp) { + return nil + } + + if err := t.preFork.Shutdown(ctx); err != nil { + return fmt.Errorf("shutting down preFork VM: %w", err) + } + + db := corethdatabase.WrapDatabase(prefixdb.NewNested(ethDBPrefix, t.db)) + _ = db + vm, err := sae.New( + ctx, + sae.Config{}, + ) + if err != nil { + return fmt.Errorf("initializing postFork VM: %w", err) + } + + t.postFork = adaptor.Convert[*blocks.Block](&saeWrapper{ + VM: vm, + }) + + t.postFork = vm + // t.Set(t.postFork) + return nil +} + +func (t *TransitionVM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { + request := ids.RequestID{ + NodeID: nodeID, + RequestID: requestID, + } + + t.AtomicVM.lock.Lock() + shouldHandle := t.outstandingAppRequests.Contains(request) + t.outstandingAppRequests.Remove(request) + + vm := t.AtomicVM.value + t.AtomicVM.lock.Unlock() + + if !shouldHandle { + return nil + } + return vm.AppRequestFailed(ctx, nodeID, requestID, appErr) +} + +func (t *TransitionVM) AppResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error { + request := ids.RequestID{ + NodeID: nodeID, + RequestID: requestID, + } + + t.AtomicVM.lock.Lock() + shouldHandle := t.outstandingAppRequests.Contains(request) + t.outstandingAppRequests.Remove(request) + + vm := t.AtomicVM.value + t.AtomicVM.lock.Unlock() + + if !shouldHandle { + return nil + } + return vm.AppResponse(ctx, nodeID, requestID, response) +} From 8f3d4186d6d50764f50016600291a7304868b46e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 17:51:45 -0400 Subject: [PATCH 02/45] hackz --- go.mod | 12 +++++++++--- go.sum | 33 ++++++++++++++++++++++++--------- plugin/evm/vm_atomic.go | 30 +++++++++++++++--------------- plugin/evm/vm_sae.go | 7 ++----- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 527dfd879b..349ab3cf74 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.23.9 require ( github.com/VictoriaMetrics/fastcache v1.12.1 - github.com/ava-labs/avalanchego v1.13.2-0.20250610153956-049b5859a039 - github.com/ava-labs/libevm v0.0.0-20250610142802-2672fbd7cdfc + github.com/ava-labs/avalanchego v1.13.2-rc.1 + github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 + github.com/ava-labs/strevm v0.0.0-00010101000000-000000000000 github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set/v2 v2.1.0 github.com/fjl/gencodec v0.1.1 @@ -41,7 +42,8 @@ require ( require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/StephenButtolph/canoto v0.15.0 // indirect + github.com/StephenButtolph/canoto v0.17.1 // indirect + github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect @@ -61,7 +63,9 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect @@ -82,6 +86,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huin/goupnp v1.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -105,6 +110,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect diff --git a/go.sum b/go.sum index 1d5589a464..e9cd27178e 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= @@ -49,8 +49,8 @@ github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKz github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/StephenButtolph/canoto v0.15.0 h1:3iGdyTSQZ7/y09WaJCe0O/HIi53ZyTrnmVzfCqt64mM= -github.com/StephenButtolph/canoto v0.15.0/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= +github.com/StephenButtolph/canoto v0.17.1 h1:WnN5czIHHALq7pwc+Z2F1sCsKJCDhxlq0zL0YK1etHc= +github.com/StephenButtolph/canoto v0.17.1/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -58,10 +58,12 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/avalanchego v1.13.2-0.20250610153956-049b5859a039 h1:Wc4uMMiPRtnQOgrdEBbNFOXV5rXr6xtPA7GbNYgCqgM= -github.com/ava-labs/avalanchego v1.13.2-0.20250610153956-049b5859a039/go.mod h1:7BHKKN46mOeRNXpt5idSEJmTz903vDZFa1PXONNmSc4= -github.com/ava-labs/libevm v0.0.0-20250610142802-2672fbd7cdfc h1:cSXaUY4hdmoJ2FJOgOzn+WiovN/ZB/zkNRgnZhE50OA= -github.com/ava-labs/libevm v0.0.0-20250610142802-2672fbd7cdfc/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= +github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= +github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= +github.com/ava-labs/avalanchego v1.13.2-rc.1 h1:TaUB0g8L1uRILvJFdOKwjo7h04rGM/u+MZEvHmh/Y6E= +github.com/ava-labs/avalanchego v1.13.2-rc.1/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 h1:vBMYo+Iazw0rGTr+cwjkBdh5eadLPlv4ywI4lKye3CA= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= 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= @@ -80,8 +82,9 @@ github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUB github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -161,6 +164,7 @@ github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5R github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -175,6 +179,8 @@ github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +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/fjl/gencodec v0.1.1 h1:DhQY29Q6JLXB/GgMqE86NbOEuvckiYcJCbXFu02toms= github.com/fjl/gencodec v0.1.1/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -226,7 +232,10 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -340,6 +349,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= @@ -482,6 +493,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -502,6 +515,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +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/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= diff --git a/plugin/evm/vm_atomic.go b/plugin/evm/vm_atomic.go index 6fb1e9539c..2cdc05628c 100644 --- a/plugin/evm/vm_atomic.go +++ b/plugin/evm/vm_atomic.go @@ -87,17 +87,17 @@ func (a *AtomicVM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.I return a.Get().GetBlockIDAtHeight(ctx, height) } -func (a *AtomicVM) GetLastStateSummary(ctx context.Context) (block.StateSummary, error) { - return a.Get().GetLastStateSummary(ctx) -} +// func (a *AtomicVM) GetLastStateSummary(ctx context.Context) (block.StateSummary, error) { +// return a.Get().GetLastStateSummary(ctx) +// } -func (a *AtomicVM) GetOngoingSyncStateSummary(ctx context.Context) (block.StateSummary, error) { - return a.Get().GetOngoingSyncStateSummary(ctx) -} +// func (a *AtomicVM) GetOngoingSyncStateSummary(ctx context.Context) (block.StateSummary, error) { +// return a.Get().GetOngoingSyncStateSummary(ctx) +// } -func (a *AtomicVM) GetStateSummary(ctx context.Context, summaryHeight uint64) (block.StateSummary, error) { - return a.Get().GetStateSummary(ctx, summaryHeight) -} +// func (a *AtomicVM) GetStateSummary(ctx context.Context, summaryHeight uint64) (block.StateSummary, error) { +// return a.Get().GetStateSummary(ctx, summaryHeight) +// } func (a *AtomicVM) HealthCheck(ctx context.Context) (interface{}, error) { return a.Get().HealthCheck(ctx) @@ -115,9 +115,9 @@ func (a *AtomicVM) ParseBlock(ctx context.Context, blockBytes []byte) (snowman.B return a.Get().ParseBlock(ctx, blockBytes) } -func (a *AtomicVM) ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error) { - return a.Get().ParseStateSummary(ctx, summaryBytes) -} +// func (a *AtomicVM) ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error) { +// return a.Get().ParseStateSummary(ctx, summaryBytes) +// } func (a *AtomicVM) SetPreference(ctx context.Context, blkID ids.ID) error { return a.Get().SetPreference(ctx, blkID) @@ -131,9 +131,9 @@ func (a *AtomicVM) Shutdown(ctx context.Context) error { return a.Get().Shutdown(ctx) } -func (a *AtomicVM) StateSyncEnabled(ctx context.Context) (bool, error) { - return a.Get().StateSyncEnabled(ctx) -} +// func (a *AtomicVM) StateSyncEnabled(ctx context.Context) (bool, error) { +// return a.Get().StateSyncEnabled(ctx) +// } func (a *AtomicVM) Version(ctx context.Context) (string, error) { return a.Get().Version(ctx) diff --git a/plugin/evm/vm_sae.go b/plugin/evm/vm_sae.go index cf51c12733..0c8f655528 100644 --- a/plugin/evm/vm_sae.go +++ b/plugin/evm/vm_sae.go @@ -16,8 +16,8 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/vms/avm/blocks" "github.com/ava-labs/strevm/adaptor" + "github.com/ava-labs/strevm/blocks" corethdatabase "github.com/ava-labs/coreth/plugin/evm/database" sae "github.com/ava-labs/strevm" @@ -26,8 +26,7 @@ import ( type vmInterface interface { block.ChainVM block.BuildBlockWithContextChainVM - block.WithVerifyContext - block.StateSyncableVM + // block.StateSyncableVM } var _ vmInterface = (*TransitionVM)(nil) @@ -123,8 +122,6 @@ func (t *TransitionVM) afterAccept(ctx context.Context, block snowman.Block) err t.postFork = adaptor.Convert[*blocks.Block](&saeWrapper{ VM: vm, }) - - t.postFork = vm // t.Set(t.postFork) return nil } From 824bedbb894b389be0887609cc183fcc016ac7a6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 25 Jun 2025 11:24:54 -0400 Subject: [PATCH 03/45] wip --- sae/main.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 sae/main.go diff --git a/sae/main.go b/sae/main.go new file mode 100644 index 0000000000..627366637b --- /dev/null +++ b/sae/main.go @@ -0,0 +1,202 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "errors" + "fmt" + "iter" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/rpcchainvm" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/ava-labs/coreth/plugin/evm/atomic" + "github.com/ava-labs/coreth/plugin/evm/customtypes" + "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/strevm/hook" + "github.com/ava-labs/strevm/worstcase" + "github.com/golang/gddo/log" + "k8s.io/utils/set" + + atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" +) + +const targetAtomicTxsSize = 40 * units.KiB + +var _ hook.Points = &hooks{} + +type hooks struct { + ctx *snow.Context + chainConfig *params.ChainConfig + mempool *atomictxpool.Mempool +} + +func (h *hooks) GasTarget(parent *types.Block) gas.Gas { + state, err := acp176.ParseState(parent.Extra()) + if err != nil { + panic(err) + } + return state.Target() +} + +func (h *hooks) ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) { + cumulativeInputUTXOs, err := inputUTXOs(ancestors) + if err != nil { + return nil, err + } + + var ( + cumulativeSize int + atomicTxs []*atomic.Tx + batchContribution = big.NewInt(0) + gasUsed gas.Gas + ) + for { + tx, exists := h.mempool.NextTx() + if !exists { + break + } + + // Ensure that adding [tx] to the block will not exceed the block size + // soft limit. + txSize := len(tx.SignedBytes()) + if cumulativeSize+txSize > targetAtomicTxsSize { + h.mempool.CancelCurrentTx(tx.ID()) + break + } + + inputUTXOs := tx.InputUTXOs() + if cumulativeInputUTXOs.Overlaps(inputUTXOs) { + // Discard the transaction from the mempool since it will fail + // verification after this block has been accepted. + // + // Note: if the proposed block is not accepted, the transaction may + // still be valid, but we discard it early here based on the + // assumption that the proposed block will most likely be accepted. + log.Debug("discarding tx due to overlapping input utxos", "txID", tx.ID()) + h.mempool.DiscardCurrentTx(tx.ID()) + continue + } + + var ( + txGasUsed, txContribution *big.Int + err error + ) + + // Note: we do not need to check if we are in at least ApricotPhase4 + // here because we assume that this function will only be called when + // the block is in at least ApricotPhase5. + txContribution, txGasUsed, err = tx.BlockFeeContribution(true, h.ctx.AVAXAssetID, header.BaseFee) + if err != nil { + return nil, err + } + + // TODO: Make this correctly + op := hook.Op{ + Gas: gas.Gas(txGasUsed.Uint64()), + } + err = state.Apply(op) + if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { + // Send [tx] back to the mempool's tx heap. + h.mempool.CancelCurrentTx(tx.ID()) + break + } + if err != nil { + log.Debug("discarding tx from mempool due to failed verification", + "txID", tx.ID(), + "err", err, + ) + h.mempool.DiscardCurrentTx(tx.ID()) + continue + } + + atomicTxs = append(atomicTxs, tx) + cumulativeInputUTXOs.Union(inputUTXOs) + + // Add the [txGasUsed] to the [batchGasUsed] when the [tx] has passed verification + gasUsed += gas.Gas(txGasUsed.Uint64()) + batchContribution.Add(batchContribution, txContribution) + cumulativeSize += txSize + } + + // If there is a non-zero number of transactions, marshal them and return the byte slice + // for the block's extra data along with the contribution and gas used. + if len(atomicTxs) > 0 { + atomicTxBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, atomicTxs) + if err != nil { + // If we fail to marshal the batch of atomic transactions for any reason, + // discard the entire set of current transactions. + log.Debug("discarding txs due to error marshaling atomic transactions", "err", err) + vm.mempool.DiscardCurrentTxs() + return nil, nil, nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) + } + return atomicTxBytes, batchContribution, batchGasUsed, nil + } + + // If there are no regular transactions and there were also no atomic + // transactions to be included, then the block is empty and should be + // considered invalid. + if len(txs) == 0 { + // this could happen due to the async logic of geth tx pool + return nil, nil, nil, atomicvm.ErrEmptyBlock + } + + // If there are no atomic transactions, but there is a non-zero number of regular transactions, then + // we return a nil slice with no contribution from the atomic transactions and a nil error. + return nil, nil, nil, nil + +} + +func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { + panic("unimplemented") +} + +func (h *hooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { + panic("unimplemented") +} + +func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { + panic("unimplemented") +} + +func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { + var inputUTXOs set.Set[ids.ID] + for block := range blocks { + // Extract atomic transactions from the block + txs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(block), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + for _, tx := range txs { + inputUTXOs.Union(tx.InputUTXOs()) + } + } + return inputUTXOs, nil +} + +func main() { + rpcchainvm.Serve(context.Background(), &evm.VM{IsPlugin: true}) +} From 1bcd4f0293ab13272e90d4dc28b95b29dcfe1eba Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 3 Jul 2025 10:41:32 -0400 Subject: [PATCH 04/45] wip --- sae/main.go | 180 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 74 deletions(-) diff --git a/sae/main.go b/sae/main.go index 627366637b..a98bcf892c 100644 --- a/sae/main.go +++ b/sae/main.go @@ -32,7 +32,11 @@ import ( const targetAtomicTxsSize = 40 * units.KiB -var _ hook.Points = &hooks{} +var ( + _ hook.Points = &hooks{} + + errEmptyBlock = errors.New("empty block") +) type hooks struct { ctx *snow.Context @@ -58,83 +62,27 @@ func (h *hooks) ConstructBlock( txs []*types.Transaction, receipts []*types.Receipt, ) (*types.Block, error) { - cumulativeInputUTXOs, err := inputUTXOs(ancestors) + ancestorInputUTXOs, err := inputUTXOs(ancestors) if err != nil { return nil, err } - var ( - cumulativeSize int - atomicTxs []*atomic.Tx - batchContribution = big.NewInt(0) - gasUsed gas.Gas + atomicTxs, batchContribution, err := packAtomicTxs( + ctx, + state, + h.ctx.AVAXAssetID, + header.BaseFee, + ancestorInputUTXOs, + h.mempool, ) - for { - tx, exists := h.mempool.NextTx() - if !exists { - break - } - - // Ensure that adding [tx] to the block will not exceed the block size - // soft limit. - txSize := len(tx.SignedBytes()) - if cumulativeSize+txSize > targetAtomicTxsSize { - h.mempool.CancelCurrentTx(tx.ID()) - break - } - - inputUTXOs := tx.InputUTXOs() - if cumulativeInputUTXOs.Overlaps(inputUTXOs) { - // Discard the transaction from the mempool since it will fail - // verification after this block has been accepted. - // - // Note: if the proposed block is not accepted, the transaction may - // still be valid, but we discard it early here based on the - // assumption that the proposed block will most likely be accepted. - log.Debug("discarding tx due to overlapping input utxos", "txID", tx.ID()) - h.mempool.DiscardCurrentTx(tx.ID()) - continue - } - - var ( - txGasUsed, txContribution *big.Int - err error - ) - - // Note: we do not need to check if we are in at least ApricotPhase4 - // here because we assume that this function will only be called when - // the block is in at least ApricotPhase5. - txContribution, txGasUsed, err = tx.BlockFeeContribution(true, h.ctx.AVAXAssetID, header.BaseFee) - if err != nil { - return nil, err - } - - // TODO: Make this correctly - op := hook.Op{ - Gas: gas.Gas(txGasUsed.Uint64()), - } - err = state.Apply(op) - if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { - // Send [tx] back to the mempool's tx heap. - h.mempool.CancelCurrentTx(tx.ID()) - break - } - if err != nil { - log.Debug("discarding tx from mempool due to failed verification", - "txID", tx.ID(), - "err", err, - ) - h.mempool.DiscardCurrentTx(tx.ID()) - continue - } - - atomicTxs = append(atomicTxs, tx) - cumulativeInputUTXOs.Union(inputUTXOs) + if err != nil { + return nil, err + } - // Add the [txGasUsed] to the [batchGasUsed] when the [tx] has passed verification - gasUsed += gas.Gas(txGasUsed.Uint64()) - batchContribution.Add(batchContribution, txContribution) - cumulativeSize += txSize + // Blocks must either settle a prior transaction, include a new ethereum tx, + // or include a new atomic tx. + if header.GasUsed == 0 && len(txs) == 0 && len(atomicTxs) == 0 { + return nil, errEmptyBlock } // If there is a non-zero number of transactions, marshal them and return the byte slice @@ -155,8 +103,7 @@ func (h *hooks) ConstructBlock( // transactions to be included, then the block is empty and should be // considered invalid. if len(txs) == 0 { - // this could happen due to the async logic of geth tx pool - return nil, nil, nil, atomicvm.ErrEmptyBlock + return nil, errEmptyBlock } // If there are no atomic transactions, but there is a non-zero number of regular transactions, then @@ -197,6 +144,91 @@ func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { return inputUTXOs, nil } +func packAtomicTxs( + ctx context.Context, + state hook.State, + avaxAssetID ids.ID, + baseFee *big.Int, + ancestorInputUTXOs set.Set[ids.ID], + mempool *atomictxpool.Mempool, +) ([]*atomic.Tx, *big.Int, error) { + var ( + cumulativeSize int + atomicTxs []*atomic.Tx + batchContribution = big.NewInt(0) + ) + for { + tx, exists := mempool.NextTx() + if !exists { + break + } + + // Ensure that adding [tx] to the block will not exceed the block size + // soft limit. + txSize := len(tx.SignedBytes()) + if cumulativeSize+txSize > targetAtomicTxsSize { + mempool.CancelCurrentTx(tx.ID()) + break + } + + inputUTXOs := tx.InputUTXOs() + if ancestorInputUTXOs.Overlaps(inputUTXOs) { + // Discard the transaction from the mempool since it will fail + // verification after this block has been accepted. + // + // Note: if the proposed block is not accepted, the transaction may + // still be valid, but we discard it early here based on the + // assumption that the proposed block will most likely be accepted. + log.Debug("discarding tx due to overlapping input utxos", + "txID", tx.ID(), + ) + mempool.DiscardCurrentTx(tx.ID()) + continue + } + + // Note: we do not need to check if we are in at least ApricotPhase4 + // here because we assume that this function will only be called when + // the block is in at least ApricotPhase5. + txContribution, txGasUsed, err := tx.BlockFeeContribution(true, avaxAssetID, baseFee) + if err != nil { + return nil, nil, err + } + + // TODO: Make this correctly + op := hook.Op{ + Gas: gas.Gas(txGasUsed.Uint64()), + } + err = state.Apply(op) + if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { + // Send [tx] back to the mempool's tx heap. + mempool.CancelCurrentTx(tx.ID()) + break + } + if err != nil { + log.Debug("discarding tx from mempool due to failed verification", + "txID", tx.ID(), + "err", err, + ) + mempool.DiscardCurrentTx(tx.ID()) + continue + } + + atomicTxs = append(atomicTxs, tx) + ancestorInputUTXOs.Union(inputUTXOs) + + batchContribution.Add(batchContribution, txContribution) + cumulativeSize += txSize + } + return atomicTxs, batchContribution, nil +} + +func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { + if len(txs) == 0 { + return nil, nil + } + return atomic.Codec.Marshal(atomic.CodecVersion, txs) +} + func main() { rpcchainvm.Serve(context.Background(), &evm.VM{IsPlugin: true}) } From 469e17c1edc509dbef6468d185532f3db23ba194 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 7 Jul 2025 10:29:04 -0400 Subject: [PATCH 05/45] wip --- go.mod | 2 +- sae/main.go | 232 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 181 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 349ab3cf74..c49075a3a3 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/urfave/cli/v2 v2.25.7 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.5.0 + go.uber.org/zap v1.26.0 golang.org/x/crypto v0.36.0 golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e golang.org/x/sync v0.12.0 @@ -129,7 +130,6 @@ require ( go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.31.0 // indirect diff --git a/sae/main.go b/sae/main.go index a98bcf892c..47109ebb63 100644 --- a/sae/main.go +++ b/sae/main.go @@ -13,19 +13,26 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" + "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/plugin/evm" "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/predicate" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/trie" "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/worstcase" - "github.com/golang/gddo/log" - "k8s.io/utils/set" + "github.com/holiman/uint256" + "go.uber.org/zap" atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" ) @@ -45,11 +52,8 @@ type hooks struct { } func (h *hooks) GasTarget(parent *types.Block) gas.Gas { - state, err := acp176.ParseState(parent.Extra()) - if err != nil { - panic(err) - } - return state.Target() + // TODO: implement me + return acp176.MinTargetPerSecond } func (h *hooks) ConstructBlock( @@ -67,8 +71,9 @@ func (h *hooks) ConstructBlock( return nil, err } - atomicTxs, batchContribution, err := packAtomicTxs( + atomicTxs, err := packAtomicTxs( ctx, + h.ctx.Log, state, h.ctx.AVAXAssetID, header.BaseFee, @@ -85,43 +90,88 @@ func (h *hooks) ConstructBlock( return nil, errEmptyBlock } - // If there is a non-zero number of transactions, marshal them and return the byte slice - // for the block's extra data along with the contribution and gas used. - if len(atomicTxs) > 0 { - atomicTxBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, atomicTxs) - if err != nil { - // If we fail to marshal the batch of atomic transactions for any reason, - // discard the entire set of current transactions. - log.Debug("discarding txs due to error marshaling atomic transactions", "err", err) - vm.mempool.DiscardCurrentTxs() - return nil, nil, nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) - } - return atomicTxBytes, batchContribution, batchGasUsed, nil + // TODO: This is where the block fee should be verified, do we still want to + // utilize a block fee? + + atomicTxBytes, err := marshalAtomicTxs(atomicTxs) + if err != nil { + // If we fail to marshal the batch of atomic transactions for any + // reason, discard the entire set of current transactions. + h.ctx.Log.Debug("discarding txs due to error marshaling atomic transactions", + zap.Error(err), + ) + h.mempool.DiscardCurrentTxs() + return nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) } - // If there are no regular transactions and there were also no atomic - // transactions to be included, then the block is empty and should be - // considered invalid. - if len(txs) == 0 { - return nil, errEmptyBlock + // TODO: What should we be doing with the ACP-176 logic here? + // + // chainConfigExtra := params.GetExtra(h.chainConfig) + // extraPrefix, err := customheader.ExtraPrefix(chainConfigExtra, parent, header, nil) // TODO: Populate desired target excess + // if err != nil { + // return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err) + // } + + rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) + predicateResults, err := calculatePredicateResults( + ctx, + h.ctx, + rules, + blockContext, + txs, + ) + if err != nil { + return nil, fmt.Errorf("calculatePredicateResults: %w", err) } - // If there are no atomic transactions, but there is a non-zero number of regular transactions, then - // we return a nil slice with no contribution from the atomic transactions and a nil error. - return nil, nil, nil, nil + predicateResultsBytes, err := predicateResults.Bytes() + if err != nil { + return nil, fmt.Errorf("predicateResults bytes: %w", err) + } + header.Extra = predicateResultsBytes // append(extraPrefix, predicateResultsBytes...) + return customtypes.NewBlockWithExtData( + header, + txs, + nil, + receipts, + trie.NewStackTrie(nil), + atomicTxBytes, + true, + ), nil } func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { - panic("unimplemented") + return nil // TODO: Implement me } -func (h *hooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { - panic("unimplemented") +func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { + return func(context.Context, *block.Context, *types.Header, *types.Header, iter.Seq[*types.Block], hook.State, []*types.Transaction, []*types.Receipt) (*types.Block, error) { + // TODO: Implement me + return b, nil + }, nil } func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { - panic("unimplemented") + txs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(block), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + baseFee := block.BaseFee() + ops := make([]hook.Op, len(txs)) + for i, tx := range txs { + op, err := atomicTxOp(tx, h.ctx.AVAXAssetID, baseFee) + if err != nil { + return nil, err + } + ops[i] = op + } + return ops, nil } func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { @@ -146,16 +196,16 @@ func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { func packAtomicTxs( ctx context.Context, + log logging.Logger, state hook.State, avaxAssetID ids.ID, baseFee *big.Int, ancestorInputUTXOs set.Set[ids.ID], mempool *atomictxpool.Mempool, -) ([]*atomic.Tx, *big.Int, error) { +) ([]*atomic.Tx, error) { var ( - cumulativeSize int - atomicTxs []*atomic.Tx - batchContribution = big.NewInt(0) + cumulativeSize int + atomicTxs []*atomic.Tx ) for { tx, exists := mempool.NextTx() @@ -179,25 +229,20 @@ func packAtomicTxs( // Note: if the proposed block is not accepted, the transaction may // still be valid, but we discard it early here based on the // assumption that the proposed block will most likely be accepted. + txID := tx.ID() log.Debug("discarding tx due to overlapping input utxos", - "txID", tx.ID(), + zap.Stringer("txID", txID), ) - mempool.DiscardCurrentTx(tx.ID()) + mempool.DiscardCurrentTx(txID) continue } - // Note: we do not need to check if we are in at least ApricotPhase4 - // here because we assume that this function will only be called when - // the block is in at least ApricotPhase5. - txContribution, txGasUsed, err := tx.BlockFeeContribution(true, avaxAssetID, baseFee) + op, err := atomicTxOp(tx, avaxAssetID, baseFee) if err != nil { - return nil, nil, err + mempool.DiscardCurrentTx(tx.ID()) + continue } - // TODO: Make this correctly - op := hook.Op{ - Gas: gas.Gas(txGasUsed.Uint64()), - } err = state.Apply(op) if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { // Send [tx] back to the mempool's tx heap. @@ -205,21 +250,82 @@ func packAtomicTxs( break } if err != nil { + txID := tx.ID() log.Debug("discarding tx from mempool due to failed verification", - "txID", tx.ID(), - "err", err, + zap.Stringer("txID", txID), + zap.Error(err), ) - mempool.DiscardCurrentTx(tx.ID()) + mempool.DiscardCurrentTx(txID) continue } atomicTxs = append(atomicTxs, tx) ancestorInputUTXOs.Union(inputUTXOs) - batchContribution.Add(batchContribution, txContribution) cumulativeSize += txSize } - return atomicTxs, batchContribution, nil + return atomicTxs, nil +} + +func atomicTxOp( + tx *atomic.Tx, + avaxAssetID ids.ID, + baseFee *big.Int, +) (hook.Op, error) { + // Note: we do not need to check if we are in at least ApricotPhase4 here + // because we assume that this function will only be called when the block + // is in at least ApricotPhase5. + gasUsed, err := tx.GasUsed(true) + if err != nil { + return hook.Op{}, err + } + burned, err := tx.Burned(avaxAssetID) + if err != nil { + return hook.Op{}, err + } + + var bigGasUsed uint256.Int + bigGasUsed.SetUint64(gasUsed) + + var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed + gasPrice.SetUint64(burned) + gasPrice.Mul(&gasPrice, atomic.X2CRate) + gasPrice.Div(&gasPrice, &bigGasUsed) + + op := hook.Op{ + Gas: gas.Gas(gasUsed), + GasPrice: gasPrice, + } + switch tx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + op.To = make(map[common.Address]uint256.Int) + for _, output := range tx.Outs { + if output.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(output.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.To[output.Address] = amount + } + case *atomic.UnsignedExportTx: + op.From = make(map[common.Address]hook.Account) + for _, input := range tx.Ins { + if input.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(input.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.From[input.Address] = hook.Account{ + Nonce: input.Nonce, + Amount: amount, + } + } + default: + return hook.Op{}, fmt.Errorf("unexpected atomic tx type: %T", tx) + } + return op, nil } func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { @@ -229,6 +335,28 @@ func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { return atomic.Codec.Marshal(atomic.CodecVersion, txs) } +func calculatePredicateResults( + ctx context.Context, + snowContext *snow.Context, + rules params.Rules, + blockContext *block.Context, + txs []*types.Transaction, +) (*predicate.Results, error) { + predicateContext := precompileconfig.PredicateContext{ + SnowCtx: snowContext, + ProposerVMBlockCtx: blockContext, + } + predicateResults := predicate.NewResults() + for _, tx := range txs { + results, err := core.CheckPredicates(rules, &predicateContext, tx) + if err != nil { + return nil, err + } + predicateResults.SetTxResults(tx.Hash(), results) + } + return predicateResults, nil +} + func main() { rpcchainvm.Serve(context.Background(), &evm.VM{IsPlugin: true}) } From c606d9cfec41115d6e4c967dadaf74bf3be68838 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 7 Jul 2025 16:43:03 -0400 Subject: [PATCH 06/45] wip --- sae/hooks.go | 356 +++++++++++++++++++++++++++++++++++++++++++++++++++ sae/main.go | 356 +-------------------------------------------------- sae/vm.go | 75 +++++++++++ 3 files changed, 438 insertions(+), 349 deletions(-) create mode 100644 sae/hooks.go create mode 100644 sae/vm.go diff --git a/sae/hooks.go b/sae/hooks.go new file mode 100644 index 0000000000..114a0a2dc5 --- /dev/null +++ b/sae/hooks.go @@ -0,0 +1,356 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "errors" + "fmt" + "iter" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/plugin/evm/atomic" + "github.com/ava-labs/coreth/plugin/evm/customtypes" + "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/predicate" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/trie" + "github.com/ava-labs/strevm/hook" + "github.com/ava-labs/strevm/worstcase" + "github.com/holiman/uint256" + "go.uber.org/zap" + + atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" +) + +const targetAtomicTxsSize = 40 * units.KiB + +var ( + _ hook.Points = &hooks{} + + errEmptyBlock = errors.New("empty block") +) + +type hooks struct { + ctx *snow.Context + chainConfig *params.ChainConfig + mempool *atomictxpool.Mempool +} + +func (h *hooks) GasTarget(parent *types.Block) gas.Gas { + // TODO: implement me + return acp176.MinTargetPerSecond +} + +func (h *hooks) ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) { + ancestorInputUTXOs, err := inputUTXOs(ancestors) + if err != nil { + return nil, err + } + + atomicTxs, err := packAtomicTxs( + ctx, + h.ctx.Log, + state, + h.ctx.AVAXAssetID, + header.BaseFee, + ancestorInputUTXOs, + h.mempool, + ) + if err != nil { + return nil, err + } + + // Blocks must either settle a prior transaction, include a new ethereum tx, + // or include a new atomic tx. + if header.GasUsed == 0 && len(txs) == 0 && len(atomicTxs) == 0 { + return nil, errEmptyBlock + } + + // TODO: This is where the block fee should be verified, do we still want to + // utilize a block fee? + + atomicTxBytes, err := marshalAtomicTxs(atomicTxs) + if err != nil { + // If we fail to marshal the batch of atomic transactions for any + // reason, discard the entire set of current transactions. + h.ctx.Log.Debug("discarding txs due to error marshaling atomic transactions", + zap.Error(err), + ) + h.mempool.DiscardCurrentTxs() + return nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) + } + + // TODO: What should we be doing with the ACP-176 logic here? + // + // chainConfigExtra := params.GetExtra(h.chainConfig) + // extraPrefix, err := customheader.ExtraPrefix(chainConfigExtra, parent, header, nil) // TODO: Populate desired target excess + // if err != nil { + // return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err) + // } + + rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) + predicateResults, err := calculatePredicateResults( + ctx, + h.ctx, + rules, + blockContext, + txs, + ) + if err != nil { + return nil, fmt.Errorf("calculatePredicateResults: %w", err) + } + + predicateResultsBytes, err := predicateResults.Bytes() + if err != nil { + return nil, fmt.Errorf("predicateResults bytes: %w", err) + } + + header.Extra = predicateResultsBytes // append(extraPrefix, predicateResultsBytes...) + return customtypes.NewBlockWithExtData( + header, + txs, + nil, + receipts, + trie.NewStackTrie(nil), + atomicTxBytes, + true, + ), nil +} + +func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { + return nil // TODO: Implement me +} + +func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { + return func(context.Context, *block.Context, *types.Header, *types.Header, iter.Seq[*types.Block], hook.State, []*types.Transaction, []*types.Receipt) (*types.Block, error) { + // TODO: Implement me + return b, nil + }, nil +} + +func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { + txs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(block), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + baseFee := block.BaseFee() + ops := make([]hook.Op, len(txs)) + for i, tx := range txs { + op, err := atomicTxOp(tx, h.ctx.AVAXAssetID, baseFee) + if err != nil { + return nil, err + } + ops[i] = op + } + return ops, nil +} + +func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { + var inputUTXOs set.Set[ids.ID] + for block := range blocks { + // Extract atomic transactions from the block + txs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(block), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + for _, tx := range txs { + inputUTXOs.Union(tx.InputUTXOs()) + } + } + return inputUTXOs, nil +} + +func packAtomicTxs( + ctx context.Context, + log logging.Logger, + state hook.State, + avaxAssetID ids.ID, + baseFee *big.Int, + ancestorInputUTXOs set.Set[ids.ID], + mempool *atomictxpool.Mempool, +) ([]*atomic.Tx, error) { + var ( + cumulativeSize int + atomicTxs []*atomic.Tx + ) + for { + tx, exists := mempool.NextTx() + if !exists { + break + } + + // Ensure that adding [tx] to the block will not exceed the block size + // soft limit. + txSize := len(tx.SignedBytes()) + if cumulativeSize+txSize > targetAtomicTxsSize { + mempool.CancelCurrentTx(tx.ID()) + break + } + + inputUTXOs := tx.InputUTXOs() + if ancestorInputUTXOs.Overlaps(inputUTXOs) { + // Discard the transaction from the mempool since it will fail + // verification after this block has been accepted. + // + // Note: if the proposed block is not accepted, the transaction may + // still be valid, but we discard it early here based on the + // assumption that the proposed block will most likely be accepted. + txID := tx.ID() + log.Debug("discarding tx due to overlapping input utxos", + zap.Stringer("txID", txID), + ) + mempool.DiscardCurrentTx(txID) + continue + } + + op, err := atomicTxOp(tx, avaxAssetID, baseFee) + if err != nil { + mempool.DiscardCurrentTx(tx.ID()) + continue + } + + err = state.Apply(op) + if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { + // Send [tx] back to the mempool's tx heap. + mempool.CancelCurrentTx(tx.ID()) + break + } + if err != nil { + txID := tx.ID() + log.Debug("discarding tx from mempool due to failed verification", + zap.Stringer("txID", txID), + zap.Error(err), + ) + mempool.DiscardCurrentTx(txID) + continue + } + + atomicTxs = append(atomicTxs, tx) + ancestorInputUTXOs.Union(inputUTXOs) + + cumulativeSize += txSize + } + return atomicTxs, nil +} + +func atomicTxOp( + tx *atomic.Tx, + avaxAssetID ids.ID, + baseFee *big.Int, +) (hook.Op, error) { + // Note: we do not need to check if we are in at least ApricotPhase4 here + // because we assume that this function will only be called when the block + // is in at least ApricotPhase5. + gasUsed, err := tx.GasUsed(true) + if err != nil { + return hook.Op{}, err + } + burned, err := tx.Burned(avaxAssetID) + if err != nil { + return hook.Op{}, err + } + + var bigGasUsed uint256.Int + bigGasUsed.SetUint64(gasUsed) + + var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed + gasPrice.SetUint64(burned) + gasPrice.Mul(&gasPrice, atomic.X2CRate) + gasPrice.Div(&gasPrice, &bigGasUsed) + + op := hook.Op{ + Gas: gas.Gas(gasUsed), + GasPrice: gasPrice, + } + switch tx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + op.To = make(map[common.Address]uint256.Int) + for _, output := range tx.Outs { + if output.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(output.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.To[output.Address] = amount + } + case *atomic.UnsignedExportTx: + op.From = make(map[common.Address]hook.Account) + for _, input := range tx.Ins { + if input.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(input.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.From[input.Address] = hook.Account{ + Nonce: input.Nonce, + Amount: amount, + } + } + default: + return hook.Op{}, fmt.Errorf("unexpected atomic tx type: %T", tx) + } + return op, nil +} + +func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { + if len(txs) == 0 { + return nil, nil + } + return atomic.Codec.Marshal(atomic.CodecVersion, txs) +} + +func calculatePredicateResults( + ctx context.Context, + snowContext *snow.Context, + rules params.Rules, + blockContext *block.Context, + txs []*types.Transaction, +) (*predicate.Results, error) { + predicateContext := precompileconfig.PredicateContext{ + SnowCtx: snowContext, + ProposerVMBlockCtx: blockContext, + } + predicateResults := predicate.NewResults() + for _, tx := range txs { + results, err := core.CheckPredicates(rules, &predicateContext, tx) + if err != nil { + return nil, err + } + predicateResults.SetTxResults(tx.Hash(), results) + } + return predicateResults, nil +} diff --git a/sae/main.go b/sae/main.go index 47109ebb63..979388e18b 100644 --- a/sae/main.go +++ b/sae/main.go @@ -5,358 +5,16 @@ package main import ( "context" - "errors" - "fmt" - "iter" - "math/big" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" - "github.com/ava-labs/coreth/core" - "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/plugin/evm" - "github.com/ava-labs/coreth/plugin/evm/atomic" - "github.com/ava-labs/coreth/plugin/evm/customtypes" - "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" - "github.com/ava-labs/coreth/precompile/precompileconfig" - "github.com/ava-labs/coreth/predicate" - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/trie" - "github.com/ava-labs/strevm/hook" - "github.com/ava-labs/strevm/worstcase" - "github.com/holiman/uint256" - "go.uber.org/zap" - - atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" -) - -const targetAtomicTxsSize = 40 * units.KiB - -var ( - _ hook.Points = &hooks{} - - errEmptyBlock = errors.New("empty block") + sae "github.com/ava-labs/strevm" + "github.com/ava-labs/strevm/adaptor" ) -type hooks struct { - ctx *snow.Context - chainConfig *params.ChainConfig - mempool *atomictxpool.Mempool -} - -func (h *hooks) GasTarget(parent *types.Block) gas.Gas { - // TODO: implement me - return acp176.MinTargetPerSecond -} - -func (h *hooks) ConstructBlock( - ctx context.Context, - blockContext *block.Context, - header *types.Header, - parent *types.Header, - ancestors iter.Seq[*types.Block], - state hook.State, - txs []*types.Transaction, - receipts []*types.Receipt, -) (*types.Block, error) { - ancestorInputUTXOs, err := inputUTXOs(ancestors) - if err != nil { - return nil, err - } - - atomicTxs, err := packAtomicTxs( - ctx, - h.ctx.Log, - state, - h.ctx.AVAXAssetID, - header.BaseFee, - ancestorInputUTXOs, - h.mempool, - ) - if err != nil { - return nil, err - } - - // Blocks must either settle a prior transaction, include a new ethereum tx, - // or include a new atomic tx. - if header.GasUsed == 0 && len(txs) == 0 && len(atomicTxs) == 0 { - return nil, errEmptyBlock - } - - // TODO: This is where the block fee should be verified, do we still want to - // utilize a block fee? - - atomicTxBytes, err := marshalAtomicTxs(atomicTxs) - if err != nil { - // If we fail to marshal the batch of atomic transactions for any - // reason, discard the entire set of current transactions. - h.ctx.Log.Debug("discarding txs due to error marshaling atomic transactions", - zap.Error(err), - ) - h.mempool.DiscardCurrentTxs() - return nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) - } - - // TODO: What should we be doing with the ACP-176 logic here? - // - // chainConfigExtra := params.GetExtra(h.chainConfig) - // extraPrefix, err := customheader.ExtraPrefix(chainConfigExtra, parent, header, nil) // TODO: Populate desired target excess - // if err != nil { - // return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err) - // } - - rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) - predicateResults, err := calculatePredicateResults( - ctx, - h.ctx, - rules, - blockContext, - txs, - ) - if err != nil { - return nil, fmt.Errorf("calculatePredicateResults: %w", err) - } - - predicateResultsBytes, err := predicateResults.Bytes() - if err != nil { - return nil, fmt.Errorf("predicateResults bytes: %w", err) - } - - header.Extra = predicateResultsBytes // append(extraPrefix, predicateResultsBytes...) - return customtypes.NewBlockWithExtData( - header, - txs, - nil, - receipts, - trie.NewStackTrie(nil), - atomicTxBytes, - true, - ), nil -} - -func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { - return nil // TODO: Implement me -} - -func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { - return func(context.Context, *block.Context, *types.Header, *types.Header, iter.Seq[*types.Block], hook.State, []*types.Transaction, []*types.Receipt) (*types.Block, error) { - // TODO: Implement me - return b, nil - }, nil -} - -func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { - txs, err := atomic.ExtractAtomicTxs( - customtypes.BlockExtData(block), - true, - atomic.Codec, - ) - if err != nil { - return nil, err - } - - baseFee := block.BaseFee() - ops := make([]hook.Op, len(txs)) - for i, tx := range txs { - op, err := atomicTxOp(tx, h.ctx.AVAXAssetID, baseFee) - if err != nil { - return nil, err - } - ops[i] = op - } - return ops, nil -} - -func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { - var inputUTXOs set.Set[ids.ID] - for block := range blocks { - // Extract atomic transactions from the block - txs, err := atomic.ExtractAtomicTxs( - customtypes.BlockExtData(block), - true, - atomic.Codec, - ) - if err != nil { - return nil, err - } - - for _, tx := range txs { - inputUTXOs.Union(tx.InputUTXOs()) - } - } - return inputUTXOs, nil -} - -func packAtomicTxs( - ctx context.Context, - log logging.Logger, - state hook.State, - avaxAssetID ids.ID, - baseFee *big.Int, - ancestorInputUTXOs set.Set[ids.ID], - mempool *atomictxpool.Mempool, -) ([]*atomic.Tx, error) { - var ( - cumulativeSize int - atomicTxs []*atomic.Tx - ) - for { - tx, exists := mempool.NextTx() - if !exists { - break - } - - // Ensure that adding [tx] to the block will not exceed the block size - // soft limit. - txSize := len(tx.SignedBytes()) - if cumulativeSize+txSize > targetAtomicTxsSize { - mempool.CancelCurrentTx(tx.ID()) - break - } - - inputUTXOs := tx.InputUTXOs() - if ancestorInputUTXOs.Overlaps(inputUTXOs) { - // Discard the transaction from the mempool since it will fail - // verification after this block has been accepted. - // - // Note: if the proposed block is not accepted, the transaction may - // still be valid, but we discard it early here based on the - // assumption that the proposed block will most likely be accepted. - txID := tx.ID() - log.Debug("discarding tx due to overlapping input utxos", - zap.Stringer("txID", txID), - ) - mempool.DiscardCurrentTx(txID) - continue - } - - op, err := atomicTxOp(tx, avaxAssetID, baseFee) - if err != nil { - mempool.DiscardCurrentTx(tx.ID()) - continue - } - - err = state.Apply(op) - if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { - // Send [tx] back to the mempool's tx heap. - mempool.CancelCurrentTx(tx.ID()) - break - } - if err != nil { - txID := tx.ID() - log.Debug("discarding tx from mempool due to failed verification", - zap.Stringer("txID", txID), - zap.Error(err), - ) - mempool.DiscardCurrentTx(txID) - continue - } - - atomicTxs = append(atomicTxs, tx) - ancestorInputUTXOs.Union(inputUTXOs) - - cumulativeSize += txSize - } - return atomicTxs, nil -} - -func atomicTxOp( - tx *atomic.Tx, - avaxAssetID ids.ID, - baseFee *big.Int, -) (hook.Op, error) { - // Note: we do not need to check if we are in at least ApricotPhase4 here - // because we assume that this function will only be called when the block - // is in at least ApricotPhase5. - gasUsed, err := tx.GasUsed(true) - if err != nil { - return hook.Op{}, err - } - burned, err := tx.Burned(avaxAssetID) - if err != nil { - return hook.Op{}, err - } - - var bigGasUsed uint256.Int - bigGasUsed.SetUint64(gasUsed) - - var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed - gasPrice.SetUint64(burned) - gasPrice.Mul(&gasPrice, atomic.X2CRate) - gasPrice.Div(&gasPrice, &bigGasUsed) - - op := hook.Op{ - Gas: gas.Gas(gasUsed), - GasPrice: gasPrice, - } - switch tx := tx.UnsignedAtomicTx.(type) { - case *atomic.UnsignedImportTx: - op.To = make(map[common.Address]uint256.Int) - for _, output := range tx.Outs { - if output.AssetID != avaxAssetID { - continue - } - var amount uint256.Int - amount.SetUint64(output.Amount) - amount.Mul(&amount, atomic.X2CRate) - op.To[output.Address] = amount - } - case *atomic.UnsignedExportTx: - op.From = make(map[common.Address]hook.Account) - for _, input := range tx.Ins { - if input.AssetID != avaxAssetID { - continue - } - var amount uint256.Int - amount.SetUint64(input.Amount) - amount.Mul(&amount, atomic.X2CRate) - op.From[input.Address] = hook.Account{ - Nonce: input.Nonce, - Amount: amount, - } - } - default: - return hook.Op{}, fmt.Errorf("unexpected atomic tx type: %T", tx) - } - return op, nil -} - -func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { - if len(txs) == 0 { - return nil, nil - } - return atomic.Codec.Marshal(atomic.CodecVersion, txs) -} - -func calculatePredicateResults( - ctx context.Context, - snowContext *snow.Context, - rules params.Rules, - blockContext *block.Context, - txs []*types.Transaction, -) (*predicate.Results, error) { - predicateContext := precompileconfig.PredicateContext{ - SnowCtx: snowContext, - ProposerVMBlockCtx: blockContext, - } - predicateResults := predicate.NewResults() - for _, tx := range txs { - results, err := core.CheckPredicates(rules, &predicateContext, tx) - if err != nil { - return nil, err - } - predicateResults.SetTxResults(tx.Hash(), results) - } - return predicateResults, nil -} - func main() { - rpcchainvm.Serve(context.Background(), &evm.VM{IsPlugin: true}) + vm := adaptor.Convert(&sae.SinceGenesis{ + Hooks: &hooks{}, + }) + + rpcchainvm.Serve(context.Background(), vm) } diff --git a/sae/vm.go b/sae/vm.go new file mode 100644 index 0000000000..7b1997c0d2 --- /dev/null +++ b/sae/vm.go @@ -0,0 +1,75 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/json" + + avalanchedb "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/core/state" + corethdb "github.com/ava-labs/coreth/plugin/evm/database" + "github.com/ava-labs/libevm/core/rawdb" + sae "github.com/ava-labs/strevm" +) + +type vm struct { + *sae.VM // Populated by [vm.Initialize] +} + +func (vm *vm) Initialize( + ctx context.Context, + chainContext *snow.Context, + db avalanchedb.Database, + genesisBytes []byte, + configBytes []byte, + upgradeBytes []byte, + _ []*common.Fx, + appSender common.AppSender, +) error { + ethDB := rawdb.NewDatabase(corethdb.WrapDatabase(db)) + + genesis := new(core.Genesis) + if err := json.Unmarshal(genesisBytes, genesis); err != nil { + return err + } + sdb := state.NewDatabase(ethdb) + chainConfig, genesisHash, err := core.SetupGenesisBlock(ethdb, sdb.TrieDB(), genesis) + if err != nil { + return err + } + + batch := ethdb.NewBatch() + // Being both the "head" and "finalized" block is a requirement of [Config]. + rawdb.WriteHeadBlockHash(batch, genesisHash) + rawdb.WriteFinalizedBlockHash(batch, genesisHash) + if err := batch.Write(); err != nil { + return err + } + + vm, err := New( + ctx, + Config{ + Hooks: s.Hooks, + ChainConfig: chainConfig, + DB: ethdb, + LastSynchronousBlock: LastSynchronousBlock{ + Hash: genesisHash, + Target: genesisBlockGasTarget, + ExcessAfter: 0, + }, + ToEngine: toEngine, + SnowCtx: chainCtx, + Now: s.Now, + }, + ) + if err != nil { + return err + } + s.VM = vm + return nil +} From 95ed0331ca56258a308c7967b57ce6c60c1baf98 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Jul 2025 11:07:52 -0400 Subject: [PATCH 07/45] wip --- go.mod | 2 +- go.sum | 4 ++-- sae/main.go | 6 +----- sae/vm.go | 35 +++++++++++++++++------------------ 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index e45a40b26d..9f62bf3f6e 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.9 require ( github.com/VictoriaMetrics/fastcache v1.12.1 - github.com/ava-labs/avalanchego v1.13.2-rc.1 + github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 github.com/ava-labs/strevm v0.0.0-00010101000000-000000000000 github.com/davecgh/go-spew v1.1.1 diff --git a/go.sum b/go.sum index 9fd50cf073..eefdacdf64 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.2-rc.1 h1:TaUB0g8L1uRILvJFdOKwjo7h04rGM/u+MZEvHmh/Y6E= -github.com/ava-labs/avalanchego v1.13.2-rc.1/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= +github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d h1:+IK0mMgrXj2yOxh23ShACLeJlG+XMb6ZDeXda5IFgb0= +github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d/go.mod h1:QmwzzCZtKaam5OY45kuzq3UinsyRXH75LkP85W7713M= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 h1:vBMYo+Iazw0rGTr+cwjkBdh5eadLPlv4ywI4lKye3CA= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= diff --git a/sae/main.go b/sae/main.go index 979388e18b..a4d20feee2 100644 --- a/sae/main.go +++ b/sae/main.go @@ -7,14 +7,10 @@ import ( "context" "github.com/ava-labs/avalanchego/vms/rpcchainvm" - sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" ) func main() { - vm := adaptor.Convert(&sae.SinceGenesis{ - Hooks: &hooks{}, - }) - + vm := adaptor.Convert(&vm{}) rpcchainvm.Serve(context.Background(), vm) } diff --git a/sae/vm.go b/sae/vm.go index 7b1997c0d2..ff53abcbc3 100644 --- a/sae/vm.go +++ b/sae/vm.go @@ -10,9 +10,10 @@ import ( avalanchedb "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/state" corethdb "github.com/ava-labs/coreth/plugin/evm/database" + "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" + "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" sae "github.com/ava-labs/strevm" ) @@ -37,13 +38,13 @@ func (vm *vm) Initialize( if err := json.Unmarshal(genesisBytes, genesis); err != nil { return err } - sdb := state.NewDatabase(ethdb) - chainConfig, genesisHash, err := core.SetupGenesisBlock(ethdb, sdb.TrieDB(), genesis) + sdb := state.NewDatabase(ethDB) + chainConfig, genesisHash, err := core.SetupGenesisBlock(ethDB, sdb.TrieDB(), genesis) if err != nil { return err } - batch := ethdb.NewBatch() + batch := ethDB.NewBatch() // Being both the "head" and "finalized" block is a requirement of [Config]. rawdb.WriteHeadBlockHash(batch, genesisHash) rawdb.WriteFinalizedBlockHash(batch, genesisHash) @@ -51,25 +52,23 @@ func (vm *vm) Initialize( return err } - vm, err := New( + vm.VM, err = sae.New( ctx, - Config{ - Hooks: s.Hooks, + sae.Config{ + Hooks: &hooks{ + ctx: chainContext, + chainConfig: chainConfig, + mempool: nil, // TODO: populate me + }, ChainConfig: chainConfig, - DB: ethdb, - LastSynchronousBlock: LastSynchronousBlock{ + DB: ethDB, + LastSynchronousBlock: sae.LastSynchronousBlock{ Hash: genesisHash, - Target: genesisBlockGasTarget, + Target: acp176.MinTargetPerSecond, ExcessAfter: 0, }, - ToEngine: toEngine, - SnowCtx: chainCtx, - Now: s.Now, + SnowCtx: chainContext, }, ) - if err != nil { - return err - } - s.VM = vm - return nil + return err } From f72a0115fbae9327a14168cc3b0bb908d8e857d2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Jul 2025 17:17:03 -0400 Subject: [PATCH 08/45] merged --- plugin/evm/vm_atomic.go | 8 ++++++-- plugin/evm/vm_sae.go | 7 ++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugin/evm/vm_atomic.go b/plugin/evm/vm_atomic.go index 2cdc05628c..a7a20e440f 100644 --- a/plugin/evm/vm_atomic.go +++ b/plugin/evm/vm_atomic.go @@ -103,8 +103,8 @@ func (a *AtomicVM) HealthCheck(ctx context.Context) (interface{}, error) { return a.Get().HealthCheck(ctx) } -func (a *AtomicVM) Initialize(ctx context.Context, chainCtx *snow.Context, db database.Database, genesisBytes []byte, upgradeBytes []byte, configBytes []byte, toEngine chan<- common.Message, fxs []*common.Fx, appSender common.AppSender) error { - return a.Get().Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, toEngine, fxs, appSender) +func (a *AtomicVM) Initialize(ctx context.Context, chainCtx *snow.Context, db database.Database, genesisBytes []byte, upgradeBytes []byte, configBytes []byte, fxs []*common.Fx, appSender common.AppSender) error { + return a.Get().Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, fxs, appSender) } func (a *AtomicVM) LastAccepted(ctx context.Context) (ids.ID, error) { @@ -123,6 +123,10 @@ func (a *AtomicVM) SetPreference(ctx context.Context, blkID ids.ID) error { return a.Get().SetPreference(ctx, blkID) } +func (a *AtomicVM) WaitForEvent(ctx context.Context) (common.Message, error) { + return a.Get().WaitForEvent(ctx) +} + func (a *AtomicVM) SetState(ctx context.Context, state snow.State) error { return a.Get().SetState(ctx, state) } diff --git a/plugin/evm/vm_sae.go b/plugin/evm/vm_sae.go index 0c8f655528..c0dd756177 100644 --- a/plugin/evm/vm_sae.go +++ b/plugin/evm/vm_sae.go @@ -40,7 +40,6 @@ type TransitionVM struct { genesisBytes []byte upgradeBytes []byte configBytes []byte - toEngine chan<- common.Message fxs []*common.Fx appSender common.AppSender @@ -54,7 +53,7 @@ type saeWrapper struct { *sae.VM } -func (*saeWrapper) Initialize(context.Context, *snow.Context, database.Database, []byte, []byte, []byte, chan<- common.Message, []*common.Fx, common.AppSender) error { +func (*saeWrapper) Initialize(context.Context, *snow.Context, database.Database, []byte, []byte, []byte, []*common.Fx, common.AppSender) error { return errors.New("unexpected call to saeWrapper.Initialize") } @@ -65,11 +64,10 @@ func (t *TransitionVM) Initialize( genesisBytes []byte, upgradeBytes []byte, configBytes []byte, - toEngine chan<- common.Message, fxs []*common.Fx, appSender common.AppSender, ) error { - if err := t.preFork.Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, toEngine, fxs, appSender); err != nil { + if err := t.preFork.Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, fxs, appSender); err != nil { return fmt.Errorf("initializing preFork VM: %w", err) } t.Set(t.preFork) @@ -79,7 +77,6 @@ func (t *TransitionVM) Initialize( t.genesisBytes = genesisBytes t.upgradeBytes = upgradeBytes t.configBytes = configBytes - t.toEngine = toEngine t.fxs = fxs t.appSender = appSender From df812dfacb7fa1111caf486fc4639fc129912f38 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 9 Jul 2025 15:49:47 -0400 Subject: [PATCH 09/45] separate atomic tx mempool --- plugin/evm/atomic/txpool/mempool.go | 369 +---------------------- plugin/evm/atomic/txpool/mempool_test.go | 34 ++- plugin/evm/atomic/txpool/metrics.go | 27 ++ plugin/evm/atomic/txpool/txs.go | 357 ++++++++++++++++++++++ plugin/evm/atomic/vm/vm.go | 15 +- sae/hooks.go | 4 +- sae/vm.go | 19 +- 7 files changed, 439 insertions(+), 386 deletions(-) create mode 100644 plugin/evm/atomic/txpool/metrics.go create mode 100644 plugin/evm/atomic/txpool/txs.go diff --git a/plugin/evm/atomic/txpool/mempool.go b/plugin/evm/atomic/txpool/mempool.go index f54fcf8f8f..761af6a8f0 100644 --- a/plugin/evm/atomic/txpool/mempool.go +++ b/plugin/evm/atomic/txpool/mempool.go @@ -6,143 +6,28 @@ package txpool import ( "errors" "fmt" - "sync" - "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/network/p2p/gossip" - "github.com/ava-labs/avalanchego/snow" - "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/config" "github.com/ava-labs/libevm/log" - "github.com/ava-labs/libevm/metrics" ) -const ( - discardedTxsCacheSize = 50 -) - -var ( - _ gossip.Set[*atomic.Tx] = (*Mempool)(nil) - - errTxAlreadyKnown = errors.New("tx already known") - errNoGasUsed = errors.New("no gas used") - ErrConflictingAtomicTx = errors.New("conflicting atomic tx present") - ErrInsufficientAtomicTxFee = errors.New("atomic tx fee too low for atomic mempool") - ErrTooManyAtomicTx = errors.New("too many atomic tx") -) - -// mempoolMetrics defines the metrics for the atomic mempool -type mempoolMetrics struct { - pendingTxs metrics.Gauge // Gauge of currently pending transactions in the txHeap - currentTxs metrics.Gauge // Gauge of current transactions to be issued into a block - issuedTxs metrics.Gauge // Gauge of transactions that have been issued into a block - - addedTxs metrics.Counter // Count of all transactions added to the mempool - discardedTxs metrics.Counter // Count of all discarded transactions -} - -// newMempoolMetrics constructs metrics for the atomic mempool -func newMempoolMetrics() *mempoolMetrics { - return &mempoolMetrics{ - pendingTxs: metrics.GetOrRegisterGauge("atomic_mempool_pending_txs", nil), - currentTxs: metrics.GetOrRegisterGauge("atomic_mempool_current_txs", nil), - issuedTxs: metrics.GetOrRegisterGauge("atomic_mempool_issued_txs", nil), - addedTxs: metrics.GetOrRegisterCounter("atomic_mempool_added_txs", nil), - discardedTxs: metrics.GetOrRegisterCounter("atomic_mempool_discarded_txs", nil), - } -} +var _ gossip.Set[*atomic.Tx] = (*Mempool)(nil) // Mempool is a simple mempool for atomic transactions type Mempool struct { - lock sync.RWMutex - - ctx *snow.Context - // maxSize is the maximum number of transactions allowed to be kept in mempool - maxSize int - // currentTxs is the set of transactions about to be added to a block. - currentTxs map[ids.ID]*atomic.Tx - // issuedTxs is the set of transactions that have been issued into a new block - issuedTxs map[ids.ID]*atomic.Tx - // discardedTxs is an LRU Cache of transactions that have been discarded after failing - // verification. - discardedTxs *lru.Cache[ids.ID, *atomic.Tx] - // pending is a channel of length one, which the mempool ensures has an item on - // it as long as there is an unissued transaction remaining in [txs] - pending chan struct{} - // txHeap is a sorted record of all txs in the mempool by [gasPrice] - // NOTE: [txHeap] ONLY contains pending txs - txHeap *txHeap - // utxoSpenders maps utxoIDs to the transaction consuming them in the mempool - utxoSpenders map[ids.ID]*atomic.Tx - // bloom is a bloom filter containing the txs in the mempool - bloom *gossip.BloomFilter - - metrics *mempoolMetrics - + *Txs verify func(tx *atomic.Tx) error } -// Initialize initializes the Mempool with `maxSize` -func (m *Mempool) Initialize(ctx *snow.Context, registerer prometheus.Registerer, maxSize int, verify func(tx *atomic.Tx) error) error { - bloom, err := gossip.NewBloomFilter(registerer, "atomic_mempool_bloom_filter", - config.TxGossipBloomMinTargetElements, - config.TxGossipBloomTargetFalsePositiveRate, - config.TxGossipBloomResetFalsePositiveRate, - ) - if err != nil { - return fmt.Errorf("failed to initialize bloom filter: %w", err) - } - - m.ctx = ctx - m.issuedTxs = make(map[ids.ID]*atomic.Tx) - m.discardedTxs = lru.NewCache[ids.ID, *atomic.Tx](discardedTxsCacheSize) - m.currentTxs = make(map[ids.ID]*atomic.Tx) - m.pending = make(chan struct{}, 1) - m.txHeap = newTxHeap(maxSize) - m.maxSize = maxSize - m.utxoSpenders = make(map[ids.ID]*atomic.Tx) - m.bloom = bloom - m.metrics = newMempoolMetrics() - m.verify = verify - return nil -} - -// PendingLen returns the number of pending transactions in the mempool -func (m *Mempool) PendingLen() int { - return m.Len() -} - -// Len returns the number of transactions in the mempool -func (m *Mempool) Len() int { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.length() -} - -// assumes the lock is held -func (m *Mempool) length() int { - return m.txHeap.Len() + len(m.issuedTxs) -} - -// atomicTxGasPrice is the [gasPrice] paid by a transaction to burn a given -// amount of [AVAXAssetID] given the value of [gasUsed]. -func (m *Mempool) atomicTxGasPrice(tx *atomic.Tx) (uint64, error) { - gasUsed, err := tx.GasUsed(true) - if err != nil { - return 0, err - } - if gasUsed == 0 { - return 0, errNoGasUsed - } - burned, err := tx.Burned(m.ctx.AVAXAssetID) - if err != nil { - return 0, err +func NewMempool(txs *Txs, verify func(tx *atomic.Tx) error) *Mempool { + return &Mempool{ + Txs: txs, + verify: verify, } - return burned / gasUsed, nil } func (m *Mempool) Add(tx *atomic.Tx) error { @@ -348,245 +233,3 @@ func (m *Mempool) addTx(tx *atomic.Tx, local bool, force bool) error { return nil } - -func (m *Mempool) Iterate(f func(tx *atomic.Tx) bool) { - m.lock.RLock() - defer m.lock.RUnlock() - - for _, item := range m.txHeap.maxHeap.items { - if !f(item.tx) { - return - } - } -} - -func (m *Mempool) GetFilter() ([]byte, []byte) { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.bloom.Marshal() -} - -// NextTx returns a transaction to be issued from the mempool. -func (m *Mempool) NextTx() (*atomic.Tx, bool) { - m.lock.Lock() - defer m.lock.Unlock() - - // We include atomic transactions in blocks sorted by the [gasPrice] they - // pay. - if m.txHeap.Len() > 0 { - tx := m.txHeap.PopMax() - m.currentTxs[tx.ID()] = tx - m.metrics.pendingTxs.Update(int64(m.txHeap.Len())) - m.metrics.currentTxs.Update(int64(len(m.currentTxs))) - return tx, true - } - - return nil, false -} - -// GetPendingTx returns the transaction [txID] and true if it is -// currently in the [txHeap] waiting to be issued into a block. -// Returns nil, false otherwise. -func (m *Mempool) GetPendingTx(txID ids.ID) (*atomic.Tx, bool) { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.txHeap.Get(txID) -} - -// GetTx returns the transaction [txID] if it was issued -// by this node and returns whether it was dropped and whether -// it exists. -func (m *Mempool) GetTx(txID ids.ID) (*atomic.Tx, bool, bool) { - m.lock.RLock() - defer m.lock.RUnlock() - - if tx, ok := m.txHeap.Get(txID); ok { - return tx, false, true - } - if tx, ok := m.issuedTxs[txID]; ok { - return tx, false, true - } - if tx, ok := m.currentTxs[txID]; ok { - return tx, false, true - } - if tx, exists := m.discardedTxs.Get(txID); exists { - return tx, true, true - } - - return nil, false, false -} - -// Has returns true if the mempool contains [txID] or it was issued. -func (m *Mempool) Has(txID ids.ID) bool { - _, dropped, found := m.GetTx(txID) - return found && !dropped -} - -// IssueCurrentTx marks [currentTx] as issued if there is one -func (m *Mempool) IssueCurrentTxs() { - m.lock.Lock() - defer m.lock.Unlock() - - for txID := range m.currentTxs { - m.issuedTxs[txID] = m.currentTxs[txID] - delete(m.currentTxs, txID) - } - m.metrics.issuedTxs.Update(int64(len(m.issuedTxs))) - m.metrics.currentTxs.Update(int64(len(m.currentTxs))) - - // If there are more transactions to be issued, add an item - // to Pending. - if m.txHeap.Len() > 0 { - m.addPending() - } -} - -// CancelCurrentTx marks the attempt to issue [txID] -// as being aborted. This should be called after NextTx returns [txID] -// and the transaction [txID] cannot be included in the block, but should -// not be discarded. For example, CancelCurrentTx should be called if including -// the transaction will put the block above the atomic tx gas limit. -func (m *Mempool) CancelCurrentTx(txID ids.ID) { - m.lock.Lock() - defer m.lock.Unlock() - - if tx, ok := m.currentTxs[txID]; ok { - m.cancelTx(tx) - } -} - -// [CancelCurrentTxs] marks the attempt to issue [currentTxs] -// as being aborted. If this is called after a buildBlock error -// caused by the atomic transaction, then DiscardCurrentTx should have been called -// such that this call will have no effect and should not re-issue the invalid tx. -func (m *Mempool) CancelCurrentTxs() { - m.lock.Lock() - defer m.lock.Unlock() - - // If building a block failed, put the currentTx back in [txs] - // if it exists. - for _, tx := range m.currentTxs { - m.cancelTx(tx) - } - - // If there are more transactions to be issued, add an item - // to Pending. - if m.txHeap.Len() > 0 { - m.addPending() - } -} - -// cancelTx removes [tx] from current transactions and moves it back into the -// tx heap. -// assumes the lock is held. -func (m *Mempool) cancelTx(tx *atomic.Tx) { - // Add tx to heap sorted by gasPrice - gasPrice, err := m.atomicTxGasPrice(tx) - if err == nil { - m.txHeap.Push(tx, gasPrice) - m.metrics.pendingTxs.Update(int64(m.txHeap.Len())) - } else { - // If the err is not nil, we simply discard the transaction because it is - // invalid. This should never happen but we guard against the case it does. - log.Error("failed to calculate atomic tx gas price while canceling current tx", "err", err) - m.removeSpenders(tx) - m.discardedTxs.Put(tx.ID(), tx) - m.metrics.discardedTxs.Inc(1) - } - - delete(m.currentTxs, tx.ID()) - m.metrics.currentTxs.Update(int64(len(m.currentTxs))) -} - -// DiscardCurrentTx marks a [tx] in the [currentTxs] map as invalid and aborts the attempt -// to issue it since it failed verification. -func (m *Mempool) DiscardCurrentTx(txID ids.ID) { - m.lock.Lock() - defer m.lock.Unlock() - - if tx, ok := m.currentTxs[txID]; ok { - m.discardCurrentTx(tx) - } -} - -// DiscardCurrentTxs marks all txs in [currentTxs] as discarded. -func (m *Mempool) DiscardCurrentTxs() { - m.lock.Lock() - defer m.lock.Unlock() - - for _, tx := range m.currentTxs { - m.discardCurrentTx(tx) - } -} - -// discardCurrentTx discards [tx] from the set of current transactions. -// Assumes the lock is held. -func (m *Mempool) discardCurrentTx(tx *atomic.Tx) { - m.removeSpenders(tx) - m.discardedTxs.Put(tx.ID(), tx) - delete(m.currentTxs, tx.ID()) - m.metrics.currentTxs.Update(int64(len(m.currentTxs))) - m.metrics.discardedTxs.Inc(1) -} - -// removeTx removes [txID] from the mempool. -// Note: removeTx will delete all entries from [utxoSpenders] corresponding -// to input UTXOs of [txID]. This means that when replacing a conflicting tx, -// removeTx must be called for all conflicts before overwriting the utxoSpenders -// map. -// Assumes lock is held. -func (m *Mempool) removeTx(tx *atomic.Tx, discard bool) { - txID := tx.ID() - - // Remove from [currentTxs], [txHeap], and [issuedTxs]. - delete(m.currentTxs, txID) - m.txHeap.Remove(txID) - delete(m.issuedTxs, txID) - - if discard { - m.discardedTxs.Put(txID, tx) - m.metrics.discardedTxs.Inc(1) - } else { - m.discardedTxs.Evict(txID) - } - m.metrics.pendingTxs.Update(int64(m.txHeap.Len())) - m.metrics.currentTxs.Update(int64(len(m.currentTxs))) - m.metrics.issuedTxs.Update(int64(len(m.issuedTxs))) - - // Remove all entries from [utxoSpenders]. - m.removeSpenders(tx) -} - -// removeSpenders deletes the entries for all input UTXOs of [tx] from the -// [utxoSpenders] map. -// Assumes the lock is held. -func (m *Mempool) removeSpenders(tx *atomic.Tx) { - for utxoID := range tx.InputUTXOs() { - delete(m.utxoSpenders, utxoID) - } -} - -// RemoveTx removes [txID] from the mempool completely. -// Evicts [tx] from the discarded cache if present. -func (m *Mempool) RemoveTx(tx *atomic.Tx) { - m.lock.Lock() - defer m.lock.Unlock() - - m.removeTx(tx, false) -} - -// addPending makes sure that an item is in the Pending channel. -func (m *Mempool) addPending() { - select { - case m.pending <- struct{}{}: - default: - } -} - -// SubscribePendingTxs implements the BuilderMempool interface and returns a channel -// that signals when there is at least one pending transaction in the mempool -func (m *Mempool) SubscribePendingTxs() <-chan struct{} { - return m.pending -} diff --git a/plugin/evm/atomic/txpool/mempool_test.go b/plugin/evm/atomic/txpool/mempool_test.go index d5f72493b2..53de065583 100644 --- a/plugin/evm/atomic/txpool/mempool_test.go +++ b/plugin/evm/atomic/txpool/mempool_test.go @@ -16,9 +16,11 @@ import ( func TestMempoolAddTx(t *testing.T) { require := require.New(t) - m := &Mempool{} + ctx := snowtest.Context(t, snowtest.CChainID) - require.NoError(m.Initialize(ctx, prometheus.NewRegistry(), 5_000, nil)) + txGroup, err := NewTxs(ctx, prometheus.NewRegistry(), 5_000) + require.NoError(err) + m := NewMempool(txGroup, nil) txs := make([]*atomic.Tx, 0) for i := 0; i < 3_000; i++ { @@ -40,9 +42,11 @@ func TestMempoolAddTx(t *testing.T) { // Add should return an error if a tx is already known func TestMempoolAdd(t *testing.T) { require := require.New(t) - m := &Mempool{} + ctx := snowtest.Context(t, snowtest.CChainID) - require.NoError(m.Initialize(ctx, prometheus.NewRegistry(), 5_000, nil)) + txGroup, err := NewTxs(ctx, prometheus.NewRegistry(), 5_000) + require.NoError(err) + m := NewMempool(txGroup, nil) tx := &atomic.Tx{ UnsignedAtomicTx: &atomictest.TestUnsignedTx{ @@ -51,7 +55,7 @@ func TestMempoolAdd(t *testing.T) { } require.NoError(m.Add(tx)) - err := m.Add(tx) + err = m.Add(tx) require.ErrorIs(err, errTxAlreadyKnown) } @@ -104,9 +108,11 @@ func TestAtomicMempoolIterate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require := require.New(t) + ctx := snowtest.Context(t, snowtest.CChainID) - m := &Mempool{} - require.NoError(m.Initialize(ctx, prometheus.NewRegistry(), 10, nil)) + txGroup, err := NewTxs(ctx, prometheus.NewRegistry(), 10) + require.NoError(err) + m := NewMempool(txGroup, nil) for _, add := range tt.add { require.NoError(m.Add(add)) @@ -136,8 +142,9 @@ func TestMempoolMaxSizeHandling(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - mempool := &Mempool{} - require.NoError(mempool.Initialize(ctx, prometheus.NewRegistry(), 1, nil)) + txs, err := NewTxs(ctx, prometheus.NewRegistry(), 1) + require.NoError(err) + mempool := NewMempool(txs, nil) // create candidate tx (we will drop before validation) tx := atomictest.GenerateTestImportTx() @@ -150,7 +157,7 @@ func TestMempoolMaxSizeHandling(t *testing.T) { // try to add one more tx tx2 := atomictest.GenerateTestImportTx() - err := mempool.AddRemoteTx(tx2) + err = mempool.AddRemoteTx(tx2) require.ErrorIs(err, ErrTooManyAtomicTx) require.False(mempool.Has(tx2.ID())) } @@ -160,15 +167,16 @@ func TestMempoolPriorityDrop(t *testing.T) { require := require.New(t) ctx := snowtest.Context(t, snowtest.CChainID) - mempool := &Mempool{} - require.NoError(mempool.Initialize(ctx, prometheus.NewRegistry(), 1, nil)) + txs, err := NewTxs(ctx, prometheus.NewRegistry(), 1) + require.NoError(err) + mempool := NewMempool(txs, nil) tx1 := atomictest.GenerateTestImportTxWithGas(1, 2) // lower fee require.NoError(mempool.AddRemoteTx(tx1)) require.True(mempool.Has(tx1.ID())) tx2 := atomictest.GenerateTestImportTxWithGas(1, 2) // lower fee - err := mempool.AddRemoteTx(tx2) + err = mempool.AddRemoteTx(tx2) require.ErrorIs(err, ErrInsufficientAtomicTxFee) require.True(mempool.Has(tx1.ID())) require.False(mempool.Has(tx2.ID())) diff --git a/plugin/evm/atomic/txpool/metrics.go b/plugin/evm/atomic/txpool/metrics.go new file mode 100644 index 0000000000..c630963105 --- /dev/null +++ b/plugin/evm/atomic/txpool/metrics.go @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txpool + +import ( + metricspkg "github.com/ava-labs/libevm/metrics" +) + +type metrics struct { + pendingTxs metricspkg.Gauge // Number of currently pending transactions in the txHeap + currentTxs metricspkg.Gauge // Number of current transactions to be issued into a block + issuedTxs metricspkg.Gauge // Number of transactions that have been issued into a block + + addedTxs metricspkg.Counter // Count of all transactions added to the mempool + discardedTxs metricspkg.Counter // Count of all discarded transactions +} + +func newMetrics() *metrics { + return &metrics{ + pendingTxs: metricspkg.GetOrRegisterGauge("atomic_mempool_pending_txs", nil), + currentTxs: metricspkg.GetOrRegisterGauge("atomic_mempool_current_txs", nil), + issuedTxs: metricspkg.GetOrRegisterGauge("atomic_mempool_issued_txs", nil), + addedTxs: metricspkg.GetOrRegisterCounter("atomic_mempool_added_txs", nil), + discardedTxs: metricspkg.GetOrRegisterCounter("atomic_mempool_discarded_txs", nil), + } +} diff --git a/plugin/evm/atomic/txpool/txs.go b/plugin/evm/atomic/txpool/txs.go new file mode 100644 index 0000000000..70216f0736 --- /dev/null +++ b/plugin/evm/atomic/txpool/txs.go @@ -0,0 +1,357 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txpool + +import ( + "errors" + "fmt" + "sync" + + "github.com/ava-labs/avalanchego/cache/lru" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/gossip" + "github.com/ava-labs/avalanchego/snow" + "github.com/prometheus/client_golang/prometheus" + + "github.com/ava-labs/coreth/plugin/evm/atomic" + "github.com/ava-labs/coreth/plugin/evm/config" + "github.com/ava-labs/libevm/log" +) + +const discardedTxsCacheSize = 50 + +var ( + errTxAlreadyKnown = errors.New("tx already known") + errNoGasUsed = errors.New("no gas used") + ErrConflictingAtomicTx = errors.New("conflicting atomic tx present") + ErrInsufficientAtomicTxFee = errors.New("atomic tx fee too low for atomic mempool") + ErrTooManyAtomicTx = errors.New("too many atomic tx") +) + +type Txs struct { + ctx *snow.Context + metrics *metrics + // maxSize is the maximum number of transactions allowed to be kept in mempool + maxSize int + + lock sync.RWMutex + + // currentTxs is the set of transactions about to be added to a block. + currentTxs map[ids.ID]*atomic.Tx + // issuedTxs is the set of transactions that have been issued into a new block + issuedTxs map[ids.ID]*atomic.Tx + // discardedTxs is an LRU Cache of transactions that have been discarded after failing + // verification. + discardedTxs *lru.Cache[ids.ID, *atomic.Tx] + // pending is a channel of length one, which the mempool ensures has an item on + // it as long as there is an unissued transaction remaining in [txs] + pending chan struct{} + // txHeap is a sorted record of all txs in the mempool by [gasPrice] + // NOTE: [txHeap] ONLY contains pending txs + txHeap *txHeap + // utxoSpenders maps utxoIDs to the transaction consuming them in the mempool + utxoSpenders map[ids.ID]*atomic.Tx + // bloom is a bloom filter containing the txs in the mempool + bloom *gossip.BloomFilter +} + +func NewTxs(ctx *snow.Context, registerer prometheus.Registerer, maxSize int) (*Txs, error) { + bloom, err := gossip.NewBloomFilter(registerer, "atomic_mempool_bloom_filter", + config.TxGossipBloomMinTargetElements, + config.TxGossipBloomTargetFalsePositiveRate, + config.TxGossipBloomResetFalsePositiveRate, + ) + if err != nil { + return nil, fmt.Errorf("failed to initialize bloom filter: %w", err) + } + + return &Txs{ + ctx: ctx, + metrics: newMetrics(), + maxSize: maxSize, + currentTxs: make(map[ids.ID]*atomic.Tx), + issuedTxs: make(map[ids.ID]*atomic.Tx), + discardedTxs: lru.NewCache[ids.ID, *atomic.Tx](discardedTxsCacheSize), + pending: make(chan struct{}, 1), + txHeap: newTxHeap(maxSize), + utxoSpenders: make(map[ids.ID]*atomic.Tx), + bloom: bloom, + }, nil +} + +// PendingLen returns the number of pending transactions +func (t *Txs) PendingLen() int { + return t.Len() +} + +// Len returns the number of transactions +func (t *Txs) Len() int { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.length() +} + +// assumes the lock is held +func (t *Txs) length() int { + return t.txHeap.Len() + len(t.issuedTxs) +} + +// atomicTxGasPrice is the [gasPrice] paid by a transaction to burn a given +// amount of [AVAXAssetID] given the value of [gasUsed]. +func (t *Txs) atomicTxGasPrice(tx *atomic.Tx) (uint64, error) { + gasUsed, err := tx.GasUsed(true) + if err != nil { + return 0, err + } + if gasUsed == 0 { + return 0, errNoGasUsed + } + burned, err := tx.Burned(t.ctx.AVAXAssetID) + if err != nil { + return 0, err + } + return burned / gasUsed, nil +} +func (t *Txs) Iterate(f func(tx *atomic.Tx) bool) { + t.lock.RLock() + defer t.lock.RUnlock() + + for _, item := range t.txHeap.maxHeap.items { + if !f(item.tx) { + return + } + } +} + +func (t *Txs) GetFilter() ([]byte, []byte) { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.bloom.Marshal() +} + +// NextTx returns a transaction to be issued from the mempool. +func (t *Txs) NextTx() (*atomic.Tx, bool) { + t.lock.Lock() + defer t.lock.Unlock() + + // We include atomic transactions in blocks sorted by the [gasPrice] they + // pay. + if t.txHeap.Len() > 0 { + tx := t.txHeap.PopMax() + t.currentTxs[tx.ID()] = tx + t.metrics.pendingTxs.Update(int64(t.txHeap.Len())) + t.metrics.currentTxs.Update(int64(len(t.currentTxs))) + return tx, true + } + + return nil, false +} + +// GetPendingTx returns the transaction [txID] and true if it is +// currently in the [txHeap] waiting to be issued into a block. +// Returns nil, false otherwise. +func (t *Txs) GetPendingTx(txID ids.ID) (*atomic.Tx, bool) { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.txHeap.Get(txID) +} + +// GetTx returns the transaction [txID] if it was issued +// by this node and returns whether it was dropped and whether +// it exists. +func (t *Txs) GetTx(txID ids.ID) (*atomic.Tx, bool, bool) { + t.lock.RLock() + defer t.lock.RUnlock() + + if tx, ok := t.txHeap.Get(txID); ok { + return tx, false, true + } + if tx, ok := t.issuedTxs[txID]; ok { + return tx, false, true + } + if tx, ok := t.currentTxs[txID]; ok { + return tx, false, true + } + if tx, exists := t.discardedTxs.Get(txID); exists { + return tx, true, true + } + + return nil, false, false +} + +// Has returns true if the mempool contains [txID] or it was issued. +func (t *Txs) Has(txID ids.ID) bool { + _, dropped, found := t.GetTx(txID) + return found && !dropped +} + +// IssueCurrentTx marks [currentTx] as issued if there is one +func (t *Txs) IssueCurrentTxs() { + t.lock.Lock() + defer t.lock.Unlock() + + for txID := range t.currentTxs { + t.issuedTxs[txID] = t.currentTxs[txID] + delete(t.currentTxs, txID) + } + t.metrics.issuedTxs.Update(int64(len(t.issuedTxs))) + t.metrics.currentTxs.Update(int64(len(t.currentTxs))) + + // If there are more transactions to be issued, add an item + // to Pending. + if t.txHeap.Len() > 0 { + t.addPending() + } +} + +// CancelCurrentTx marks the attempt to issue [txID] +// as being aborted. This should be called after NextTx returns [txID] +// and the transaction [txID] cannot be included in the block, but should +// not be discarded. For example, CancelCurrentTx should be called if including +// the transaction will put the block above the atomic tx gas limit. +func (t *Txs) CancelCurrentTx(txID ids.ID) { + t.lock.Lock() + defer t.lock.Unlock() + + if tx, ok := t.currentTxs[txID]; ok { + t.cancelTx(tx) + } +} + +// [CancelCurrentTxs] marks the attempt to issue [currentTxs] +// as being aborted. If this is called after a buildBlock error +// caused by the atomic transaction, then DiscardCurrentTx should have been called +// such that this call will have no effect and should not re-issue the invalid tx. +func (t *Txs) CancelCurrentTxs() { + t.lock.Lock() + defer t.lock.Unlock() + + // If building a block failed, put the currentTx back in [txs] + // if it exists. + for _, tx := range t.currentTxs { + t.cancelTx(tx) + } + + // If there are more transactions to be issued, add an item + // to Pending. + if t.txHeap.Len() > 0 { + t.addPending() + } +} + +// cancelTx removes [tx] from current transactions and moves it back into the +// tx heap. +// assumes the lock is held. +func (t *Txs) cancelTx(tx *atomic.Tx) { + // Add tx to heap sorted by gasPrice + gasPrice, err := t.atomicTxGasPrice(tx) + if err == nil { + t.txHeap.Push(tx, gasPrice) + t.metrics.pendingTxs.Update(int64(t.txHeap.Len())) + } else { + // If the err is not nil, we simply discard the transaction because it is + // invalid. This should never happen but we guard against the case it does. + log.Error("failed to calculate atomic tx gas price while canceling current tx", "err", err) + t.removeSpenders(tx) + t.discardedTxs.Put(tx.ID(), tx) + t.metrics.discardedTxs.Inc(1) + } + + delete(t.currentTxs, tx.ID()) + t.metrics.currentTxs.Update(int64(len(t.currentTxs))) +} + +// DiscardCurrentTx marks a [tx] in the [currentTxs] map as invalid and aborts the attempt +// to issue it since it failed verification. +func (t *Txs) DiscardCurrentTx(txID ids.ID) { + t.lock.Lock() + defer t.lock.Unlock() + + if tx, ok := t.currentTxs[txID]; ok { + t.discardCurrentTx(tx) + } +} + +// DiscardCurrentTxs marks all txs in [currentTxs] as discarded. +func (t *Txs) DiscardCurrentTxs() { + t.lock.Lock() + defer t.lock.Unlock() + + for _, tx := range t.currentTxs { + t.discardCurrentTx(tx) + } +} + +// discardCurrentTx discards [tx] from the set of current transactions. +// Assumes the lock is held. +func (t *Txs) discardCurrentTx(tx *atomic.Tx) { + t.removeSpenders(tx) + t.discardedTxs.Put(tx.ID(), tx) + delete(t.currentTxs, tx.ID()) + t.metrics.currentTxs.Update(int64(len(t.currentTxs))) + t.metrics.discardedTxs.Inc(1) +} + +// removeTx removes [txID] from the mempool. +// Note: removeTx will delete all entries from [utxoSpenders] corresponding +// to input UTXOs of [txID]. This means that when replacing a conflicting tx, +// removeTx must be called for all conflicts before overwriting the utxoSpenders +// map. +// Assumes lock is held. +func (t *Txs) removeTx(tx *atomic.Tx, discard bool) { + txID := tx.ID() + + // Remove from [currentTxs], [txHeap], and [issuedTxs]. + delete(t.currentTxs, txID) + t.txHeap.Remove(txID) + delete(t.issuedTxs, txID) + + if discard { + t.discardedTxs.Put(txID, tx) + t.metrics.discardedTxs.Inc(1) + } else { + t.discardedTxs.Evict(txID) + } + t.metrics.pendingTxs.Update(int64(t.txHeap.Len())) + t.metrics.currentTxs.Update(int64(len(t.currentTxs))) + t.metrics.issuedTxs.Update(int64(len(t.issuedTxs))) + + // Remove all entries from [utxoSpenders]. + t.removeSpenders(tx) +} + +// removeSpenders deletes the entries for all input UTXOs of [tx] from the +// [utxoSpenders] map. +// Assumes the lock is held. +func (t *Txs) removeSpenders(tx *atomic.Tx) { + for utxoID := range tx.InputUTXOs() { + delete(t.utxoSpenders, utxoID) + } +} + +// RemoveTx removes [txID] from the mempool completely. +// Evicts [tx] from the discarded cache if present. +func (t *Txs) RemoveTx(tx *atomic.Tx) { + t.lock.Lock() + defer t.lock.Unlock() + + t.removeTx(tx, false) +} + +// addPending makes sure that an item is in the Pending channel. +func (t *Txs) addPending() { + select { + case t.pending <- struct{}{}: + default: + } +} + +// SubscribePendingTxs implements the BuilderMempool interface and returns a channel +// that signals when there is at least one pending transaction in the mempool +func (t *Txs) SubscribePendingTxs() <-chan struct{} { + return t.pending +} diff --git a/plugin/evm/atomic/vm/vm.go b/plugin/evm/atomic/vm/vm.go index fd5023adb6..83a5cc38e3 100644 --- a/plugin/evm/atomic/vm/vm.go +++ b/plugin/evm/atomic/vm/vm.go @@ -30,6 +30,7 @@ import ( "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/coreth/consensus/dummy" "github.com/ava-labs/coreth/core/state" @@ -147,7 +148,12 @@ func (vm *VM) Initialize( MetricName: "sync_atomic_trie_leaves", Handler: leafHandler, } - vm.AtomicMempool = &txpool.Mempool{} + + // TODO: Fix the metrics creation + mempoolTxs, err := txpool.NewTxs(chainCtx, prometheus.NewRegistry(), defaultMempoolSize) + if err != nil { + return fmt.Errorf("failed to initialize mempool: %w", err) + } extensionConfig := &extension.Config{ ConsensusCallbacks: vm.createConsensusCallbacks(), @@ -156,7 +162,7 @@ func (vm *VM) Initialize( SyncExtender: syncExtender, SyncSummaryProvider: syncProvider, ExtraSyncLeafHandlerConfig: atomicLeafTypeConfig, - ExtraMempool: vm.AtomicMempool, + ExtraMempool: mempoolTxs, Clock: &vm.clock, } if err := vm.InnerVM.SetExtensionConfig(extensionConfig); err != nil { @@ -178,10 +184,7 @@ func (vm *VM) Initialize( } // Now we can initialize the mempool and so - err := vm.AtomicMempool.Initialize(chainCtx, vm.InnerVM.MetricRegistry(), defaultMempoolSize, vm.verifyTxAtTip) - if err != nil { - return fmt.Errorf("failed to initialize mempool: %w", err) - } + vm.AtomicMempool = txpool.NewMempool(mempoolTxs, vm.verifyTxAtTip) // initialize bonus blocks on mainnet var ( diff --git a/sae/hooks.go b/sae/hooks.go index 114a0a2dc5..057159c210 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -46,7 +46,7 @@ var ( type hooks struct { ctx *snow.Context chainConfig *params.ChainConfig - mempool *atomictxpool.Mempool + mempool *atomictxpool.Txs } func (h *hooks) GasTarget(parent *types.Block) gas.Gas { @@ -199,7 +199,7 @@ func packAtomicTxs( avaxAssetID ids.ID, baseFee *big.Int, ancestorInputUTXOs set.Set[ids.ID], - mempool *atomictxpool.Mempool, + mempool *atomictxpool.Txs, ) ([]*atomic.Tx, error) { var ( cumulativeSize int diff --git a/sae/vm.go b/sae/vm.go index ff53abcbc3..1fe8847902 100644 --- a/sae/vm.go +++ b/sae/vm.go @@ -6,18 +6,23 @@ package main import ( "context" "encoding/json" + "fmt" avalanchedb "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/coreth/core/state" + "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" corethdb "github.com/ava-labs/coreth/plugin/evm/database" "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" sae "github.com/ava-labs/strevm" + "github.com/prometheus/client_golang/prometheus" ) +const atomicMempoolSize = 4096 // number of transactions + type vm struct { *sae.VM // Populated by [vm.Initialize] } @@ -28,7 +33,7 @@ func (vm *vm) Initialize( db avalanchedb.Database, genesisBytes []byte, configBytes []byte, - upgradeBytes []byte, + _ []byte, _ []*common.Fx, appSender common.AppSender, ) error { @@ -52,13 +57,23 @@ func (vm *vm) Initialize( return err } + // TODO: Fix metrics + mempoolTxs, err := txpool.NewTxs( + chainContext, + prometheus.NewRegistry(), + atomicMempoolSize, + ) + if err != nil { + return fmt.Errorf("failed to initialize mempool: %w", err) + } + vm.VM, err = sae.New( ctx, sae.Config{ Hooks: &hooks{ ctx: chainContext, chainConfig: chainConfig, - mempool: nil, // TODO: populate me + mempool: mempoolTxs, }, ChainConfig: chainConfig, DB: ethDB, From 10c6252f26808d16f8b090d9581ba5dbdbba5732 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 11 Jul 2025 13:23:59 -0400 Subject: [PATCH 10/45] wip --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 689745bc12..6458ef8dad 100644 --- a/go.mod +++ b/go.mod @@ -180,3 +180,4 @@ require ( ) replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm +replace github.com/ava-labs/libevm => /Users/stephen/go/src/github.com/ava-labs/libevm From 6311f2e2395908943107c22e9792aee9b4951087 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 11 Jul 2025 14:04:57 -0400 Subject: [PATCH 11/45] wip --- core/state/statedb.go | 49 +++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index ebfd677f22..f20fcb02f4 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -35,6 +35,7 @@ import ( "github.com/ava-labs/coreth/utils" "github.com/ava-labs/libevm/common" ethstate "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/holiman/uint256" ) @@ -53,8 +54,7 @@ type StateDB struct { *ethstate.StateDB // The tx context - thash common.Hash - txIndex int + txHash common.Hash // Some fields remembered as they are used in tests db Database @@ -92,64 +92,54 @@ func WithConcurrentWorkers(prefetchers int) ethstate.PrefetcherOption { } // Retrieve the balance from the given address or 0 if object not found -func (s *StateDB) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int { +func GetBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash) *big.Int { NormalizeCoinID(&coinID) - return s.StateDB.GetState(addr, coinID).Big() -} - -// GetState retrieves a value from the given account's storage trie. -func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - NormalizeStateKey(&hash) - return s.StateDB.GetState(addr, hash) + return s.GetState(addr, coinID, stateconf.SkipStateKeyTransformation()).Big() } // AddBalance adds amount to the account associated with addr. -func (s *StateDB) AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { +func AddBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash, amount *big.Int) { if amount.Sign() == 0 { s.AddBalance(addr, new(uint256.Int)) // used to cause touch return } - if !ethstate.GetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr) { - ethstate.SetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr, true) + + if !ethstate.GetExtra(s, customtypes.IsMultiCoinPayloads, addr) { + ethstate.SetExtra(s, customtypes.IsMultiCoinPayloads, addr, true) } - newAmount := new(big.Int).Add(s.GetBalanceMultiCoin(addr, coinID), amount) + + newAmount := new(big.Int).Add(GetBalanceMultiCoin(s, addr, coinID), amount) NormalizeCoinID(&coinID) - s.StateDB.SetState(addr, coinID, common.BigToHash(newAmount)) + s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) } // SubBalance subtracts amount from the account associated with addr. -func (s *StateDB) SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { +func SubBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash, amount *big.Int) { if amount.Sign() == 0 { return } // Note: It's not needed to set the IsMultiCoin (extras) flag here, as this // call would always be preceded by a call to AddBalanceMultiCoin, which would // set the extra flag. Seems we should remove the redundant code. - if !ethstate.GetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr) { - ethstate.SetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr, true) + if !ethstate.GetExtra(s, customtypes.IsMultiCoinPayloads, addr) { + ethstate.SetExtra(s, customtypes.IsMultiCoinPayloads, addr, true) } - newAmount := new(big.Int).Sub(s.GetBalanceMultiCoin(addr, coinID), amount) + newAmount := new(big.Int).Sub(GetBalanceMultiCoin(s, addr, coinID), amount) NormalizeCoinID(&coinID) - s.StateDB.SetState(addr, coinID, common.BigToHash(newAmount)) -} - -func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { - NormalizeStateKey(&key) - s.StateDB.SetState(addr, key, value) + s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) } // SetTxContext sets the current transaction hash and index which are // used when the EVM emits new state logs. It should be invoked before // transaction execution. func (s *StateDB) SetTxContext(thash common.Hash, ti int) { - s.thash = thash - s.txIndex = ti + s.txHash = thash s.StateDB.SetTxContext(thash, ti) } // GetTxHash returns the current tx hash on the StateDB set by SetTxContext. func (s *StateDB) GetTxHash() common.Hash { - return s.thash + return s.txHash } func (s *StateDB) Copy() *StateDB { @@ -157,8 +147,7 @@ func (s *StateDB) Copy() *StateDB { StateDB: s.StateDB.Copy(), db: s.db, snaps: s.snaps, - thash: s.thash, - txIndex: s.txIndex, + txHash: s.txHash, } } From 24147dde819ece4943dce2ec0d3851c619a11ece Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 11 Jul 2025 14:19:57 -0400 Subject: [PATCH 12/45] wip --- core/state/statedb.go | 57 ++----------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index f20fcb02f4..98c6c6cbd0 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -39,40 +39,9 @@ import ( "github.com/holiman/uint256" ) -// StateDB structs within the ethereum protocol are used to store anything -// within the merkle trie. StateDBs take care of caching and storing -// nested states. It's the general query interface to retrieve: -// -// * Contracts -// * Accounts -// -// Once the state is committed, tries cached in stateDB (including account -// trie, storage tries) will no longer be functional. A new state instance -// must be created with new root and updated database for accessing post- -// commit states. -type StateDB struct { - *ethstate.StateDB +type StateDB = ethstate.StateDB - // The tx context - txHash common.Hash - - // Some fields remembered as they are used in tests - db Database - snaps ethstate.SnapshotTree -} - -// New creates a new state from a given trie. -func New(root common.Hash, db Database, snaps ethstate.SnapshotTree) (*StateDB, error) { - stateDB, err := ethstate.New(root, db, snaps) - if err != nil { - return nil, err - } - return &StateDB{ - StateDB: stateDB, - db: db, - snaps: snaps, - }, nil -} +var New = ethstate.New type workerPool struct { *utils.BoundedWorkers @@ -129,28 +98,6 @@ func SubBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) } -// SetTxContext sets the current transaction hash and index which are -// used when the EVM emits new state logs. It should be invoked before -// transaction execution. -func (s *StateDB) SetTxContext(thash common.Hash, ti int) { - s.txHash = thash - s.StateDB.SetTxContext(thash, ti) -} - -// GetTxHash returns the current tx hash on the StateDB set by SetTxContext. -func (s *StateDB) GetTxHash() common.Hash { - return s.txHash -} - -func (s *StateDB) Copy() *StateDB { - return &StateDB{ - StateDB: s.StateDB.Copy(), - db: s.db, - snaps: s.snaps, - txHash: s.txHash, - } -} - // NormalizeCoinID ORs the 0th bit of the first byte in // `coinID`, which ensures this bit will be 1 and all other // bits are left the same. From 9f65dadcc942fe40b64d01f9b60f51d060afa619 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 14 Jul 2025 18:10:33 -0400 Subject: [PATCH 13/45] kind of working? --- accounts/abi/bind/bind_test.go | 4 + core/blockchain_ext_test.go | 7 +- core/evm.go | 15 +- core/extstate/statedb.go | 75 +++++++--- .../statedb_multicoin_test.go | 101 ++++++++----- core/state/state_object_test.go | 15 +- core/state/statedb.go | 82 +++-------- nativeasset/contract_test.go | 136 ++++++++++-------- plugin/evm/atomic/atomictest/tx.go | 3 +- plugin/evm/atomic/export_tx.go | 40 +++--- plugin/evm/atomic/import_tx.go | 9 +- plugin/evm/atomic/tx.go | 17 +-- plugin/evm/export_tx_test.go | 15 +- plugin/evm/import_tx_test.go | 4 +- precompile/contract/interfaces.go | 12 +- 15 files changed, 290 insertions(+), 245 deletions(-) rename core/{state => extstate}/statedb_multicoin_test.go (64%) diff --git a/accounts/abi/bind/bind_test.go b/accounts/abi/bind/bind_test.go index 1779627be1..3bc1aabe92 100644 --- a/accounts/abi/bind/bind_test.go +++ b/accounts/abi/bind/bind_test.go @@ -2171,23 +2171,27 @@ func golangBindings(t *testing.T, overload bool) { // Convert the package to go modules and use the current source for go-ethereum moder := exec.Command(gocmd, "mod", "init", "bindtest") moder.Dir = pkg + t.Logf("running command: %s", moder.String()) if out, err := moder.CombinedOutput(); err != nil { t.Fatalf("failed to convert binding test to modules: %v\n%s", err, out) } pwd, _ := os.Getwd() replacer := exec.Command(gocmd, "mod", "edit", "-x", "-require", "github.com/ava-labs/coreth@v0.0.0", "-replace", "github.com/ava-labs/coreth="+filepath.Join(pwd, "..", "..", "..")) // Repo root replacer.Dir = pkg + t.Logf("running command: %s", replacer.String()) if out, err := replacer.CombinedOutput(); err != nil { t.Fatalf("failed to replace binding test dependency to current source tree: %v\n%s", err, out) } tidier := exec.Command(gocmd, "mod", "tidy", "-compat=1.23") tidier.Dir = pkg + t.Logf("running command: %s", tidier.String()) if out, err := tidier.CombinedOutput(); err != nil { t.Fatalf("failed to tidy Go module file: %v\n%s", err, out) } // Test the entire package and report any failures cmd := exec.Command(gocmd, "test", "-v", "-count", "1") cmd.Dir = pkg + t.Logf("running command: %s", cmd.String()) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("failed to run binding test: %v\n%s", err, out) } diff --git a/core/blockchain_ext_test.go b/core/blockchain_ext_test.go index f03917d5f5..a75b257cde 100644 --- a/core/blockchain_ext_test.go +++ b/core/blockchain_ext_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/ava-labs/coreth/consensus/dummy" + "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap4" @@ -23,7 +24,8 @@ import ( var TestCallbacks = dummy.ConsensusCallbacks{ OnExtraStateChange: func(block *types.Block, _ *types.Header, sdb *state.StateDB) (*big.Int, *big.Int, error) { - sdb.AddBalanceMultiCoin(common.HexToAddress("0xdeadbeef"), common.HexToHash("0xdeadbeef"), big.NewInt(block.Number().Int64())) + ws := extstate.New(sdb) + ws.AddBalanceMultiCoin(common.HexToAddress("0xdeadbeef"), common.HexToHash("0xdeadbeef"), big.NewInt(block.Number().Int64())) return nil, nil, nil }, OnFinalizeAndAssemble: func( @@ -32,7 +34,8 @@ var TestCallbacks = dummy.ConsensusCallbacks{ sdb *state.StateDB, _ []*types.Transaction, ) ([]byte, *big.Int, *big.Int, error) { - sdb.AddBalanceMultiCoin(common.HexToAddress("0xdeadbeef"), common.HexToHash("0xdeadbeef"), big.NewInt(header.Number.Int64())) + ws := extstate.New(sdb) + ws.AddBalanceMultiCoin(common.HexToAddress("0xdeadbeef"), common.HexToHash("0xdeadbeef"), big.NewInt(header.Number.Int64())) return nil, nil, nil, nil }, } diff --git a/core/evm.go b/core/evm.go index 75096a152c..d2166768c3 100644 --- a/core/evm.go +++ b/core/evm.go @@ -41,6 +41,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/holiman/uint256" ) @@ -77,19 +78,19 @@ func (hooks) OverrideEVMResetArgs(rules params.Rules, args *vm.EVMResetArgs) *vm } func wrapStateDB(rules params.Rules, db vm.StateDB) vm.StateDB { + stateDB := extstate.New(db.(*state.StateDB)) if params.GetRulesExtra(rules).IsApricotPhase1 { - db = &StateDbAP1{db.(extstate.VmStateDB)} + return stateDB } - return extstate.New(db.(extstate.VmStateDB)) + return &StateDBAP0{stateDB} } -type StateDbAP1 struct { - extstate.VmStateDB +type StateDBAP0 struct { + *extstate.StateDB } -func (s *StateDbAP1) GetCommittedState(addr common.Address, key common.Hash) common.Hash { - state.NormalizeStateKey(&key) - return s.VmStateDB.GetCommittedState(addr, key) +func (s *StateDBAP0) GetCommittedState(addr common.Address, key common.Hash, _ ...stateconf.StateDBStateOption) common.Hash { + return s.StateDB.GetCommittedState(addr, key, stateconf.SkipStateKeyTransformation()) } // ChainContext supports retrieving headers and consensus parameters from the diff --git a/core/extstate/statedb.go b/core/extstate/statedb.go index b024ccf733..31ee341a5b 100644 --- a/core/extstate/statedb.go +++ b/core/extstate/statedb.go @@ -7,37 +7,28 @@ import ( "math/big" "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/predicate" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/libevm/stateconf" + "github.com/holiman/uint256" ) -type VmStateDB interface { - vm.StateDB - Logs() []*types.Log - GetTxHash() common.Hash - GetBalanceMultiCoin(common.Address, common.Hash) *big.Int - AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) - SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) -} - -// Allow embedding VmStateDB without exporting the embedded field. -type vmStateDB = VmStateDB - type StateDB struct { - vmStateDB + *state.StateDB // Ordered storage slots to be used in predicate verification as set in the tx access list. // Only set in [StateDB.Prepare], and un-modified through execution. predicateStorageSlots map[common.Address][][]byte } -// New creates a new [*StateDB] with the given [VmStateDB], effectively wrapping it -// with additional functionality. -func New(vm VmStateDB) *StateDB { +// New creates a new [StateDB] with the given [state.StateDB], wrapping it with +// additional functionality. +func New(vm *state.StateDB) *StateDB { return &StateDB{ - vmStateDB: vm, + StateDB: vm, predicateStorageSlots: make(map[common.Address][][]byte), } } @@ -45,7 +36,7 @@ func New(vm VmStateDB) *StateDB { func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, dst *common.Address, precompiles []common.Address, list types.AccessList) { rulesExtra := params.GetRulesExtra(rules) s.predicateStorageSlots = predicate.PreparePredicateStorageSlots(rulesExtra, list) - s.vmStateDB.Prepare(rules, sender, coinbase, dst, precompiles, list) + s.StateDB.Prepare(rules, sender, coinbase, dst, precompiles, list) } // GetPredicateStorageSlots returns the storage slots associated with the address, index pair. @@ -65,3 +56,49 @@ func (s *StateDB) GetPredicateStorageSlots(address common.Address, index int) ([ } return predicates[index], true } + +// Retrieve the balance from the given address or 0 if object not found +func (s *StateDB) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int { + NormalizeCoinID(&coinID) + return s.GetState(addr, coinID, stateconf.SkipStateKeyTransformation()).Big() +} + +// AddBalance adds amount to the account associated with addr. +func (s *StateDB) AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { + if amount.Sign() == 0 { + s.AddBalance(addr, new(uint256.Int)) // used to cause touch + return + } + + if !state.GetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr) { + state.SetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr, true) + } + + newAmount := new(big.Int).Add(s.GetBalanceMultiCoin(addr, coinID), amount) + NormalizeCoinID(&coinID) + s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) +} + +// SubBalance subtracts amount from the account associated with addr. +func (s *StateDB) SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) { + if amount.Sign() == 0 { + return + } + // Note: It's not needed to set the IsMultiCoin (extras) flag here, as this + // call would always be preceded by a call to AddBalanceMultiCoin, which would + // set the extra flag. Seems we should remove the redundant code. + if !state.GetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr) { + state.SetExtra(s.StateDB, customtypes.IsMultiCoinPayloads, addr, true) + } + newAmount := new(big.Int).Sub(s.GetBalanceMultiCoin(addr, coinID), amount) + NormalizeCoinID(&coinID) + s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) +} + +// NormalizeCoinID ORs the 0th bit of the first byte in +// `coinID`, which ensures this bit will be 1 and all other +// bits are left the same. +// This partitions multicoin storage from normal state storage. +func NormalizeCoinID(coinID *common.Hash) { + coinID[0] |= 0x01 +} diff --git a/core/state/statedb_multicoin_test.go b/core/extstate/statedb_multicoin_test.go similarity index 64% rename from core/state/statedb_multicoin_test.go rename to core/extstate/statedb_multicoin_test.go index 0cf27e1c3f..ae5ae6f2f7 100644 --- a/core/state/statedb_multicoin_test.go +++ b/core/extstate/statedb_multicoin_test.go @@ -1,48 +1,59 @@ // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package state +package extstate_test import ( "math/big" "testing" + "github.com/ava-labs/coreth/core/extstate" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/core/state/snapshot" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm/stateconf" "github.com/holiman/uint256" "github.com/stretchr/testify/require" + + . "github.com/ava-labs/coreth/core/extstate" ) func TestMultiCoinOperations(t *testing.T) { - s := newStateEnv() + db := rawdb.NewMemoryDatabase() + s, _ := state.New(types.EmptyRootHash, state.NewDatabase(db), nil) addr := common.Address{1} assetID := common.Hash{2} - root, err := s.state.Commit(0, false) + root, err := s.Commit(0, false) require.NoError(t, err, "committing state") - s.state, err = New(root, s.state.db, s.state.snaps) + s, err = state.New( + root, + state.NewDatabase(db), + nil, + ) require.NoError(t, err, "creating statedb") - s.state.AddBalance(addr, new(uint256.Int)) + s.AddBalance(addr, new(uint256.Int)) - balance := s.state.GetBalanceMultiCoin(addr, assetID) + ws := New(s) + balance := ws.GetBalanceMultiCoin(addr, assetID) require.Equal(t, "0", balance.String(), "expected zero big.Int multicoin balance as string") - s.state.AddBalanceMultiCoin(addr, assetID, big.NewInt(10)) - s.state.SubBalanceMultiCoin(addr, assetID, big.NewInt(5)) - s.state.AddBalanceMultiCoin(addr, assetID, big.NewInt(3)) + ws.AddBalanceMultiCoin(addr, assetID, big.NewInt(10)) + ws.SubBalanceMultiCoin(addr, assetID, big.NewInt(5)) + ws.AddBalanceMultiCoin(addr, assetID, big.NewInt(3)) - balance = s.state.GetBalanceMultiCoin(addr, assetID) + balance = ws.GetBalanceMultiCoin(addr, assetID) require.Equal(t, "8", balance.String(), "unexpected multicoin balance string") } func TestMultiCoinSnapshot(t *testing.T) { db := rawdb.NewMemoryDatabase() - sdb := NewDatabase(db) + sdb := state.NewDatabase(db) // Create empty [snapshot.Tree] and [StateDB] root := common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") @@ -52,9 +63,9 @@ func TestMultiCoinSnapshot(t *testing.T) { addr := common.Address{1} assetID1 := common.Hash{1} assetID2 := common.Hash{2} + assertBalances := func(t *testing.T, stateDB *StateDB, regular, multicoin1, multicoin2 int64) { + t.Helper() - var stateDB *StateDB - assertBalances := func(regular, multicoin1, multicoin2 int64) { balance := stateDB.GetBalance(addr) require.Equal(t, uint256.NewInt(uint64(regular)), balance, "incorrect non-multicoin balance") balanceBig := stateDB.GetBalanceMultiCoin(addr, assetID1) @@ -64,59 +75,69 @@ func TestMultiCoinSnapshot(t *testing.T) { } // Create new state - stateDB, err := New(root, sdb, snapTree) + stateDB, err := state.New(root, sdb, snapTree) require.NoError(t, err, "creating statedb") - assertBalances(0, 0, 0) - stateDB.AddBalance(addr, uint256.NewInt(10)) - assertBalances(10, 0, 0) + ws := New(stateDB) + assertBalances(t, ws, 0, 0, 0) + + ws.AddBalance(addr, uint256.NewInt(10)) + assertBalances(t, ws, 10, 0, 0) // Commit and get the new root snapshotOpt := snapshot.WithBlockHashes(common.Hash{}, common.Hash{}) - root, err = stateDB.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) + root, err = ws.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) require.NoError(t, err, "committing statedb") - assertBalances(10, 0, 0) + assertBalances(t, ws, 10, 0, 0) // Create a new state from the latest root, add a multicoin balance, and // commit it to the tree. - stateDB, err = New(root, sdb, snapTree) + stateDB, err = state.New(root, sdb, snapTree) require.NoError(t, err, "creating statedb") - stateDB.AddBalanceMultiCoin(addr, assetID1, big.NewInt(10)) + + ws = New(stateDB) + ws.AddBalanceMultiCoin(addr, assetID1, big.NewInt(10)) snapshotOpt = snapshot.WithBlockHashes(common.Hash{}, common.Hash{}) - root, err = stateDB.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) + root, err = ws.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) require.NoError(t, err, "committing statedb") - assertBalances(10, 10, 0) + assertBalances(t, ws, 10, 10, 0) // Add more layers than the cap and ensure the balances and layers are correct for i := 0; i < 256; i++ { - stateDB, err = New(root, sdb, snapTree) + stateDB, err = state.New(root, sdb, snapTree) require.NoErrorf(t, err, "creating statedb %d", i) - stateDB.AddBalanceMultiCoin(addr, assetID1, big.NewInt(1)) - stateDB.AddBalanceMultiCoin(addr, assetID2, big.NewInt(2)) + + ws = New(stateDB) + ws.AddBalanceMultiCoin(addr, assetID1, big.NewInt(1)) + ws.AddBalanceMultiCoin(addr, assetID2, big.NewInt(2)) snapshotOpt = snapshot.WithBlockHashes(common.Hash{}, common.Hash{}) - root, err = stateDB.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) + root, err = ws.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) require.NoErrorf(t, err, "committing statedb %d", i) } - assertBalances(10, 266, 512) + assertBalances(t, ws, 10, 266, 512) // Do one more add, including the regular balance which is now in the // collapsed snapshot - stateDB, err = New(root, sdb, snapTree) + stateDB, err = state.New(root, sdb, snapTree) require.NoError(t, err, "creating statedb") - stateDB.AddBalance(addr, uint256.NewInt(1)) - stateDB.AddBalanceMultiCoin(addr, assetID1, big.NewInt(1)) + + ws = New(stateDB) + ws.AddBalance(addr, uint256.NewInt(1)) + ws.AddBalanceMultiCoin(addr, assetID1, big.NewInt(1)) snapshotOpt = snapshot.WithBlockHashes(common.Hash{}, common.Hash{}) - root, err = stateDB.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) + root, err = ws.Commit(0, false, stateconf.WithSnapshotUpdateOpts(snapshotOpt)) require.NoError(t, err, "committing statedb") - stateDB, err = New(root, sdb, snapTree) + stateDB, err = state.New(root, sdb, snapTree) require.NoError(t, err, "creating statedb") - assertBalances(11, 267, 512) + + ws = New(stateDB) + assertBalances(t, ws, 11, 267, 512) } func TestGenerateMultiCoinAccounts(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() - database := NewDatabase(diskdb) + database := state.NewDatabase(diskdb) addr := common.BytesToAddress([]byte("addr1")) addrHash := crypto.Keccak256Hash(addr[:]) @@ -124,10 +145,12 @@ func TestGenerateMultiCoinAccounts(t *testing.T) { assetID := common.BytesToHash([]byte("coin1")) assetBalance := big.NewInt(10) - stateDB, err := New(common.Hash{}, database, nil) + stateDB, err := state.New(common.Hash{}, database, nil) require.NoError(t, err, "creating statedb") - stateDB.AddBalanceMultiCoin(addr, assetID, assetBalance) - root, err := stateDB.Commit(0, false) + + ws := New(stateDB) + ws.AddBalanceMultiCoin(addr, assetID, assetBalance) + root, err := ws.Commit(0, false) require.NoError(t, err, "committing statedb") triedb := database.TrieDB() @@ -150,7 +173,7 @@ func TestGenerateMultiCoinAccounts(t *testing.T) { require.NoError(t, err, "getting account from snapshot") require.True(t, customtypes.IsMultiCoin(snapAccount), "snap account must be multi-coin") - NormalizeCoinID(&assetID) + extstate.NormalizeCoinID(&assetID) assetHash := crypto.Keccak256Hash(assetID.Bytes()) storageBytes, err := snap.Storage(addrHash, assetHash) require.NoError(t, err, "getting storage from snapshot") diff --git a/core/state/state_object_test.go b/core/state/state_object_test.go index 8aaab8decc..e6da868235 100644 --- a/core/state/state_object_test.go +++ b/core/state/state_object_test.go @@ -31,26 +31,25 @@ import ( "bytes" "testing" + "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/libevm/common" ) +var NormalizeStateKey = normalizeStateKeys{} + func TestStateObjectPartition(t *testing.T) { h1 := common.Hash{} - NormalizeCoinID(&h1) - - h2 := common.Hash{} - NormalizeStateKey(&h2) + extstate.NormalizeCoinID(&h1) + h2 := NormalizeStateKey.TransformStateKey(common.Address{}, common.Hash{}) if bytes.Equal(h1.Bytes(), h2.Bytes()) { t.Fatalf("Expected normalized hashes to be unique") } h3 := common.Hash([32]byte{0xff}) - NormalizeCoinID(&h3) - - h4 := common.Hash([32]byte{0xff}) - NormalizeStateKey(&h4) + extstate.NormalizeCoinID(&h3) + h4 := NormalizeStateKey.TransformStateKey(common.Address{}, common.Hash{0xff}) if bytes.Equal(h3.Bytes(), h4.Bytes()) { t.Fatal("Expected normalized hashes to be unqiue") } diff --git a/core/state/statedb.go b/core/state/statedb.go index 98c6c6cbd0..4b801984ff 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -29,19 +29,27 @@ package state import ( - "math/big" - - "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/utils" "github.com/ava-labs/libevm/common" - ethstate "github.com/ava-labs/libevm/core/state" - "github.com/ava-labs/libevm/libevm/stateconf" - "github.com/holiman/uint256" + "github.com/ava-labs/libevm/core/state" ) -type StateDB = ethstate.StateDB +func init() { + state.RegisterExtras(normalizeStateKeys{}) +} + +type normalizeStateKeys struct{} + +// TransformStateKey sets the 0th bit of the first byte in `key` to 0. +// This partitions normal state storage from multicoin storage. +func (normalizeStateKeys) TransformStateKey(_ common.Address, key common.Hash) common.Hash { + key[0] &= 0xfe + return key +} -var New = ethstate.New +type StateDB = state.StateDB + +var New = state.New type workerPool struct { *utils.BoundedWorkers @@ -53,63 +61,9 @@ func (wp *workerPool) Done() { wp.BoundedWorkers.Wait() } -func WithConcurrentWorkers(prefetchers int) ethstate.PrefetcherOption { +func WithConcurrentWorkers(prefetchers int) state.PrefetcherOption { pool := &workerPool{ BoundedWorkers: utils.NewBoundedWorkers(prefetchers), } - return ethstate.WithWorkerPools(func() ethstate.WorkerPool { return pool }) -} - -// Retrieve the balance from the given address or 0 if object not found -func GetBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash) *big.Int { - NormalizeCoinID(&coinID) - return s.GetState(addr, coinID, stateconf.SkipStateKeyTransformation()).Big() -} - -// AddBalance adds amount to the account associated with addr. -func AddBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash, amount *big.Int) { - if amount.Sign() == 0 { - s.AddBalance(addr, new(uint256.Int)) // used to cause touch - return - } - - if !ethstate.GetExtra(s, customtypes.IsMultiCoinPayloads, addr) { - ethstate.SetExtra(s, customtypes.IsMultiCoinPayloads, addr, true) - } - - newAmount := new(big.Int).Add(GetBalanceMultiCoin(s, addr, coinID), amount) - NormalizeCoinID(&coinID) - s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) -} - -// SubBalance subtracts amount from the account associated with addr. -func SubBalanceMultiCoin(s *ethstate.StateDB, addr common.Address, coinID common.Hash, amount *big.Int) { - if amount.Sign() == 0 { - return - } - // Note: It's not needed to set the IsMultiCoin (extras) flag here, as this - // call would always be preceded by a call to AddBalanceMultiCoin, which would - // set the extra flag. Seems we should remove the redundant code. - if !ethstate.GetExtra(s, customtypes.IsMultiCoinPayloads, addr) { - ethstate.SetExtra(s, customtypes.IsMultiCoinPayloads, addr, true) - } - newAmount := new(big.Int).Sub(GetBalanceMultiCoin(s, addr, coinID), amount) - NormalizeCoinID(&coinID) - s.SetState(addr, coinID, common.BigToHash(newAmount), stateconf.SkipStateKeyTransformation()) -} - -// NormalizeCoinID ORs the 0th bit of the first byte in -// `coinID`, which ensures this bit will be 1 and all other -// bits are left the same. -// This partitions multicoin storage from normal state storage. -func NormalizeCoinID(coinID *common.Hash) { - coinID[0] |= 0x01 -} - -// NormalizeStateKey ANDs the 0th bit of the first byte in -// `key`, which ensures this bit will be 0 and all other bits -// are left the same. -// This partitions normal state storage from multicoin storage. -func NormalizeStateKey(key *common.Hash) { - key[0] &= 0xfe + return state.WithWorkerPools(func() state.WorkerPool { return pool }) } diff --git a/nativeasset/contract_test.go b/nativeasset/contract_test.go index 90ad257ec5..4f79559d50 100644 --- a/nativeasset/contract_test.go +++ b/nativeasset/contract_test.go @@ -7,6 +7,7 @@ import ( "math/big" "testing" + "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/core/state" . "github.com/ava-labs/coreth/nativeasset" "github.com/ava-labs/coreth/params" @@ -22,11 +23,6 @@ import ( _ "github.com/ava-labs/coreth/core" ) -type stateDB interface { - vm.StateDB - GetBalanceMultiCoin(common.Address, common.Hash) *big.Int -} - // CanTransfer checks whether there are enough funds in the address' account to make a transfer. // This does not take the necessary gas in to account to make the transfer valid. func CanTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool { @@ -67,7 +63,7 @@ func TestStatefulPrecompile(t *testing.T) { } type statefulContractTest struct { - setupStateDB func() stateDB + setupStateDB func() *state.StateDB from common.Address precompileAddr common.Address input []byte @@ -77,7 +73,7 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr error expectedResult []byte name string - stateDBCheck func(*testing.T, stateDB) + stateDBCheck func(*testing.T, *state.StateDB) } userAddr1 := common.BytesToAddress([]byte("user1")) @@ -95,11 +91,12 @@ func TestStatefulPrecompile(t *testing.T) { tests := []statefulContractTest{ { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee @@ -119,19 +116,21 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: uninitialized multicoin balance returns 0", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee statedb.SetBalance(userAddr1, u256Hundred) // Initialize multicoin balance and set it back to 0 - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.SubBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.SubBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -145,18 +144,20 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: initialized multicoin balance returns 0", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee statedb.SetBalance(userAddr1, u256Hundred) // Initialize multicoin balance to 100 - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -170,7 +171,7 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: returns correct non-zero multicoin balance", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) @@ -188,7 +189,7 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: invalid input data reverts", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) @@ -206,7 +207,7 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: insufficient gas errors", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) @@ -224,14 +225,16 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset balance: non-zero value with insufficient funds reverts before running pre-compile", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -243,11 +246,12 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr: nil, expectedResult: nil, name: "native asset call: multicoin transfer", - stateDBCheck: func(t *testing.T, stateDB stateDB) { + stateDBCheck: func(t *testing.T, stateDB *state.StateDB) { user1Balance := stateDB.GetBalance(userAddr1) user2Balance := stateDB.GetBalance(userAddr2) - user1AssetBalance := stateDB.GetBalanceMultiCoin(userAddr1, assetID) - user2AssetBalance := stateDB.GetBalanceMultiCoin(userAddr2, assetID) + sdb := extstate.New(stateDB) + user1AssetBalance := sdb.GetBalanceMultiCoin(userAddr1, assetID) + user2AssetBalance := sdb.GetBalanceMultiCoin(userAddr2, assetID) expectedBalance := big.NewInt(50) assert.Equal(t, u256Hundred, user1Balance, "user 1 balance") @@ -257,14 +261,16 @@ func TestStatefulPrecompile(t *testing.T) { }, }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -276,12 +282,13 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr: nil, expectedResult: nil, name: "native asset call: multicoin transfer with non-zero value", - stateDBCheck: func(t *testing.T, stateDB stateDB) { + stateDBCheck: func(t *testing.T, stateDB *state.StateDB) { user1Balance := stateDB.GetBalance(userAddr1) user2Balance := stateDB.GetBalance(userAddr2) nativeAssetCallAddrBalance := stateDB.GetBalance(NativeAssetCallAddr) - user1AssetBalance := stateDB.GetBalanceMultiCoin(userAddr1, assetID) - user2AssetBalance := stateDB.GetBalanceMultiCoin(userAddr2, assetID) + sdb := extstate.New(stateDB) + user1AssetBalance := sdb.GetBalanceMultiCoin(userAddr1, assetID) + user2AssetBalance := sdb.GetBalanceMultiCoin(userAddr2, assetID) expectedBalance := big.NewInt(50) assert.Equal(t, uint256.NewInt(51), user1Balance, "user 1 balance") @@ -292,14 +299,16 @@ func TestStatefulPrecompile(t *testing.T) { }, }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -311,11 +320,12 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr: vm.ErrInsufficientBalance, expectedResult: nil, name: "native asset call: insufficient multicoin funds", - stateDBCheck: func(t *testing.T, stateDB stateDB) { + stateDBCheck: func(t *testing.T, stateDB *state.StateDB) { user1Balance := stateDB.GetBalance(userAddr1) user2Balance := stateDB.GetBalance(userAddr2) - user1AssetBalance := stateDB.GetBalanceMultiCoin(userAddr1, assetID) - user2AssetBalance := stateDB.GetBalanceMultiCoin(userAddr2, assetID) + sdb := extstate.New(stateDB) + user1AssetBalance := sdb.GetBalanceMultiCoin(userAddr1, assetID) + user2AssetBalance := sdb.GetBalanceMultiCoin(userAddr2, assetID) assert.Equal(t, bigHundred, user1Balance, "user 1 balance") assert.Equal(t, big0, user2Balance, "user 2 balance") @@ -324,14 +334,16 @@ func TestStatefulPrecompile(t *testing.T) { }, }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, uint256.NewInt(50)) - statedb.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -343,11 +355,12 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr: vm.ErrInsufficientBalance, expectedResult: nil, name: "native asset call: insufficient funds", - stateDBCheck: func(t *testing.T, stateDB stateDB) { + stateDBCheck: func(t *testing.T, stateDB *state.StateDB) { user1Balance := stateDB.GetBalance(userAddr1) user2Balance := stateDB.GetBalance(userAddr2) - user1AssetBalance := stateDB.GetBalanceMultiCoin(userAddr1, assetID) - user2AssetBalance := stateDB.GetBalanceMultiCoin(userAddr2, assetID) + sdb := extstate.New(stateDB) + user1AssetBalance := sdb.GetBalanceMultiCoin(userAddr1, assetID) + user2AssetBalance := sdb.GetBalanceMultiCoin(userAddr2, assetID) assert.Equal(t, big.NewInt(50), user1Balance, "user 1 balance") assert.Equal(t, big0, user2Balance, "user 2 balance") @@ -356,14 +369,16 @@ func TestStatefulPrecompile(t *testing.T) { }, }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -377,14 +392,16 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset call: insufficient gas for native asset call", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -396,11 +413,12 @@ func TestStatefulPrecompile(t *testing.T) { expectedErr: vm.ErrOutOfGas, expectedResult: nil, name: "native asset call: insufficient gas to create new account", - stateDBCheck: func(t *testing.T, stateDB stateDB) { + stateDBCheck: func(t *testing.T, stateDB *state.StateDB) { user1Balance := stateDB.GetBalance(userAddr1) user2Balance := stateDB.GetBalance(userAddr2) - user1AssetBalance := stateDB.GetBalanceMultiCoin(userAddr1, assetID) - user2AssetBalance := stateDB.GetBalanceMultiCoin(userAddr2, assetID) + sdb := extstate.New(stateDB) + user1AssetBalance := sdb.GetBalanceMultiCoin(userAddr1, assetID) + user2AssetBalance := sdb.GetBalanceMultiCoin(userAddr2, assetID) assert.Equal(t, bigHundred, user1Balance, "user 1 balance") assert.Equal(t, big0, user2Balance, "user 2 balance") @@ -409,14 +427,16 @@ func TestStatefulPrecompile(t *testing.T) { }, }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, @@ -430,14 +450,16 @@ func TestStatefulPrecompile(t *testing.T) { name: "native asset call: invalid input", }, { - setupStateDB: func() stateDB { + setupStateDB: func() *state.StateDB { statedb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) if err != nil { t.Fatal(err) } + statedb.SetBalance(userAddr1, u256Hundred) - statedb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) - statedb.Finalise(true) + sdb := extstate.New(statedb) + sdb.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) + sdb.Finalise(true) return statedb }, from: userAddr1, diff --git a/plugin/evm/atomic/atomictest/tx.go b/plugin/evm/atomic/atomictest/tx.go index 0a7419756d..76085bb8f2 100644 --- a/plugin/evm/atomic/atomictest/tx.go +++ b/plugin/evm/atomic/atomictest/tx.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" ) @@ -89,7 +90,7 @@ func (t *TestUnsignedTx) Visit(v atomic.Visitor) error { } // EVMStateTransfer implements the UnsignedAtomicTx interface -func (t *TestUnsignedTx) EVMStateTransfer(ctx *snow.Context, state atomic.StateDB) error { +func (t *TestUnsignedTx) EVMStateTransfer(ctx *snow.Context, state *state.StateDB) error { return t.EVMStateTransferV } diff --git a/plugin/evm/atomic/export_tx.go b/plugin/evm/atomic/export_tx.go index 911e0b8bc2..76cec9d19c 100644 --- a/plugin/evm/atomic/export_tx.go +++ b/plugin/evm/atomic/export_tx.go @@ -9,6 +9,8 @@ import ( "fmt" "math/big" + "github.com/ava-labs/coreth/core/extstate" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap0" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap5" @@ -219,7 +221,7 @@ func (utx *UnsignedExportTx) AtomicOps() (ids.ID, *atomic.Requests, error) { func NewExportTx( ctx *snow.Context, rules extras.Rules, - state StateDB, + stateDB *state.StateDB, assetID ids.ID, // AssetID of the tokens to export amount uint64, // Amount of tokens to export chainID ids.ID, // Chain to send the UTXOs to @@ -248,7 +250,7 @@ func NewExportTx( // consume non-AVAX if assetID != ctx.AVAXAssetID { - ins, signers, err = getSpendableFunds(ctx, state, keys, assetID, amount) + ins, signers, err = getSpendableFunds(ctx, stateDB, keys, assetID, amount) if err != nil { return nil, fmt.Errorf("couldn't generate tx inputs/signers: %w", err) } @@ -276,14 +278,14 @@ func NewExportTx( return nil, err } - avaxIns, avaxSigners, err = getSpendableAVAXWithFee(ctx, state, keys, avaxNeeded, cost, baseFee) + avaxIns, avaxSigners, err = getSpendableAVAXWithFee(ctx, stateDB, keys, avaxNeeded, cost, baseFee) default: var newAvaxNeeded uint64 newAvaxNeeded, err = math.Add64(avaxNeeded, ap0.AtomicTxFee) if err != nil { return nil, errOverflowExport } - avaxIns, avaxSigners, err = getSpendableFunds(ctx, state, keys, ctx.AVAXAssetID, newAvaxNeeded) + avaxIns, avaxSigners, err = getSpendableFunds(ctx, stateDB, keys, ctx.AVAXAssetID, newAvaxNeeded) } if err != nil { return nil, fmt.Errorf("couldn't generate tx inputs/signers: %w", err) @@ -310,7 +312,8 @@ func NewExportTx( } // EVMStateTransfer executes the state update from the atomic export transaction -func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) error { +func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state.StateDB) error { + ws := extstate.New(stateDB) addrs := map[[20]byte]uint64{} for _, from := range utx.Ins { if from.AssetID == ctx.AVAXAssetID { @@ -321,25 +324,25 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) uint256.NewInt(from.Amount), uint256.NewInt(X2CRate.Uint64()), ) - if state.GetBalance(from.Address).Cmp(amount) < 0 { + if ws.GetBalance(from.Address).Cmp(amount) < 0 { return errInsufficientFunds } - state.SubBalance(from.Address, amount) + ws.SubBalance(from.Address, amount) } else { log.Debug("export_tx", "dest", utx.DestinationChain, "addr", from.Address, "amount", from.Amount, "assetID", from.AssetID) amount := new(big.Int).SetUint64(from.Amount) - if state.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { + if ws.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { return errInsufficientFunds } - state.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) + ws.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) } - if state.GetNonce(from.Address) != from.Nonce { + if ws.GetNonce(from.Address) != from.Nonce { return errInvalidNonce } addrs[from.Address] = from.Nonce } for addr, nonce := range addrs { - state.SetNonce(addr, nonce+1) + ws.SetNonce(addr, nonce+1) } return nil } @@ -351,11 +354,12 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) // [tx.Sign] which supports multiple keys on a single input. func getSpendableFunds( ctx *snow.Context, - state StateDB, + stateDB *state.StateDB, keys []*secp256k1.PrivateKey, assetID ids.ID, amount uint64, ) ([]EVMInput, [][]*secp256k1.PrivateKey, error) { + ws := extstate.New(stateDB) inputs := []EVMInput{} signers := [][]*secp256k1.PrivateKey{} // Note: we assume that each key in [keys] is unique, so that iterating over @@ -369,9 +373,9 @@ func getSpendableFunds( if assetID == ctx.AVAXAssetID { // If the asset is AVAX, we divide by the x2cRate to convert back to the correct // denomination of AVAX that can be exported. - balance = new(uint256.Int).Div(state.GetBalance(addr), X2CRate).Uint64() + balance = new(uint256.Int).Div(ws.GetBalance(addr), X2CRate).Uint64() } else { - balance = state.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() + balance = ws.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() } if balance == 0 { continue @@ -379,7 +383,7 @@ func getSpendableFunds( if amount < balance { balance = amount } - nonce := state.GetNonce(addr) + nonce := ws.GetNonce(addr) inputs = append(inputs, EVMInput{ Address: addr, @@ -408,7 +412,7 @@ func getSpendableFunds( // [tx.Sign] which supports multiple keys on a single input. func getSpendableAVAXWithFee( ctx *snow.Context, - state StateDB, + stateDB *state.StateDB, keys []*secp256k1.PrivateKey, amount uint64, cost uint64, @@ -450,7 +454,7 @@ func getSpendableAVAXWithFee( addr := key.EthAddress() // Since the asset is AVAX, we divide by the x2cRate to convert back to // the correct denomination of AVAX that can be exported. - balance := new(uint256.Int).Div(state.GetBalance(addr), X2CRate).Uint64() + balance := new(uint256.Int).Div(stateDB.GetBalance(addr), X2CRate).Uint64() // If the balance for [addr] is insufficient to cover the additional cost // of adding an input to the transaction, skip adding the input altogether if balance <= additionalFee { @@ -473,7 +477,7 @@ func getSpendableAVAXWithFee( if amount < balance { inputAmount = amount } - nonce := state.GetNonce(addr) + nonce := stateDB.GetNonce(addr) inputs = append(inputs, EVMInput{ Address: addr, diff --git a/plugin/evm/atomic/import_tx.go b/plugin/evm/atomic/import_tx.go index 5adf697a39..ad32217b6e 100644 --- a/plugin/evm/atomic/import_tx.go +++ b/plugin/evm/atomic/import_tx.go @@ -10,6 +10,8 @@ import ( "math/big" "slices" + "github.com/ava-labs/coreth/core/extstate" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap0" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap5" @@ -332,18 +334,19 @@ func NewImportTx( // EVMStateTransfer performs the state transfer to increase the balances of // accounts accordingly with the imported EVMOutputs -func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) error { +func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state.StateDB) error { + ws := extstate.New(stateDB) for _, to := range utx.Outs { if to.AssetID == ctx.AVAXAssetID { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", "AVAX") // If the asset is AVAX, convert the input amount in nAVAX to gWei by // multiplying by the x2c rate. amount := new(uint256.Int).Mul(uint256.NewInt(to.Amount), X2CRate) - state.AddBalance(to.Address, amount) + ws.AddBalance(to.Address, amount) } else { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", to.AssetID) amount := new(big.Int).SetUint64(to.Amount) - state.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) + ws.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) } } return nil diff --git a/plugin/evm/atomic/tx.go b/plugin/evm/atomic/tx.go index bfecba4a21..d4c33451c8 100644 --- a/plugin/evm/atomic/tx.go +++ b/plugin/evm/atomic/tx.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/holiman/uint256" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/avalanchego/chains/atomic" @@ -137,20 +138,6 @@ type UnsignedTx interface { SignedBytes() []byte } -type StateDB interface { - AddBalance(common.Address, *uint256.Int) - AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) - - SubBalance(common.Address, *uint256.Int) - SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) - - GetBalance(common.Address) *uint256.Int - GetBalanceMultiCoin(common.Address, common.Hash) *big.Int - - GetNonce(common.Address) uint64 - SetNonce(common.Address, uint64) -} - // UnsignedAtomicTx is an unsigned operation that can be atomically accepted type UnsignedAtomicTx interface { UnsignedTx @@ -168,7 +155,7 @@ type UnsignedAtomicTx interface { // The set of atomic requests must be returned in a consistent order. AtomicOps() (ids.ID, *atomic.Requests, error) - EVMStateTransfer(ctx *snow.Context, state StateDB) error + EVMStateTransfer(ctx *snow.Context, state *state.StateDB) error } // Tx is a signed transaction diff --git a/plugin/evm/export_tx_test.go b/plugin/evm/export_tx_test.go index 38fec8449d..2d486ae6a8 100644 --- a/plugin/evm/export_tx_test.go +++ b/plugin/evm/export_tx_test.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" atomicvm "github.com/ava-labs/coreth/plugin/evm/atomic/vm" @@ -437,14 +438,15 @@ func TestExportTxEVMStateTransfer(t *testing.T) { t.Fatalf("address balance %s equal %s not %s", addr.String(), avaxBalance, test.avaxBalance) } + ws := extstate.New(stateDB) for assetID, expectedBalance := range test.balances { - balance := stateDB.GetBalanceMultiCoin(ethAddr, common.Hash(assetID)) + balance := ws.GetBalanceMultiCoin(ethAddr, common.Hash(assetID)) if avaxBalance.Cmp(test.avaxBalance) != 0 { t.Fatalf("%s address balance %s equal %s not %s", assetID, addr.String(), balance, expectedBalance) } } - if stateDB.GetNonce(ethAddr) != test.expectedNonce { + if ws.GetNonce(ethAddr) != test.expectedNonce { t.Fatalf("failed to set nonce to %d", test.expectedNonce) } }) @@ -1957,12 +1959,12 @@ func TestNewExportTxMulticoin(t *testing.T) { t.Fatal(err) } - state, err := tvm.vm.blockChain.State() + stateDB, err := tvm.vm.blockChain.State() if err != nil { t.Fatal(err) } - tx, err = atomic.NewExportTx(tvm.vm.ctx, tvm.vm.currentRules(), state, tid, exportAmount, tvm.vm.ctx.XChainID, exportId, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]}) + tx, err = atomic.NewExportTx(tvm.vm.ctx, tvm.vm.currentRules(), stateDB, tid, exportAmount, tvm.vm.ctx.XChainID, exportId, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]}) if err != nil { t.Fatal(err) } @@ -2000,8 +2002,9 @@ func TestNewExportTxMulticoin(t *testing.T) { if stdb.GetBalance(addr).Cmp(uint256.NewInt(test.bal*units.Avax)) != 0 { t.Fatalf("address balance %s equal %s not %s", addr.String(), stdb.GetBalance(addr), new(big.Int).SetUint64(test.bal*units.Avax)) } - if stdb.GetBalanceMultiCoin(addr, common.BytesToHash(tid[:])).Cmp(new(big.Int).SetUint64(test.balmc)) != 0 { - t.Fatalf("address balance multicoin %s equal %s not %s", addr.String(), stdb.GetBalanceMultiCoin(addr, common.BytesToHash(tid[:])), new(big.Int).SetUint64(test.balmc)) + ws := extstate.New(stdb) + if ws.GetBalanceMultiCoin(addr, common.BytesToHash(tid[:])).Cmp(new(big.Int).SetUint64(test.balmc)) != 0 { + t.Fatalf("address balance multicoin %s equal %s not %s", addr.String(), ws.GetBalanceMultiCoin(addr, common.BytesToHash(tid[:])), new(big.Int).SetUint64(test.balmc)) } }) } diff --git a/plugin/evm/import_tx_test.go b/plugin/evm/import_tx_test.go index d5991cf948..d5d45eed11 100644 --- a/plugin/evm/import_tx_test.go +++ b/plugin/evm/import_tx_test.go @@ -7,6 +7,7 @@ import ( "math/big" "testing" + "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/plugin/evm/atomic" atomicvm "github.com/ava-labs/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap0" @@ -1276,7 +1277,8 @@ func TestImportTxEVMStateTransfer(t *testing.T) { t.Fatal(err) } - assetBalance := sdb.GetBalanceMultiCoin(testEthAddrs[0], common.Hash(assetID)) + ws := extstate.New(sdb) + assetBalance := ws.GetBalanceMultiCoin(testEthAddrs[0], common.Hash(assetID)) if assetBalance.Cmp(common.Big1) != 0 { t.Fatalf("Expected asset balance to be %d, found balance: %d", common.Big1, assetBalance) } diff --git a/precompile/contract/interfaces.go b/precompile/contract/interfaces.go index 5ec7f901a7..21f1c068cb 100644 --- a/precompile/contract/interfaces.go +++ b/precompile/contract/interfaces.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/libevm/common" ethtypes "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/holiman/uint256" ) @@ -23,17 +24,18 @@ type StatefulPrecompiledContract interface { // StateDB is the interface for accessing EVM state type StateDB interface { - GetState(common.Address, common.Hash) common.Hash - SetState(common.Address, common.Hash, common.Hash) + GetState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash + SetState(common.Address, common.Hash, common.Hash, ...stateconf.StateDBStateOption) SetNonce(common.Address, uint64) GetNonce(common.Address) uint64 GetBalance(common.Address) *uint256.Int AddBalance(common.Address, *uint256.Int) - GetBalanceMultiCoin(common.Address, common.Hash) *big.Int - AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) - SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) + + GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int + AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) + SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) CreateAccount(common.Address) Exist(common.Address) bool From aaaa838f5fb42a7cea821849efe396a2df7115f8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 14:06:00 -0400 Subject: [PATCH 14/45] implement construct block from block --- go.mod | 1 + go.sum | 2 - sae/hooks.go | 116 ++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 6458ef8dad..4705e77035 100644 --- a/go.mod +++ b/go.mod @@ -180,4 +180,5 @@ require ( ) replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm + replace github.com/ava-labs/libevm => /Users/stephen/go/src/github.com/ava-labs/libevm diff --git a/go.sum b/go.sum index 3a54af0e76..c39da4c820 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,6 @@ github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d h1:+IK0mMg github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d/go.mod h1:QmwzzCZtKaam5OY45kuzq3UinsyRXH75LkP85W7713M= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.8 h1:f0ZbAiRE1srMiv/0DuXvPQZwgYbLC9OgAWbQUCMebTE= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.8/go.mod h1:j6spQFNSBAfcXKt9g0xbObW/8tMlGP4bFjPIsJmDg/o= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 h1:vBMYo+Iazw0rGTr+cwjkBdh5eadLPlv4ywI4lKye3CA= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= 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/sae/hooks.go b/sae/hooks.go index 057159c210..59e15a99ca 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -63,6 +63,69 @@ func (h *hooks) ConstructBlock( state hook.State, txs []*types.Transaction, receipts []*types.Receipt, +) (*types.Block, error) { + return h.constructBlock( + ctx, + blockContext, + header, + parent, + ancestors, + state, + txs, + receipts, + h.mempool, + ) +} + +func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { + return nil // TODO: Implement me +} + +func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { + atomicTxs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(b), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + atomicTxSlice := txSlice(atomicTxs) + return func( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, + ) (*types.Block, error) { + return h.constructBlock( + ctx, + blockContext, + header, + parent, + ancestors, + state, + txs, + receipts, + &atomicTxSlice, + ) + }, nil +} + +func (h *hooks) constructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, + potentialAtomicTxs txs, ) (*types.Block, error) { ancestorInputUTXOs, err := inputUTXOs(ancestors) if err != nil { @@ -76,7 +139,7 @@ func (h *hooks) ConstructBlock( h.ctx.AVAXAssetID, header.BaseFee, ancestorInputUTXOs, - h.mempool, + potentialAtomicTxs, ) if err != nil { return nil, err @@ -98,7 +161,7 @@ func (h *hooks) ConstructBlock( h.ctx.Log.Debug("discarding txs due to error marshaling atomic transactions", zap.Error(err), ) - h.mempool.DiscardCurrentTxs() + potentialAtomicTxs.DiscardCurrentTxs() return nil, fmt.Errorf("failed to marshal batch of atomic transactions due to %w", err) } @@ -139,17 +202,6 @@ func (h *hooks) ConstructBlock( ), nil } -func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { - return nil // TODO: Implement me -} - -func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { - return func(context.Context, *block.Context, *types.Header, *types.Header, iter.Seq[*types.Block], hook.State, []*types.Transaction, []*types.Receipt) (*types.Block, error) { - // TODO: Implement me - return b, nil - }, nil -} - func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { txs, err := atomic.ExtractAtomicTxs( customtypes.BlockExtData(block), @@ -192,6 +244,28 @@ func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { return inputUTXOs, nil } +type txs interface { + NextTx() (*atomic.Tx, bool) + CancelCurrentTx(txID ids.ID) + DiscardCurrentTx(txID ids.ID) + DiscardCurrentTxs() +} + +type txSlice []*atomic.Tx + +func (t *txSlice) NextTx() (*atomic.Tx, bool) { + if len(*t) == 0 { + return nil, false + } + tx := (*t)[0] + *t = (*t)[1:] + return tx, true +} + +func (*txSlice) CancelCurrentTx(ids.ID) {} +func (*txSlice) DiscardCurrentTx(ids.ID) {} +func (*txSlice) DiscardCurrentTxs() {} + func packAtomicTxs( ctx context.Context, log logging.Logger, @@ -199,14 +273,14 @@ func packAtomicTxs( avaxAssetID ids.ID, baseFee *big.Int, ancestorInputUTXOs set.Set[ids.ID], - mempool *atomictxpool.Txs, + txs txs, ) ([]*atomic.Tx, error) { var ( cumulativeSize int atomicTxs []*atomic.Tx ) for { - tx, exists := mempool.NextTx() + tx, exists := txs.NextTx() if !exists { break } @@ -215,7 +289,7 @@ func packAtomicTxs( // soft limit. txSize := len(tx.SignedBytes()) if cumulativeSize+txSize > targetAtomicTxsSize { - mempool.CancelCurrentTx(tx.ID()) + txs.CancelCurrentTx(tx.ID()) break } @@ -231,20 +305,20 @@ func packAtomicTxs( log.Debug("discarding tx due to overlapping input utxos", zap.Stringer("txID", txID), ) - mempool.DiscardCurrentTx(txID) + txs.DiscardCurrentTx(txID) continue } op, err := atomicTxOp(tx, avaxAssetID, baseFee) if err != nil { - mempool.DiscardCurrentTx(tx.ID()) + txs.DiscardCurrentTx(tx.ID()) continue } err = state.Apply(op) if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { // Send [tx] back to the mempool's tx heap. - mempool.CancelCurrentTx(tx.ID()) + txs.CancelCurrentTx(tx.ID()) break } if err != nil { @@ -253,7 +327,7 @@ func packAtomicTxs( zap.Stringer("txID", txID), zap.Error(err), ) - mempool.DiscardCurrentTx(txID) + txs.DiscardCurrentTx(txID) continue } @@ -345,6 +419,8 @@ func calculatePredicateResults( ProposerVMBlockCtx: blockContext, } predicateResults := predicate.NewResults() + // TODO: Each transaction's predicates should be able to be calculated + // concurrently. for _, tx := range txs { results, err := core.CheckPredicates(rules, &predicateContext, tx) if err != nil { From 579284400e37b903e313ca187941affde57ba6ca Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 14:10:56 -0400 Subject: [PATCH 15/45] cleanup --- sae/hooks.go | 139 ---------------------------------------------- sae/predicates.go | 40 +++++++++++++ sae/txs.go | 132 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 139 deletions(-) create mode 100644 sae/predicates.go create mode 100644 sae/txs.go diff --git a/sae/hooks.go b/sae/hooks.go index 59e15a99ca..89e887d518 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -17,19 +17,14 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" - "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" - "github.com/ava-labs/coreth/precompile/precompileconfig" - "github.com/ava-labs/coreth/predicate" - "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/trie" "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/worstcase" - "github.com/holiman/uint256" "go.uber.org/zap" atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" @@ -224,48 +219,6 @@ func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([ return ops, nil } -func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { - var inputUTXOs set.Set[ids.ID] - for block := range blocks { - // Extract atomic transactions from the block - txs, err := atomic.ExtractAtomicTxs( - customtypes.BlockExtData(block), - true, - atomic.Codec, - ) - if err != nil { - return nil, err - } - - for _, tx := range txs { - inputUTXOs.Union(tx.InputUTXOs()) - } - } - return inputUTXOs, nil -} - -type txs interface { - NextTx() (*atomic.Tx, bool) - CancelCurrentTx(txID ids.ID) - DiscardCurrentTx(txID ids.ID) - DiscardCurrentTxs() -} - -type txSlice []*atomic.Tx - -func (t *txSlice) NextTx() (*atomic.Tx, bool) { - if len(*t) == 0 { - return nil, false - } - tx := (*t)[0] - *t = (*t)[1:] - return tx, true -} - -func (*txSlice) CancelCurrentTx(ids.ID) {} -func (*txSlice) DiscardCurrentTx(ids.ID) {} -func (*txSlice) DiscardCurrentTxs() {} - func packAtomicTxs( ctx context.Context, log logging.Logger, @@ -338,95 +291,3 @@ func packAtomicTxs( } return atomicTxs, nil } - -func atomicTxOp( - tx *atomic.Tx, - avaxAssetID ids.ID, - baseFee *big.Int, -) (hook.Op, error) { - // Note: we do not need to check if we are in at least ApricotPhase4 here - // because we assume that this function will only be called when the block - // is in at least ApricotPhase5. - gasUsed, err := tx.GasUsed(true) - if err != nil { - return hook.Op{}, err - } - burned, err := tx.Burned(avaxAssetID) - if err != nil { - return hook.Op{}, err - } - - var bigGasUsed uint256.Int - bigGasUsed.SetUint64(gasUsed) - - var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed - gasPrice.SetUint64(burned) - gasPrice.Mul(&gasPrice, atomic.X2CRate) - gasPrice.Div(&gasPrice, &bigGasUsed) - - op := hook.Op{ - Gas: gas.Gas(gasUsed), - GasPrice: gasPrice, - } - switch tx := tx.UnsignedAtomicTx.(type) { - case *atomic.UnsignedImportTx: - op.To = make(map[common.Address]uint256.Int) - for _, output := range tx.Outs { - if output.AssetID != avaxAssetID { - continue - } - var amount uint256.Int - amount.SetUint64(output.Amount) - amount.Mul(&amount, atomic.X2CRate) - op.To[output.Address] = amount - } - case *atomic.UnsignedExportTx: - op.From = make(map[common.Address]hook.Account) - for _, input := range tx.Ins { - if input.AssetID != avaxAssetID { - continue - } - var amount uint256.Int - amount.SetUint64(input.Amount) - amount.Mul(&amount, atomic.X2CRate) - op.From[input.Address] = hook.Account{ - Nonce: input.Nonce, - Amount: amount, - } - } - default: - return hook.Op{}, fmt.Errorf("unexpected atomic tx type: %T", tx) - } - return op, nil -} - -func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { - if len(txs) == 0 { - return nil, nil - } - return atomic.Codec.Marshal(atomic.CodecVersion, txs) -} - -func calculatePredicateResults( - ctx context.Context, - snowContext *snow.Context, - rules params.Rules, - blockContext *block.Context, - txs []*types.Transaction, -) (*predicate.Results, error) { - predicateContext := precompileconfig.PredicateContext{ - SnowCtx: snowContext, - ProposerVMBlockCtx: blockContext, - } - predicateResults := predicate.NewResults() - // TODO: Each transaction's predicates should be able to be calculated - // concurrently. - for _, tx := range txs { - results, err := core.CheckPredicates(rules, &predicateContext, tx) - if err != nil { - return nil, err - } - predicateResults.SetTxResults(tx.Hash(), results) - } - return predicateResults, nil -} diff --git a/sae/predicates.go b/sae/predicates.go new file mode 100644 index 0000000000..0285a691ff --- /dev/null +++ b/sae/predicates.go @@ -0,0 +1,40 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/precompile/precompileconfig" + "github.com/ava-labs/coreth/predicate" + "github.com/ava-labs/libevm/core/types" +) + +func calculatePredicateResults( + ctx context.Context, + snowContext *snow.Context, + rules params.Rules, + blockContext *block.Context, + txs []*types.Transaction, +) (*predicate.Results, error) { + predicateContext := precompileconfig.PredicateContext{ + SnowCtx: snowContext, + ProposerVMBlockCtx: blockContext, + } + predicateResults := predicate.NewResults() + // TODO: Each transaction's predicates should be able to be calculated + // concurrently. + for _, tx := range txs { + results, err := core.CheckPredicates(rules, &predicateContext, tx) + if err != nil { + return nil, err + } + predicateResults.SetTxResults(tx.Hash(), results) + } + return predicateResults, nil +} diff --git a/sae/txs.go b/sae/txs.go new file mode 100644 index 0000000000..2bfb622576 --- /dev/null +++ b/sae/txs.go @@ -0,0 +1,132 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "fmt" + "iter" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/coreth/plugin/evm/atomic" + "github.com/ava-labs/coreth/plugin/evm/customtypes" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/strevm/hook" + "github.com/holiman/uint256" +) + +type txs interface { + NextTx() (*atomic.Tx, bool) + CancelCurrentTx(txID ids.ID) + DiscardCurrentTx(txID ids.ID) + DiscardCurrentTxs() +} + +type txSlice []*atomic.Tx + +func (t *txSlice) NextTx() (*atomic.Tx, bool) { + if len(*t) == 0 { + return nil, false + } + tx := (*t)[0] + *t = (*t)[1:] + return tx, true +} + +func (*txSlice) CancelCurrentTx(ids.ID) {} +func (*txSlice) DiscardCurrentTx(ids.ID) {} +func (*txSlice) DiscardCurrentTxs() {} + +// inputUTXOs returns the set of all UTXOIDs consumed by atomic txs in the +// iterator. +func inputUTXOs(blocks iter.Seq[*types.Block]) (set.Set[ids.ID], error) { + var inputUTXOs set.Set[ids.ID] + for block := range blocks { + // Extract atomic transactions from the block + txs, err := atomic.ExtractAtomicTxs( + customtypes.BlockExtData(block), + true, + atomic.Codec, + ) + if err != nil { + return nil, err + } + + for _, tx := range txs { + inputUTXOs.Union(tx.InputUTXOs()) + } + } + return inputUTXOs, nil +} + +func atomicTxOp( + tx *atomic.Tx, + avaxAssetID ids.ID, + baseFee *big.Int, +) (hook.Op, error) { + // Note: we do not need to check if we are in at least ApricotPhase4 here + // because we assume that this function will only be called when the block + // is in at least ApricotPhase5. + gasUsed, err := tx.GasUsed(true) + if err != nil { + return hook.Op{}, err + } + burned, err := tx.Burned(avaxAssetID) + if err != nil { + return hook.Op{}, err + } + + var bigGasUsed uint256.Int + bigGasUsed.SetUint64(gasUsed) + + var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed + gasPrice.SetUint64(burned) + gasPrice.Mul(&gasPrice, atomic.X2CRate) + gasPrice.Div(&gasPrice, &bigGasUsed) + + op := hook.Op{ + Gas: gas.Gas(gasUsed), + GasPrice: gasPrice, + } + switch tx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + op.To = make(map[common.Address]uint256.Int) + for _, output := range tx.Outs { + if output.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(output.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.To[output.Address] = amount + } + case *atomic.UnsignedExportTx: + op.From = make(map[common.Address]hook.Account) + for _, input := range tx.Ins { + if input.AssetID != avaxAssetID { + continue + } + var amount uint256.Int + amount.SetUint64(input.Amount) + amount.Mul(&amount, atomic.X2CRate) + op.From[input.Address] = hook.Account{ + Nonce: input.Nonce, + Amount: amount, + } + } + default: + return hook.Op{}, fmt.Errorf("unexpected atomic tx type: %T", tx) + } + return op, nil +} + +func marshalAtomicTxs(txs []*atomic.Tx) ([]byte, error) { + if len(txs) == 0 { + return nil, nil + } + return atomic.Codec.Marshal(atomic.CodecVersion, txs) +} From 2317ad6c760f8ba923f5d712992f639f9e84440a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 14:29:36 -0400 Subject: [PATCH 16/45] nits --- sae/hooks.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sae/hooks.go b/sae/hooks.go index 89e887d518..22544996f4 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -73,7 +73,10 @@ func (h *hooks) ConstructBlock( } func (h *hooks) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { - return nil // TODO: Implement me + // TODO: Write warp information + // TODO: Apply atomic txs to shared memory + // TODO: Update last executed height to support restarts + return nil } func (h *hooks) ConstructBlockFromBlock(ctx context.Context, b *types.Block) (hook.ConstructBlock, error) { @@ -262,6 +265,12 @@ func packAtomicTxs( continue } + // TODO: The transaction signatures must be verified and the import + // UTXOs must be read from shared memory here. + + // The atomicTxOp will verify that export txs have sufficient funds and + // utilize proper nonces. It additionally enforces that sufficient fees + // are paid by the transactions. op, err := atomicTxOp(tx, avaxAssetID, baseFee) if err != nil { txs.DiscardCurrentTx(tx.ID()) From 832b29bf9a9c1f4a2046a2e75af0d88c199c431b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 15:23:11 -0400 Subject: [PATCH 17/45] wip --- plugin/evm/atomic/vm/tx_semantic_verifier.go | 275 ++++++++++++------- 1 file changed, 174 insertions(+), 101 deletions(-) diff --git a/plugin/evm/atomic/vm/tx_semantic_verifier.go b/plugin/evm/atomic/vm/tx_semantic_verifier.go index c47d20fbd8..6c73dc9ac0 100644 --- a/plugin/evm/atomic/vm/tx_semantic_verifier.go +++ b/plugin/evm/atomic/vm/tx_semantic_verifier.go @@ -9,11 +9,13 @@ import ( "fmt" "math/big" + avagoatomic "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/coreth/params/extras" @@ -78,86 +80,24 @@ type semanticVerifier struct { // ImportTx verifies this transaction is valid. func (s *semanticVerifier) ImportTx(utx *atomic.UnsignedImportTx) error { - backend := s.backend - ctx := backend.Ctx - rules := backend.Rules - stx := s.tx - if err := utx.Verify(ctx, rules); err != nil { - return err - } - - // Check the transaction consumes and produces the right amounts - fc := avax.NewFlowChecker() - switch { - // Apply dynamic fees to import transactions as of Apricot Phase 3 - case rules.IsApricotPhase3: - gasUsed, err := stx.GasUsed(rules.IsApricotPhase5) - if err != nil { - return err - } - txFee, err := atomic.CalculateDynamicFee(gasUsed, s.baseFee) - if err != nil { - return err - } - fc.Produce(ctx.AVAXAssetID, txFee) - - // Apply fees to import transactions as of Apricot Phase 2 - case rules.IsApricotPhase2: - fc.Produce(ctx.AVAXAssetID, ap0.AtomicTxFee) - } - for _, out := range utx.Outs { - fc.Produce(out.AssetID, out.Amount) - } - for _, in := range utx.ImportedInputs { - fc.Consume(in.AssetID(), in.Input().Amount()) - } - - if err := fc.Verify(); err != nil { - return fmt.Errorf("import tx flow check failed due to: %w", err) - } - - if len(stx.Creds) != len(utx.ImportedInputs) { - return fmt.Errorf("import tx contained mismatched number of inputs/credentials (%d vs. %d)", len(utx.ImportedInputs), len(stx.Creds)) - } - - if !backend.Bootstrapped { - // Allow for force committing during bootstrapping - return nil - } - - utxoIDs := make([][]byte, len(utx.ImportedInputs)) - for i, in := range utx.ImportedInputs { - inputID := in.UTXOID.InputID() - utxoIDs[i] = inputID[:] - } - // allUTXOBytes is guaranteed to be the same length as utxoIDs - allUTXOBytes, err := ctx.SharedMemory.Get(utx.SourceChain, utxoIDs) + b := s.backend + err := VerifyTx( + b.Ctx, + b.Rules, + b.Fx, + b.SecpCache, + b.Bootstrapped, + s.tx, + s.baseFee, + ) if err != nil { - return fmt.Errorf("failed to fetch import UTXOs from %s due to: %w", utx.SourceChain, err) + return err } - for i, in := range utx.ImportedInputs { - utxoBytes := allUTXOBytes[i] - - utxo := &avax.UTXO{} - if _, err := atomic.Codec.Unmarshal(utxoBytes, utxo); err != nil { - return fmt.Errorf("failed to unmarshal UTXO: %w", err) - } - - cred := stx.Creds[i] - - utxoAssetID := utxo.AssetID() - inAssetID := in.AssetID() - if utxoAssetID != inAssetID { - return ErrAssetIDMismatch - } - - if err := backend.Fx.VerifyTransfer(utx, in.In, cred, utxo.Out); err != nil { - return fmt.Errorf("import tx transfer failed verification: %w", err) - } + if !b.Bootstrapped { + return nil // Allow for force committing during bootstrapping } - - return conflicts(backend, utx.InputUTXOs(), s.parent) + return conflicts(b, utx.InputUTXOs(), s.parent) } // conflicts returns an error if [inputs] conflicts with any of the atomic inputs contained in [ancestor] @@ -203,49 +143,183 @@ func conflicts(backend *VerifierBackend, inputs set.Set[ids.ID], ancestor extens // ExportTx verifies this transaction is valid. func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { - backend := s.backend - ctx := backend.Ctx - rules := backend.Rules - stx := s.tx - if err := utx.Verify(ctx, rules); err != nil { + b := s.backend + return VerifyTx( + b.Ctx, + b.Rules, + b.Fx, + b.SecpCache, + b.Bootstrapped, + s.tx, + s.baseFee, + ) +} + +func VerifyTx( + ctx *snow.Context, + rules extras.Rules, + fx fx.Fx, + cache *secp256k1.RecoverCache, + bootstrapped bool, + tx *atomic.Tx, + baseFee *big.Int, +) error { + if err := tx.UnsignedAtomicTx.Verify(ctx, rules); err != nil { + return err + } + txFee, err := txFee(rules, tx, baseFee) + if err != nil { + return err + } + if err := verifyFlowCheck(tx, ctx.AVAXAssetID, txFee); err != nil { return err } - // Check the transaction consumes and produces the right amounts - fc := avax.NewFlowChecker() + if !bootstrapped { + return nil // Allow for force committing during bootstrapping + } + return verifyCredentials(ctx.SharedMemory, fx, cache, tx) +} + +func txFee( + rules extras.Rules, + tx *atomic.Tx, + baseFee *big.Int, +) (uint64, error) { switch { // Apply dynamic fees to export transactions as of Apricot Phase 3 case rules.IsApricotPhase3: - gasUsed, err := stx.GasUsed(rules.IsApricotPhase5) + gasUsed, err := tx.GasUsed(rules.IsApricotPhase5) if err != nil { - return err + return 0, err } - txFee, err := atomic.CalculateDynamicFee(gasUsed, s.baseFee) + txFee, err := atomic.CalculateDynamicFee(gasUsed, baseFee) if err != nil { - return err + return 0, err } - fc.Produce(ctx.AVAXAssetID, txFee) - // Apply fees to export transactions before Apricot Phase 3 + return txFee, nil + // Apply fees to import transactions as of Apricot Phase 2 + case rules.IsApricotPhase2: + return ap0.AtomicTxFee, nil + // Prior to AP2, only export txs were required to pay a fee. We enforce the + // more lax restriction here that neither txs were required to pay a fee + // prior to AP2 to avoid maintaining tx specific code. These checks are no + // longer really required because processing during these old rules are + // restricted to be valid because of how bootstrapping syncs blocks. default: - fc.Produce(ctx.AVAXAssetID, ap0.AtomicTxFee) - } - for _, out := range utx.ExportedOutputs { - fc.Produce(out.AssetID(), out.Output().Amount()) - } - for _, in := range utx.Ins { - fc.Consume(in.AssetID, in.Amount) + return 0, nil } +} + +func verifyFlowCheck( + tx *atomic.Tx, + avaxAssetID ids.ID, + txFee uint64, +) error { + // Check the transaction consumes and produces the right amounts + fc := avax.NewFlowChecker() + fc.Produce(avaxAssetID, txFee) + switch utx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + for _, out := range utx.Outs { + fc.Produce(out.AssetID, out.Amount) + } + for _, in := range utx.ImportedInputs { + fc.Consume(in.AssetID(), in.Input().Amount()) + } + case *atomic.UnsignedExportTx: + for _, out := range utx.ExportedOutputs { + fc.Produce(out.AssetID(), out.Output().Amount()) + } + for _, in := range utx.Ins { + fc.Consume(in.AssetID, in.Amount) + } + default: + return fmt.Errorf("unexpected tx type: %T", utx) + } if err := fc.Verify(); err != nil { return fmt.Errorf("export tx flow check failed due to: %w", err) } + return nil +} + +func verifyCredentials( + sharedMemory avagoatomic.SharedMemory, + fx fx.Fx, + cache *secp256k1.RecoverCache, + tx *atomic.Tx, +) error { + switch utx := tx.UnsignedAtomicTx.(type) { + case *atomic.UnsignedImportTx: + return verifyCredentialsImportTx(sharedMemory, fx, utx, tx.Creds) + case *atomic.UnsignedExportTx: + return verifyCredentialsExportTx(cache, utx, tx.Creds) + default: + return fmt.Errorf("unexpected tx type: %T", utx) + } +} + +func verifyCredentialsImportTx( + sharedMemory avagoatomic.SharedMemory, + fx fx.Fx, + utx *atomic.UnsignedImportTx, + creds []verify.Verifiable, +) error { + if len(utx.ImportedInputs) != len(creds) { + return fmt.Errorf("import tx contained mismatched number of inputs/credentials (%d vs. %d)", + len(utx.ImportedInputs), + len(creds), + ) + } + + utxoIDs := make([][]byte, len(utx.ImportedInputs)) + for i, in := range utx.ImportedInputs { + inputID := in.UTXOID.InputID() + utxoIDs[i] = inputID[:] + } + // allUTXOBytes is guaranteed to be the same length as utxoIDs + allUTXOBytes, err := sharedMemory.Get(utx.SourceChain, utxoIDs) + if err != nil { + return fmt.Errorf("failed to fetch import UTXOs from %s due to: %w", utx.SourceChain, err) + } + + for i, in := range utx.ImportedInputs { + utxoBytes := allUTXOBytes[i] + + utxo := &avax.UTXO{} + if _, err := atomic.Codec.Unmarshal(utxoBytes, utxo); err != nil { + return fmt.Errorf("failed to unmarshal UTXO: %w", err) + } + + utxoAssetID := utxo.AssetID() + inAssetID := in.AssetID() + if utxoAssetID != inAssetID { + return ErrAssetIDMismatch + } + + cred := creds[i] + if err := fx.VerifyTransfer(utx, in.In, cred, utxo.Out); err != nil { + return fmt.Errorf("import tx transfer failed verification: %w", err) + } + } + return nil +} - if len(utx.Ins) != len(stx.Creds) { - return fmt.Errorf("export tx contained mismatched number of inputs/credentials (%d vs. %d)", len(utx.Ins), len(stx.Creds)) +func verifyCredentialsExportTx( + cache *secp256k1.RecoverCache, + utx *atomic.UnsignedExportTx, + creds []verify.Verifiable, +) error { + if len(utx.Ins) != len(creds) { + return fmt.Errorf("export tx contained mismatched number of inputs/credentials (%d vs. %d)", + len(utx.Ins), + len(creds), + ) } for i, input := range utx.Ins { - cred, ok := stx.Creds[i].(*secp256k1fx.Credential) + cred, ok := creds[i].(*secp256k1fx.Credential) if !ok { return fmt.Errorf("expected *secp256k1fx.Credential but got %T", cred) } @@ -256,7 +330,7 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { if len(cred.Sigs) != 1 { return fmt.Errorf("expected one signature for EVM Input Credential, but found: %d", len(cred.Sigs)) } - pubKey, err := s.backend.SecpCache.RecoverPublicKey(utx.Bytes(), cred.Sigs[0][:]) + pubKey, err := cache.RecoverPublicKey(utx.Bytes(), cred.Sigs[0][:]) if err != nil { return err } @@ -264,6 +338,5 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { return errPublicKeySignatureMismatch } } - return nil } From 06a219b7e782d7aba119398ce934ccdd198532d0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 15:25:01 -0400 Subject: [PATCH 18/45] remove weird platformvm dependency --- plugin/evm/atomic/vm/tx_semantic_verifier.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin/evm/atomic/vm/tx_semantic_verifier.go b/plugin/evm/atomic/vm/tx_semantic_verifier.go index 6c73dc9ac0..9721ac3b7f 100644 --- a/plugin/evm/atomic/vm/tx_semantic_verifier.go +++ b/plugin/evm/atomic/vm/tx_semantic_verifier.go @@ -16,7 +16,6 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" @@ -42,7 +41,7 @@ type BlockFetcher interface { type VerifierBackend struct { Ctx *snow.Context - Fx fx.Fx + Fx *secp256k1fx.Fx Rules extras.Rules Bootstrapped bool BlockFetcher BlockFetcher @@ -158,7 +157,7 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { func VerifyTx( ctx *snow.Context, rules extras.Rules, - fx fx.Fx, + fx *secp256k1fx.Fx, cache *secp256k1.RecoverCache, bootstrapped bool, tx *atomic.Tx, @@ -246,7 +245,7 @@ func verifyFlowCheck( func verifyCredentials( sharedMemory avagoatomic.SharedMemory, - fx fx.Fx, + fx *secp256k1fx.Fx, cache *secp256k1.RecoverCache, tx *atomic.Tx, ) error { @@ -262,7 +261,7 @@ func verifyCredentials( func verifyCredentialsImportTx( sharedMemory avagoatomic.SharedMemory, - fx fx.Fx, + fx *secp256k1fx.Fx, utx *atomic.UnsignedImportTx, creds []verify.Verifiable, ) error { From cdff895ba70c819eb92d5d6d8be39cabfef0fb02 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 15 Jul 2025 15:52:31 -0400 Subject: [PATCH 19/45] wip --- sae/hooks.go | 77 ++++++++++++++++++++++++++++++++++++++-------------- sae/txs.go | 4 +++ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/sae/hooks.go b/sae/hooks.go index 22544996f4..128a1e4d6f 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -13,11 +13,13 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/coreth/params" + "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" @@ -28,6 +30,7 @@ import ( "go.uber.org/zap" atomictxpool "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" + atomicvm "github.com/ava-labs/coreth/plugin/evm/atomic/vm" ) const targetAtomicTxsSize = 40 * units.KiB @@ -42,6 +45,14 @@ type hooks struct { ctx *snow.Context chainConfig *params.ChainConfig mempool *atomictxpool.Txs + + // TODO: Handle this correctly + bootstrapped bool + + // TODO: Make this a global and initialize it + fx secp256k1fx.Fx + // TODO: Make this a global and initialize it + cache *secp256k1.RecoverCache } func (h *hooks) GasTarget(parent *types.Block) gas.Gas { @@ -130,11 +141,16 @@ func (h *hooks) constructBlock( return nil, err } + rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) + rulesExtra := params.GetRulesExtra(rules) atomicTxs, err := packAtomicTxs( ctx, - h.ctx.Log, + h.ctx, + &h.fx, + h.cache, + rulesExtra, + h.bootstrapped, state, - h.ctx.AVAXAssetID, header.BaseFee, ancestorInputUTXOs, potentialAtomicTxs, @@ -171,7 +187,6 @@ func (h *hooks) constructBlock( // return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err) // } - rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) predicateResults, err := calculatePredicateResults( ctx, h.ctx, @@ -224,9 +239,12 @@ func (h *hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([ func packAtomicTxs( ctx context.Context, - log logging.Logger, + snowContext *snow.Context, + fx *secp256k1fx.Fx, + cache *secp256k1.RecoverCache, + rules *extras.Rules, + bootstrapped bool, state hook.State, - avaxAssetID ids.ID, baseFee *big.Int, ancestorInputUTXOs set.Set[ids.ID], txs txs, @@ -249,29 +267,46 @@ func packAtomicTxs( break } + // VerifyTx ensures: + // 1. Transactions are syntactically valid. + // 2. Transactions do not produces more assets than they consume, + // including the fees. + // 3. Inputs all have corresponding credentials with valid signatures. + // 4. ImportTxs are consuming UTXOs that are currently in shared memory. + err := atomicvm.VerifyTx( + snowContext, + *rules, + fx, + cache, + bootstrapped, + tx, + baseFee, + ) + if err != nil { + txID := tx.ID() + snowContext.Log.Debug("discarding tx due to failed verification", + zap.Stringer("txID", txID), + zap.Error(err), + ) + txs.DiscardCurrentTx(txID) + continue + } + + // Verify that any ImportTxs do not conflict with prior ImportTxs, + // either in the same block or in an ancestor. inputUTXOs := tx.InputUTXOs() if ancestorInputUTXOs.Overlaps(inputUTXOs) { - // Discard the transaction from the mempool since it will fail - // verification after this block has been accepted. - // - // Note: if the proposed block is not accepted, the transaction may - // still be valid, but we discard it early here based on the - // assumption that the proposed block will most likely be accepted. txID := tx.ID() - log.Debug("discarding tx due to overlapping input utxos", + snowContext.Log.Debug("discarding tx due to overlapping input utxos", zap.Stringer("txID", txID), ) txs.DiscardCurrentTx(txID) continue } - // TODO: The transaction signatures must be verified and the import - // UTXOs must be read from shared memory here. - - // The atomicTxOp will verify that export txs have sufficient funds and - // utilize proper nonces. It additionally enforces that sufficient fees - // are paid by the transactions. - op, err := atomicTxOp(tx, avaxAssetID, baseFee) + // The atomicTxOp will verify that ExportTxs have sufficient funds and + // utilize proper nonces. + op, err := atomicTxOp(tx, snowContext.AVAXAssetID, baseFee) if err != nil { txs.DiscardCurrentTx(tx.ID()) continue @@ -285,7 +320,7 @@ func packAtomicTxs( } if err != nil { txID := tx.ID() - log.Debug("discarding tx from mempool due to failed verification", + snowContext.Log.Debug("discarding tx from mempool due to failed verification", zap.Stringer("txID", txID), zap.Error(err), ) diff --git a/sae/txs.go b/sae/txs.go index 2bfb622576..43079c643c 100644 --- a/sae/txs.go +++ b/sae/txs.go @@ -99,6 +99,8 @@ func atomicTxOp( if output.AssetID != avaxAssetID { continue } + + // TODO: This implementation assumes that the addresses are unique. var amount uint256.Int amount.SetUint64(output.Amount) amount.Mul(&amount, atomic.X2CRate) @@ -110,6 +112,8 @@ func atomicTxOp( if input.AssetID != avaxAssetID { continue } + + // TODO: This implementation assumes that the addresses are unique. var amount uint256.Int amount.SetUint64(input.Amount) amount.Mul(&amount, atomic.X2CRate) From 0c7155c1ba49a84f20ede58d5a2767f6a775e215 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 18 Jul 2025 11:31:47 -0400 Subject: [PATCH 20/45] merged --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7eef38e4e6..0939ee220f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.9 require ( github.com/VictoriaMetrics/fastcache v1.12.1 - github.com/ava-labs/avalanchego v1.13.3-rc.1 + github.com/ava-labs/avalanchego v1.13.3-rc.2 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.8 github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 github.com/ava-labs/strevm v0.0.0-00010101000000-000000000000 diff --git a/go.sum b/go.sum index dc2051b36b..f133ddd4c3 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.3-rc.1 h1:YhtNjw+MljEohXPdbBqSVGvV74AKjpyknhCunZFFS4E= -github.com/ava-labs/avalanchego v1.13.3-rc.1/go.mod h1:t5+0qviLU5jwkaBvi9e4KSbuiDIy67OS+VKU7roB38A= +github.com/ava-labs/avalanchego v1.13.3-rc.2 h1:P+XSQqfAuhNPq+RG/dwDQ07o3sMh9ZsyeSgH/OV4y5s= +github.com/ava-labs/avalanchego v1.13.3-rc.2/go.mod h1:dXVZK6Sw3ZPIQ9sLjto0VMWeehExppNHFTWFwyUv5tk= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.8 h1:f0ZbAiRE1srMiv/0DuXvPQZwgYbLC9OgAWbQUCMebTE= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.8/go.mod h1:j6spQFNSBAfcXKt9g0xbObW/8tMlGP4bFjPIsJmDg/o= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= From 356fd7bfb297971bd859d5977f4c9d861d47768d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 15:56:28 -0400 Subject: [PATCH 21/45] reduce diff --- accounts/abi/bind/bind_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/accounts/abi/bind/bind_test.go b/accounts/abi/bind/bind_test.go index 3bc1aabe92..1779627be1 100644 --- a/accounts/abi/bind/bind_test.go +++ b/accounts/abi/bind/bind_test.go @@ -2171,27 +2171,23 @@ func golangBindings(t *testing.T, overload bool) { // Convert the package to go modules and use the current source for go-ethereum moder := exec.Command(gocmd, "mod", "init", "bindtest") moder.Dir = pkg - t.Logf("running command: %s", moder.String()) if out, err := moder.CombinedOutput(); err != nil { t.Fatalf("failed to convert binding test to modules: %v\n%s", err, out) } pwd, _ := os.Getwd() replacer := exec.Command(gocmd, "mod", "edit", "-x", "-require", "github.com/ava-labs/coreth@v0.0.0", "-replace", "github.com/ava-labs/coreth="+filepath.Join(pwd, "..", "..", "..")) // Repo root replacer.Dir = pkg - t.Logf("running command: %s", replacer.String()) if out, err := replacer.CombinedOutput(); err != nil { t.Fatalf("failed to replace binding test dependency to current source tree: %v\n%s", err, out) } tidier := exec.Command(gocmd, "mod", "tidy", "-compat=1.23") tidier.Dir = pkg - t.Logf("running command: %s", tidier.String()) if out, err := tidier.CombinedOutput(); err != nil { t.Fatalf("failed to tidy Go module file: %v\n%s", err, out) } // Test the entire package and report any failures cmd := exec.Command(gocmd, "test", "-v", "-count", "1") cmd.Dir = pkg - t.Logf("running command: %s", cmd.String()) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("failed to run binding test: %v\n%s", err, out) } From 156a54212a2a6dbd9f1d039a4a39648899c25cf7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 15:56:50 -0400 Subject: [PATCH 22/45] reduce diff --- core/extstate/statedb_multicoin_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/extstate/statedb_multicoin_test.go b/core/extstate/statedb_multicoin_test.go index 67d3e21cc6..f041d6750c 100644 --- a/core/extstate/statedb_multicoin_test.go +++ b/core/extstate/statedb_multicoin_test.go @@ -17,8 +17,6 @@ import ( "github.com/ava-labs/libevm/libevm/stateconf" "github.com/holiman/uint256" "github.com/stretchr/testify/require" - - . "github.com/ava-labs/coreth/core/extstate" ) func TestMultiCoinOperations(t *testing.T) { From 9f4a918c5fae12a481dab72fce18e509b72ba145 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 15:57:21 -0400 Subject: [PATCH 23/45] reduce diff --- go.mod | 2 -- go.sum | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index a8bc20b039..dde5e52daf 100644 --- a/go.mod +++ b/go.mod @@ -179,5 +179,3 @@ require ( ) replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm - -replace github.com/ava-labs/libevm => /Users/stephen/go/src/github.com/ava-labs/libevm diff --git a/go.sum b/go.sum index 50b68b9fe0..79dd0ee6a1 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/ava-labs/avalanchego v1.13.3 h1:wYootDIqq1FG0FrrNI+yRqLB3szpvuTmW1j0i github.com/ava-labs/avalanchego v1.13.3/go.mod h1:OKgu35RVcoX4M8TB0U6yNvcDNKc2CwvaHrU8PgXVq64= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.9 h1:zw0g+cUbZDsGdWx1PKmBChkpy+ixL3QgiI86DUOuXvo= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.9/go.mod h1:cq89ua3iiZ5wPBALTEQS5eG8DIZcs7ov6OiL4YR1BVY= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.3 h1:qYwmFh5fG8T4ShagvJs6TNXTfVMdSyvQYwVZb3tx+4k= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.3/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g= 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= From b33dfbd41d9ff6386ae962d00b3d6962911013d2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 15:58:45 -0400 Subject: [PATCH 24/45] reduce diff --- nativeasset/contract_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nativeasset/contract_test.go b/nativeasset/contract_test.go index 058251b671..1acdbb21fc 100644 --- a/nativeasset/contract_test.go +++ b/nativeasset/contract_test.go @@ -96,7 +96,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee @@ -121,7 +120,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee @@ -149,7 +147,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - // Create account statedb.CreateAccount(userAddr1) // Set balance to pay for gas fee @@ -230,7 +227,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) @@ -266,7 +262,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) @@ -304,7 +299,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) @@ -339,7 +333,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, uint256.NewInt(50)) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50)) @@ -374,7 +367,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) @@ -397,7 +389,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) @@ -432,7 +423,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) @@ -455,7 +445,6 @@ func TestStatefulPrecompile(t *testing.T) { if err != nil { t.Fatal(err) } - statedb.SetBalance(userAddr1, u256Hundred) wrappedStateDB := extstate.New(statedb) wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred) From 7c6fda01345e01692a2021251c95f895545376d3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 16:02:19 -0400 Subject: [PATCH 25/45] reduce diff --- plugin/evm/atomic/atomictest/tx.go | 3 +-- plugin/evm/atomic/export_tx.go | 30 +++++++++++++----------------- plugin/evm/atomic/import_tx.go | 9 +++------ plugin/evm/atomic/tx.go | 17 +++++++++++++++-- plugin/evm/export_tx_test.go | 1 - 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/plugin/evm/atomic/atomictest/tx.go b/plugin/evm/atomic/atomictest/tx.go index eb102b0855..533b897993 100644 --- a/plugin/evm/atomic/atomictest/tx.go +++ b/plugin/evm/atomic/atomictest/tx.go @@ -16,7 +16,6 @@ import ( "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" - "github.com/ava-labs/libevm/core/state" ) // TODO: Remove this and use actual codec and transactions (export, import) @@ -89,7 +88,7 @@ func (t *TestUnsignedTx) Visit(v atomic.Visitor) error { } // EVMStateTransfer implements the UnsignedAtomicTx interface -func (t *TestUnsignedTx) EVMStateTransfer(ctx *snow.Context, state *state.StateDB) error { +func (t *TestUnsignedTx) EVMStateTransfer(ctx *snow.Context, state atomic.StateDB) error { return t.EVMStateTransferV } diff --git a/plugin/evm/atomic/export_tx.go b/plugin/evm/atomic/export_tx.go index f6d39de0e4..4a2a54eaa1 100644 --- a/plugin/evm/atomic/export_tx.go +++ b/plugin/evm/atomic/export_tx.go @@ -21,12 +21,10 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap0" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/log" "github.com/holiman/uint256" ) @@ -220,7 +218,7 @@ func (utx *UnsignedExportTx) AtomicOps() (ids.ID, *atomic.Requests, error) { func NewExportTx( ctx *snow.Context, rules extras.Rules, - stateDB *state.StateDB, + stateDB StateDB, assetID ids.ID, // AssetID of the tokens to export amount uint64, // Amount of tokens to export chainID ids.ID, // Chain to send the UTXOs to @@ -311,8 +309,7 @@ func NewExportTx( } // EVMStateTransfer executes the state update from the atomic export transaction -func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state.StateDB) error { - ws := extstate.New(stateDB) +func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB) error { addrs := map[[20]byte]uint64{} for _, from := range utx.Ins { if from.AssetID == ctx.AVAXAssetID { @@ -323,25 +320,25 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state. uint256.NewInt(from.Amount), uint256.NewInt(X2CRate.Uint64()), ) - if ws.GetBalance(from.Address).Cmp(amount) < 0 { + if stateDB.GetBalance(from.Address).Cmp(amount) < 0 { return errInsufficientFunds } - ws.SubBalance(from.Address, amount) + stateDB.SubBalance(from.Address, amount) } else { log.Debug("export_tx", "dest", utx.DestinationChain, "addr", from.Address, "amount", from.Amount, "assetID", from.AssetID) amount := new(big.Int).SetUint64(from.Amount) - if ws.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { + if stateDB.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { return errInsufficientFunds } - ws.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) + stateDB.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) } - if ws.GetNonce(from.Address) != from.Nonce { + if stateDB.GetNonce(from.Address) != from.Nonce { return errInvalidNonce } addrs[from.Address] = from.Nonce } for addr, nonce := range addrs { - ws.SetNonce(addr, nonce+1) + stateDB.SetNonce(addr, nonce+1) } return nil } @@ -353,12 +350,11 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state. // [tx.Sign] which supports multiple keys on a single input. func getSpendableFunds( ctx *snow.Context, - stateDB *state.StateDB, + stateDB StateDB, keys []*secp256k1.PrivateKey, assetID ids.ID, amount uint64, ) ([]EVMInput, [][]*secp256k1.PrivateKey, error) { - ws := extstate.New(stateDB) inputs := []EVMInput{} signers := [][]*secp256k1.PrivateKey{} // Note: we assume that each key in [keys] is unique, so that iterating over @@ -372,9 +368,9 @@ func getSpendableFunds( if assetID == ctx.AVAXAssetID { // If the asset is AVAX, we divide by the x2cRate to convert back to the correct // denomination of AVAX that can be exported. - balance = new(uint256.Int).Div(ws.GetBalance(addr), X2CRate).Uint64() + balance = new(uint256.Int).Div(stateDB.GetBalance(addr), X2CRate).Uint64() } else { - balance = ws.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() + balance = stateDB.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() } if balance == 0 { continue @@ -382,7 +378,7 @@ func getSpendableFunds( if amount < balance { balance = amount } - nonce := ws.GetNonce(addr) + nonce := stateDB.GetNonce(addr) inputs = append(inputs, EVMInput{ Address: addr, @@ -411,7 +407,7 @@ func getSpendableFunds( // [tx.Sign] which supports multiple keys on a single input. func getSpendableAVAXWithFee( ctx *snow.Context, - stateDB *state.StateDB, + stateDB StateDB, keys []*secp256k1.PrivateKey, amount uint64, cost uint64, diff --git a/plugin/evm/atomic/import_tx.go b/plugin/evm/atomic/import_tx.go index 69282c0663..b8678915fb 100644 --- a/plugin/evm/atomic/import_tx.go +++ b/plugin/evm/atomic/import_tx.go @@ -20,12 +20,10 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap0" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap5" "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/log" "github.com/holiman/uint256" ) @@ -333,19 +331,18 @@ func NewImportTx( // EVMStateTransfer performs the state transfer to increase the balances of // accounts accordingly with the imported EVMOutputs -func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, stateDB *state.StateDB) error { - ws := extstate.New(stateDB) +func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB) error { for _, to := range utx.Outs { if to.AssetID == ctx.AVAXAssetID { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", "AVAX") // If the asset is AVAX, convert the input amount in nAVAX to gWei by // multiplying by the x2c rate. amount := new(uint256.Int).Mul(uint256.NewInt(to.Amount), X2CRate) - ws.AddBalance(to.Address, amount) + stateDB.AddBalance(to.Address, amount) } else { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", to.AssetID) amount := new(big.Int).SetUint64(to.Amount) - ws.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) + stateDB.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) } } return nil diff --git a/plugin/evm/atomic/tx.go b/plugin/evm/atomic/tx.go index c51dd43138..c7f4ed3f04 100644 --- a/plugin/evm/atomic/tx.go +++ b/plugin/evm/atomic/tx.go @@ -23,7 +23,6 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/core/state" "github.com/holiman/uint256" ) @@ -136,6 +135,20 @@ type UnsignedTx interface { SignedBytes() []byte } +type StateDB interface { + AddBalance(common.Address, *uint256.Int) + AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) + + SubBalance(common.Address, *uint256.Int) + SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) + + GetBalance(common.Address) *uint256.Int + GetBalanceMultiCoin(common.Address, common.Hash) *big.Int + + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) +} + // UnsignedAtomicTx is an unsigned operation that can be atomically accepted type UnsignedAtomicTx interface { UnsignedTx @@ -153,7 +166,7 @@ type UnsignedAtomicTx interface { // The set of atomic requests must be returned in a consistent order. AtomicOps() (ids.ID, *atomic.Requests, error) - EVMStateTransfer(ctx *snow.Context, state *state.StateDB) error + EVMStateTransfer(ctx *snow.Context, state StateDB) error } // Tx is a signed transaction diff --git a/plugin/evm/export_tx_test.go b/plugin/evm/export_tx_test.go index e9a747fb00..bfdf1728cb 100644 --- a/plugin/evm/export_tx_test.go +++ b/plugin/evm/export_tx_test.go @@ -441,7 +441,6 @@ func TestExportTxEVMStateTransfer(t *testing.T) { t.Fatalf("address balance %s equal %s not %s", addr.String(), avaxBalance, test.avaxBalance) } - ws := extstate.New(stateDB) for assetID, expectedBalance := range test.balances { balance := wrappedStateDB.GetBalanceMultiCoin(ethAddr, common.Hash(assetID)) if avaxBalance.Cmp(test.avaxBalance) != 0 { From 7b64107ae5db725a2677ea297c44cf68d1b3ee63 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 16:03:05 -0400 Subject: [PATCH 26/45] reduce diff --- plugin/evm/atomic/export_tx.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugin/evm/atomic/export_tx.go b/plugin/evm/atomic/export_tx.go index 4a2a54eaa1..8619f12199 100644 --- a/plugin/evm/atomic/export_tx.go +++ b/plugin/evm/atomic/export_tx.go @@ -218,7 +218,7 @@ func (utx *UnsignedExportTx) AtomicOps() (ids.ID, *atomic.Requests, error) { func NewExportTx( ctx *snow.Context, rules extras.Rules, - stateDB StateDB, + state StateDB, assetID ids.ID, // AssetID of the tokens to export amount uint64, // Amount of tokens to export chainID ids.ID, // Chain to send the UTXOs to @@ -247,7 +247,7 @@ func NewExportTx( // consume non-AVAX if assetID != ctx.AVAXAssetID { - ins, signers, err = getSpendableFunds(ctx, stateDB, keys, assetID, amount) + ins, signers, err = getSpendableFunds(ctx, state, keys, assetID, amount) if err != nil { return nil, fmt.Errorf("couldn't generate tx inputs/signers: %w", err) } @@ -275,14 +275,14 @@ func NewExportTx( return nil, err } - avaxIns, avaxSigners, err = getSpendableAVAXWithFee(ctx, stateDB, keys, avaxNeeded, cost, baseFee) + avaxIns, avaxSigners, err = getSpendableAVAXWithFee(ctx, state, keys, avaxNeeded, cost, baseFee) default: var newAvaxNeeded uint64 newAvaxNeeded, err = math.Add64(avaxNeeded, ap0.AtomicTxFee) if err != nil { return nil, errOverflowExport } - avaxIns, avaxSigners, err = getSpendableFunds(ctx, stateDB, keys, ctx.AVAXAssetID, newAvaxNeeded) + avaxIns, avaxSigners, err = getSpendableFunds(ctx, state, keys, ctx.AVAXAssetID, newAvaxNeeded) } if err != nil { return nil, fmt.Errorf("couldn't generate tx inputs/signers: %w", err) @@ -350,7 +350,7 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB // [tx.Sign] which supports multiple keys on a single input. func getSpendableFunds( ctx *snow.Context, - stateDB StateDB, + state StateDB, keys []*secp256k1.PrivateKey, assetID ids.ID, amount uint64, @@ -368,9 +368,9 @@ func getSpendableFunds( if assetID == ctx.AVAXAssetID { // If the asset is AVAX, we divide by the x2cRate to convert back to the correct // denomination of AVAX that can be exported. - balance = new(uint256.Int).Div(stateDB.GetBalance(addr), X2CRate).Uint64() + balance = new(uint256.Int).Div(state.GetBalance(addr), X2CRate).Uint64() } else { - balance = stateDB.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() + balance = state.GetBalanceMultiCoin(addr, common.Hash(assetID)).Uint64() } if balance == 0 { continue @@ -378,7 +378,7 @@ func getSpendableFunds( if amount < balance { balance = amount } - nonce := stateDB.GetNonce(addr) + nonce := state.GetNonce(addr) inputs = append(inputs, EVMInput{ Address: addr, @@ -407,7 +407,7 @@ func getSpendableFunds( // [tx.Sign] which supports multiple keys on a single input. func getSpendableAVAXWithFee( ctx *snow.Context, - stateDB StateDB, + state StateDB, keys []*secp256k1.PrivateKey, amount uint64, cost uint64, @@ -449,7 +449,7 @@ func getSpendableAVAXWithFee( addr := key.EthAddress() // Since the asset is AVAX, we divide by the x2cRate to convert back to // the correct denomination of AVAX that can be exported. - balance := new(uint256.Int).Div(stateDB.GetBalance(addr), X2CRate).Uint64() + balance := new(uint256.Int).Div(state.GetBalance(addr), X2CRate).Uint64() // If the balance for [addr] is insufficient to cover the additional cost // of adding an input to the transaction, skip adding the input altogether if balance <= additionalFee { @@ -472,7 +472,7 @@ func getSpendableAVAXWithFee( if amount < balance { inputAmount = amount } - nonce := stateDB.GetNonce(addr) + nonce := state.GetNonce(addr) inputs = append(inputs, EVMInput{ Address: addr, From ee78d0ab0ca365006212a718282357d171afd03b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Jul 2025 16:03:46 -0400 Subject: [PATCH 27/45] reduce diff --- plugin/evm/atomic/export_tx.go | 14 +++++++------- plugin/evm/atomic/import_tx.go | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin/evm/atomic/export_tx.go b/plugin/evm/atomic/export_tx.go index 8619f12199..0a13520c5f 100644 --- a/plugin/evm/atomic/export_tx.go +++ b/plugin/evm/atomic/export_tx.go @@ -309,7 +309,7 @@ func NewExportTx( } // EVMStateTransfer executes the state update from the atomic export transaction -func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB) error { +func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) error { addrs := map[[20]byte]uint64{} for _, from := range utx.Ins { if from.AssetID == ctx.AVAXAssetID { @@ -320,25 +320,25 @@ func (utx *UnsignedExportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB uint256.NewInt(from.Amount), uint256.NewInt(X2CRate.Uint64()), ) - if stateDB.GetBalance(from.Address).Cmp(amount) < 0 { + if state.GetBalance(from.Address).Cmp(amount) < 0 { return errInsufficientFunds } - stateDB.SubBalance(from.Address, amount) + state.SubBalance(from.Address, amount) } else { log.Debug("export_tx", "dest", utx.DestinationChain, "addr", from.Address, "amount", from.Amount, "assetID", from.AssetID) amount := new(big.Int).SetUint64(from.Amount) - if stateDB.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { + if state.GetBalanceMultiCoin(from.Address, common.Hash(from.AssetID)).Cmp(amount) < 0 { return errInsufficientFunds } - stateDB.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) + state.SubBalanceMultiCoin(from.Address, common.Hash(from.AssetID), amount) } - if stateDB.GetNonce(from.Address) != from.Nonce { + if state.GetNonce(from.Address) != from.Nonce { return errInvalidNonce } addrs[from.Address] = from.Nonce } for addr, nonce := range addrs { - stateDB.SetNonce(addr, nonce+1) + state.SetNonce(addr, nonce+1) } return nil } diff --git a/plugin/evm/atomic/import_tx.go b/plugin/evm/atomic/import_tx.go index b8678915fb..c1a6289142 100644 --- a/plugin/evm/atomic/import_tx.go +++ b/plugin/evm/atomic/import_tx.go @@ -331,18 +331,18 @@ func NewImportTx( // EVMStateTransfer performs the state transfer to increase the balances of // accounts accordingly with the imported EVMOutputs -func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, stateDB StateDB) error { +func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, state StateDB) error { for _, to := range utx.Outs { if to.AssetID == ctx.AVAXAssetID { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", "AVAX") // If the asset is AVAX, convert the input amount in nAVAX to gWei by // multiplying by the x2c rate. amount := new(uint256.Int).Mul(uint256.NewInt(to.Amount), X2CRate) - stateDB.AddBalance(to.Address, amount) + state.AddBalance(to.Address, amount) } else { log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", to.AssetID) amount := new(big.Int).SetUint64(to.Amount) - stateDB.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) + state.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) } } return nil From 92d7c78a14603e5c64cba33eed7172f04f000130 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Aug 2025 13:39:25 -0400 Subject: [PATCH 28/45] reduce diff --- precompile/contract/interfaces.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/precompile/contract/interfaces.go b/precompile/contract/interfaces.go index dd074aa5e2..a896d6f862 100644 --- a/precompile/contract/interfaces.go +++ b/precompile/contract/interfaces.go @@ -34,10 +34,9 @@ type StateDB interface { GetBalance(common.Address) *uint256.Int AddBalance(common.Address, *uint256.Int) - - GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int - AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) - SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) + GetBalanceMultiCoin(common.Address, common.Hash) *big.Int + AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) + SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) CreateAccount(common.Address) Exist(common.Address) bool From 81a683ad2ac9198cb9b5964da0832e8a9def293c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Aug 2025 16:25:43 -0400 Subject: [PATCH 29/45] nits --- plugin/evm/atomic/atomictest/tx.go | 1 - sae/vm.go | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/plugin/evm/atomic/atomictest/tx.go b/plugin/evm/atomic/atomictest/tx.go index 3ad0a728d9..d9b778afc1 100644 --- a/plugin/evm/atomic/atomictest/tx.go +++ b/plugin/evm/atomic/atomictest/tx.go @@ -6,7 +6,6 @@ package atomictest import ( "math/rand" - avalancheatomic "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" "github.com/ava-labs/avalanchego/ids" diff --git a/sae/vm.go b/sae/vm.go index e37a5ffed1..6eabaa07a1 100644 --- a/sae/vm.go +++ b/sae/vm.go @@ -18,7 +18,6 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/state" sae "github.com/ava-labs/strevm" - "github.com/prometheus/client_golang/prometheus" ) const atomicMempoolSize = 4096 // number of transactions @@ -57,12 +56,7 @@ func (vm *vm) Initialize( return err } - // TODO: Fix metrics - mempoolTxs, err := txpool.NewTxs( - chainContext, - prometheus.NewRegistry(), - atomicMempoolSize, - ) + mempoolTxs := txpool.NewTxs(chainContext, atomicMempoolSize) if err != nil { return fmt.Errorf("failed to initialize mempool: %w", err) } From 995905c0d4c37067a37f08f2d19a36cfa7ce77ae Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Aug 2025 16:30:04 -0400 Subject: [PATCH 30/45] nit cleanup --- sae/txs.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/sae/txs.go b/sae/txs.go index 43079c643c..ee44674c17 100644 --- a/sae/txs.go +++ b/sae/txs.go @@ -68,26 +68,18 @@ func atomicTxOp( avaxAssetID ids.ID, baseFee *big.Int, ) (hook.Op, error) { - // Note: we do not need to check if we are in at least ApricotPhase4 here - // because we assume that this function will only be called when the block - // is in at least ApricotPhase5. + // We do not need to check if we are in ApricotPhase5 here because we assume + // that this function will only be called when the block is in at least + // ApricotPhase5. gasUsed, err := tx.GasUsed(true) if err != nil { return hook.Op{}, err } - burned, err := tx.Burned(avaxAssetID) + gasPrice, err := atomic.EffectiveGasPrice(tx.UnsignedAtomicTx, avaxAssetID, true) if err != nil { return hook.Op{}, err } - var bigGasUsed uint256.Int - bigGasUsed.SetUint64(gasUsed) - - var gasPrice uint256.Int // gasPrice = burned * x2cRate / gasUsed - gasPrice.SetUint64(burned) - gasPrice.Mul(&gasPrice, atomic.X2CRate) - gasPrice.Div(&gasPrice, &bigGasUsed) - op := hook.Op{ Gas: gas.Gas(gasUsed), GasPrice: gasPrice, From 9b43d2b8279a76bfac25193c120f0c6cbf263c2a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Aug 2025 15:29:42 -0400 Subject: [PATCH 31/45] Cleanup predicate execution --- core/predicate_check.go | 53 ++++++++++++++++++++++++++++--------- plugin/evm/wrapped_block.go | 16 +++++------ sae/hooks.go | 11 +++++--- sae/predicates.go | 40 ---------------------------- 4 files changed, 54 insertions(+), 66 deletions(-) delete mode 100644 sae/predicates.go diff --git a/core/predicate_check.go b/core/predicate_check.go index a939cc7957..5281d464c2 100644 --- a/core/predicate_check.go +++ b/core/predicate_check.go @@ -18,43 +18,70 @@ import ( var ErrMissingPredicateContext = errors.New("missing predicate context") -// CheckPredicates verifies the predicates of [tx] and returns the result. Returning an error invalidates the block. -func CheckPredicates(rules params.Rules, predicateContext *precompileconfig.PredicateContext, tx *types.Transaction) (predicate.PrecompileResults, error) { - // Check that the transaction can cover its IntrinsicGas (including the gas required by the predicate) before - // verifying the predicate. - intrinsicGas, err := IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, rules) +// CheckBlockPredicates verifies the predicates of all txs and returns the +// result. +// +// Returning an error invalidates the block. +func CheckBlockPredicates( + rules params.Rules, + predicateContext *precompileconfig.PredicateContext, + txs []*types.Transaction, +) (predicate.BlockResults, error) { + var results predicate.BlockResults + // TODO: Calculate tx predicates concurrently. + for _, tx := range txs { + txResults, err := CheckPredicates(rules, predicateContext, tx) + if err != nil { + return nil, err + } + results.Set(tx.Hash(), txResults) + } + return results, nil +} + +// CheckPredicates verifies the predicates of tx and returns the result. +// +// Returning an error invalidates the transaction. +func CheckPredicates( + rules params.Rules, + predicateContext *precompileconfig.PredicateContext, + tx *types.Transaction, +) (predicate.PrecompileResults, error) { + // Check that the transaction can cover its IntrinsicGas (including the gas + // required by the predicate) before verifying the predicate. + accessList := tx.AccessList() + intrinsicGas, err := IntrinsicGas(tx.Data(), accessList, tx.To() == nil, rules) if err != nil { return nil, err } - if tx.Gas() < intrinsicGas { - return nil, fmt.Errorf("%w for predicate verification (%d) < intrinsic gas (%d)", ErrIntrinsicGas, tx.Gas(), intrinsicGas) + if gasLimit := tx.Gas(); gasLimit < intrinsicGas { + return nil, fmt.Errorf("%w for predicate verification (%d) < intrinsic gas (%d)", ErrIntrinsicGas, gasLimit, intrinsicGas) } rulesExtra := params.GetRulesExtra(rules) - predicateResults := make(predicate.PrecompileResults) // Short circuit early if there are no precompile predicates to verify if !rulesExtra.PredicatersExist() { - return predicateResults, nil + return nil, nil } // Prepare the predicate storage slots from the transaction's access list - predicateArguments := predicate.FromAccessList(rulesExtra, tx.AccessList()) + predicateArguments := predicate.FromAccessList(rulesExtra, accessList) // If there are no predicates to verify, return early and skip requiring the proposervm block // context to be populated. if len(predicateArguments) == 0 { - return predicateResults, nil + return nil, nil } if predicateContext == nil || predicateContext.ProposerVMBlockCtx == nil { return nil, ErrMissingPredicateContext } + predicateResults := make(predicate.PrecompileResults, len(predicateArguments)) for address, predicates := range predicateArguments { // Since [address] is only added to [predicateArguments] when there's a valid predicate in the ruleset // there's no need to check if the predicate exists here. - rules := params.GetRulesExtra(rules) - predicaterContract := rules.Predicaters[address] + predicaterContract := rulesExtra.Predicaters[address] bitset := set.NewBits() for i, predicate := range predicates { if err := predicaterContract.VerifyPredicate(predicateContext, predicate); err != nil { diff --git a/plugin/evm/wrapped_block.go b/plugin/evm/wrapped_block.go index 852082a995..d9dcc6d244 100644 --- a/plugin/evm/wrapped_block.go +++ b/plugin/evm/wrapped_block.go @@ -14,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/vms/evm/predicate" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" @@ -441,15 +440,14 @@ func (b *wrappedBlock) verifyPredicates(predicateContext *precompileconfig.Predi return nil } - predicateResults := predicate.BlockResults{} - for _, tx := range b.ethBlock.Transactions() { - results, err := core.CheckPredicates(rules, predicateContext, tx) - if err != nil { - return err - } - predicateResults.Set(tx.Hash(), results) + predicateResults, err := core.CheckBlockPredicates( + rules, + predicateContext, + b.ethBlock.Transactions(), + ) + if err != nil { + return err } - // TODO: document required gas constraints to ensure marshalling predicate results does not error predicateResultsBytes, err := predicateResults.Bytes() if err != nil { return fmt.Errorf("failed to marshal predicate results: %w", err) diff --git a/sae/hooks.go b/sae/hooks.go index 128a1e4d6f..f44ee23a08 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -18,11 +18,13 @@ import ( "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/plugin/evm/upgrade/acp176" + "github.com/ava-labs/coreth/precompile/precompileconfig" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/trie" "github.com/ava-labs/strevm/hook" @@ -187,11 +189,12 @@ func (h *hooks) constructBlock( // return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err) // } - predicateResults, err := calculatePredicateResults( - ctx, - h.ctx, + predicateResults, err := core.CheckBlockPredicates( rules, - blockContext, + &precompileconfig.PredicateContext{ + SnowCtx: h.ctx, + ProposerVMBlockCtx: blockContext, + }, txs, ) if err != nil { diff --git a/sae/predicates.go b/sae/predicates.go deleted file mode 100644 index 7aeda473fd..0000000000 --- a/sae/predicates.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/vms/evm/predicate" - "github.com/ava-labs/coreth/core" - "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/precompile/precompileconfig" - "github.com/ava-labs/libevm/core/types" -) - -func calculatePredicateResults( - ctx context.Context, - snowContext *snow.Context, - rules params.Rules, - blockContext *block.Context, - txs []*types.Transaction, -) (predicate.BlockResults, error) { - predicateContext := precompileconfig.PredicateContext{ - SnowCtx: snowContext, - ProposerVMBlockCtx: blockContext, - } - var results predicate.BlockResults - // TODO: Each transaction's predicates should be able to be calculated - // concurrently. - for _, tx := range txs { - txResults, err := core.CheckPredicates(rules, &predicateContext, tx) - if err != nil { - return nil, err - } - results.Set(tx.Hash(), txResults) - } - return results, nil -} From 85666131555d43d8fe0abd9c1dc3bd603ea27c0b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Aug 2025 15:32:03 -0400 Subject: [PATCH 32/45] nit --- sae/hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sae/hooks.go b/sae/hooks.go index f44ee23a08..8acda25340 100644 --- a/sae/hooks.go +++ b/sae/hooks.go @@ -198,7 +198,7 @@ func (h *hooks) constructBlock( txs, ) if err != nil { - return nil, fmt.Errorf("calculatePredicateResults: %w", err) + return nil, fmt.Errorf("CheckBlockPredicates: %w", err) } predicateResultsBytes, err := predicateResults.Bytes() From 500ddf51d3cc7dfe2422c091a5151695ebcf4e62 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 14:45:38 -0500 Subject: [PATCH 33/45] wip --- sae/{ => evm}/hooks.go | 2 +- sae/{ => evm}/txs.go | 2 +- sae/{ => evm}/vm.go | 6 +++--- sae/factory/factory.go | 19 +++++++++++++++++++ sae/main.go | 3 ++- 5 files changed, 26 insertions(+), 6 deletions(-) rename sae/{ => evm}/hooks.go (99%) rename sae/{ => evm}/txs.go (99%) rename sae/{ => evm}/vm.go (97%) create mode 100644 sae/factory/factory.go diff --git a/sae/hooks.go b/sae/evm/hooks.go similarity index 99% rename from sae/hooks.go rename to sae/evm/hooks.go index 8acda25340..357ac9dc7d 100644 --- a/sae/hooks.go +++ b/sae/evm/hooks.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package main +package evm import ( "context" diff --git a/sae/txs.go b/sae/evm/txs.go similarity index 99% rename from sae/txs.go rename to sae/evm/txs.go index ee44674c17..59878ddb69 100644 --- a/sae/txs.go +++ b/sae/evm/txs.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package main +package evm import ( "fmt" diff --git a/sae/vm.go b/sae/evm/vm.go similarity index 97% rename from sae/vm.go rename to sae/evm/vm.go index 6eabaa07a1..90b189914a 100644 --- a/sae/vm.go +++ b/sae/evm/vm.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package main +package evm import ( "context" @@ -22,11 +22,11 @@ import ( const atomicMempoolSize = 4096 // number of transactions -type vm struct { +type VM struct { *sae.VM // Populated by [vm.Initialize] } -func (vm *vm) Initialize( +func (vm *VM) Initialize( ctx context.Context, chainContext *snow.Context, db avalanchedb.Database, diff --git a/sae/factory/factory.go b/sae/factory/factory.go new file mode 100644 index 0000000000..ab2418c32a --- /dev/null +++ b/sae/factory/factory.go @@ -0,0 +1,19 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package factory + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms" + "github.com/ava-labs/coreth/sae/evm" + "github.com/ava-labs/strevm/adaptor" +) + +var _ vms.Factory = (*Factory)(nil) + +type Factory struct{} + +func (*Factory) New(logging.Logger) (interface{}, error) { + return adaptor.Convert(&evm.VM{}), nil +} diff --git a/sae/main.go b/sae/main.go index a4d20feee2..4757598e97 100644 --- a/sae/main.go +++ b/sae/main.go @@ -7,10 +7,11 @@ import ( "context" "github.com/ava-labs/avalanchego/vms/rpcchainvm" + "github.com/ava-labs/coreth/sae/evm" "github.com/ava-labs/strevm/adaptor" ) func main() { - vm := adaptor.Convert(&vm{}) + vm := adaptor.Convert(&evm.VM{}) rpcchainvm.Serve(context.Background(), vm) } From 4a780a9acdda9554d02b615ee59535ff754bbe3c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 15:02:50 -0500 Subject: [PATCH 34/45] Allow compilation --- go.mod | 4 +--- go.sum | 2 ++ sae/evm/vm.go | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bdb57587ec..d09338a65f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 - github.com/ava-labs/strevm v0.0.0-00010101000000-000000000000 + github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 github.com/fjl/gencodec v0.1.1 @@ -194,5 +194,3 @@ tool ( github.com/ava-labs/libevm/rlp/rlpgen github.com/onsi/ginkgo/v2/ginkgo ) - -replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm diff --git a/go.sum b/go.sum index 95467962c9..bc307caa1c 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 h1:obPwnVCkF5+B2f8WbTepHj0Zg github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13/go.mod h1:gsGr1ICjokI9CyPaaRHMqDoDCaT1VguC/IyOTx6rJ14= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26 h1:IjN4TSjp0VSCmXkdvomyJZIr6Usnb8/sQyuvODC0B/I= +github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= 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/sae/evm/vm.go b/sae/evm/vm.go index e84a6266ed..b79f790829 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -81,3 +81,10 @@ func (vm *VM) Initialize( ) return err } + +func (vm *VM) Shutdown(ctx context.Context) error { + if vm.VM == nil { + return nil + } + return vm.VM.Shutdown(ctx) +} From 87817f565602bc68c32251649b25027269c0d9a9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 15:38:52 -0500 Subject: [PATCH 35/45] Slow is smooth, smooth is fast --- plugin/evm/vm_atomic.go | 144 ----------------------------------- plugin/evm/vm_sae.go | 162 ---------------------------------------- 2 files changed, 306 deletions(-) delete mode 100644 plugin/evm/vm_atomic.go delete mode 100644 plugin/evm/vm_sae.go diff --git a/plugin/evm/vm_atomic.go b/plugin/evm/vm_atomic.go deleted file mode 100644 index 3db27e328a..0000000000 --- a/plugin/evm/vm_atomic.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package evm - -import ( - "context" - "net/http" - "sync" - "time" - - "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" -) - -var _ vmInterface = (*AtomicVM)(nil) - -type AtomicVM struct { - lock sync.RWMutex - value vmInterface -} - -func (a *AtomicVM) Get() vmInterface { - a.lock.RLock() - defer a.lock.RUnlock() - - return a.value -} - -func (a *AtomicVM) Set(value vmInterface) { - a.lock.Lock() - defer a.lock.Unlock() - - a.value = value -} - -func (a *AtomicVM) AppGossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error { - return a.Get().AppGossip(ctx, nodeID, msg) -} - -func (a *AtomicVM) AppRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, request []byte) error { - return a.Get().AppRequest(ctx, nodeID, requestID, deadline, request) -} - -func (a *AtomicVM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { - return a.Get().AppRequestFailed(ctx, nodeID, requestID, appErr) -} - -func (a *AtomicVM) AppResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error { - return a.Get().AppResponse(ctx, nodeID, requestID, response) -} - -func (a *AtomicVM) BuildBlock(ctx context.Context) (snowman.Block, error) { - return a.Get().BuildBlock(ctx) -} - -func (a *AtomicVM) BuildBlockWithContext(ctx context.Context, blockCtx *block.Context) (snowman.Block, error) { - return a.Get().BuildBlockWithContext(ctx, blockCtx) -} - -func (a *AtomicVM) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error { - return a.Get().Connected(ctx, nodeID, nodeVersion) -} - -func (a *AtomicVM) NewHTTPHandler(ctx context.Context) (http.Handler, error) { - return a.Get().NewHTTPHandler(ctx) -} - -func (a *AtomicVM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) { - return a.Get().CreateHandlers(ctx) -} - -func (a *AtomicVM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { - return a.Get().Disconnected(ctx, nodeID) -} - -func (a *AtomicVM) GetBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { - return a.Get().GetBlock(ctx, blkID) -} - -func (a *AtomicVM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) { - return a.Get().GetBlockIDAtHeight(ctx, height) -} - -// func (a *AtomicVM) GetLastStateSummary(ctx context.Context) (block.StateSummary, error) { -// return a.Get().GetLastStateSummary(ctx) -// } - -// func (a *AtomicVM) GetOngoingSyncStateSummary(ctx context.Context) (block.StateSummary, error) { -// return a.Get().GetOngoingSyncStateSummary(ctx) -// } - -// func (a *AtomicVM) GetStateSummary(ctx context.Context, summaryHeight uint64) (block.StateSummary, error) { -// return a.Get().GetStateSummary(ctx, summaryHeight) -// } - -func (a *AtomicVM) HealthCheck(ctx context.Context) (interface{}, error) { - return a.Get().HealthCheck(ctx) -} - -func (a *AtomicVM) Initialize(ctx context.Context, chainCtx *snow.Context, db database.Database, genesisBytes []byte, upgradeBytes []byte, configBytes []byte, fxs []*common.Fx, appSender common.AppSender) error { - return a.Get().Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, fxs, appSender) -} - -func (a *AtomicVM) LastAccepted(ctx context.Context) (ids.ID, error) { - return a.Get().LastAccepted(ctx) -} - -func (a *AtomicVM) ParseBlock(ctx context.Context, blockBytes []byte) (snowman.Block, error) { - return a.Get().ParseBlock(ctx, blockBytes) -} - -// func (a *AtomicVM) ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error) { -// return a.Get().ParseStateSummary(ctx, summaryBytes) -// } - -func (a *AtomicVM) SetPreference(ctx context.Context, blkID ids.ID) error { - return a.Get().SetPreference(ctx, blkID) -} - -func (a *AtomicVM) WaitForEvent(ctx context.Context) (common.Message, error) { - return a.Get().WaitForEvent(ctx) -} - -func (a *AtomicVM) SetState(ctx context.Context, state snow.State) error { - return a.Get().SetState(ctx, state) -} - -func (a *AtomicVM) Shutdown(ctx context.Context) error { - return a.Get().Shutdown(ctx) -} - -// func (a *AtomicVM) StateSyncEnabled(ctx context.Context) (bool, error) { -// return a.Get().StateSyncEnabled(ctx) -// } - -func (a *AtomicVM) Version(ctx context.Context) (string, error) { - return a.Get().Version(ctx) -} diff --git a/plugin/evm/vm_sae.go b/plugin/evm/vm_sae.go deleted file mode 100644 index 164d45f769..0000000000 --- a/plugin/evm/vm_sae.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package evm - -import ( - "context" - "errors" - "fmt" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/prefixdb" - "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/utils/set" - "github.com/ava-labs/strevm/adaptor" - "github.com/ava-labs/strevm/blocks" - - corethdatabase "github.com/ava-labs/avalanchego/vms/evm/database" - sae "github.com/ava-labs/strevm" -) - -type vmInterface interface { - block.ChainVM - block.BuildBlockWithContextChainVM - // block.StateSyncableVM -} - -var _ vmInterface = (*TransitionVM)(nil) - -type TransitionVM struct { - AtomicVM // current vm backend - outstandingAppRequests set.Set[ids.RequestID] // protected by atomicVM lock - - chainCtx *snow.Context - db database.Database - genesisBytes []byte - upgradeBytes []byte - configBytes []byte - fxs []*common.Fx - appSender common.AppSender - - preFork *VM - - saeVM *sae.VM - postFork vmInterface -} - -type saeWrapper struct { - *sae.VM -} - -func (*saeWrapper) Initialize(context.Context, *snow.Context, database.Database, []byte, []byte, []byte, []*common.Fx, common.AppSender) error { - return errors.New("unexpected call to saeWrapper.Initialize") -} - -func (t *TransitionVM) Initialize( - ctx context.Context, - chainCtx *snow.Context, - db database.Database, - genesisBytes []byte, - upgradeBytes []byte, - configBytes []byte, - fxs []*common.Fx, - appSender common.AppSender, -) error { - if err := t.preFork.Initialize(ctx, chainCtx, db, genesisBytes, upgradeBytes, configBytes, fxs, appSender); err != nil { - return fmt.Errorf("initializing preFork VM: %w", err) - } - t.Set(t.preFork) - - t.chainCtx = chainCtx - t.db = db - t.genesisBytes = genesisBytes - t.upgradeBytes = upgradeBytes - t.configBytes = configBytes - t.fxs = fxs - t.appSender = appSender - - lastAcceptedID, err := t.preFork.LastAccepted(ctx) - if err != nil { - return fmt.Errorf("getting preFork last accepted ID: %w", err) - } - - lastAccepted, err := t.preFork.GetBlock(ctx, lastAcceptedID) - if err != nil { - return fmt.Errorf("getting preFork last accepted %q: %w", lastAcceptedID, err) - } - - if err := t.afterAccept(ctx, lastAccepted); err != nil { - return fmt.Errorf("running post accept hook on %q: %w", lastAcceptedID, err) - } - return nil -} - -func (t *TransitionVM) afterAccept(ctx context.Context, block snowman.Block) error { - lastAcceptedTimestamp := block.Timestamp() - if !t.chainCtx.NetworkUpgrades.IsGraniteActivated(lastAcceptedTimestamp) { - return nil - } - - if err := t.preFork.Shutdown(ctx); err != nil { - return fmt.Errorf("shutting down preFork VM: %w", err) - } - - db := corethdatabase.New(prefixdb.NewNested(ethDBPrefix, t.db)) - _ = db - vm, err := sae.New( - ctx, - sae.Config{}, - ) - if err != nil { - return fmt.Errorf("initializing postFork VM: %w", err) - } - - t.postFork = adaptor.Convert[*blocks.Block](&saeWrapper{ - VM: vm, - }) - // t.Set(t.postFork) - return nil -} - -func (t *TransitionVM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { - request := ids.RequestID{ - NodeID: nodeID, - RequestID: requestID, - } - - t.AtomicVM.lock.Lock() - shouldHandle := t.outstandingAppRequests.Contains(request) - t.outstandingAppRequests.Remove(request) - - vm := t.AtomicVM.value - t.AtomicVM.lock.Unlock() - - if !shouldHandle { - return nil - } - return vm.AppRequestFailed(ctx, nodeID, requestID, appErr) -} - -func (t *TransitionVM) AppResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error { - request := ids.RequestID{ - NodeID: nodeID, - RequestID: requestID, - } - - t.AtomicVM.lock.Lock() - shouldHandle := t.outstandingAppRequests.Contains(request) - t.outstandingAppRequests.Remove(request) - - vm := t.AtomicVM.value - t.AtomicVM.lock.Unlock() - - if !shouldHandle { - return nil - } - return vm.AppResponse(ctx, nodeID, requestID, response) -} From c5720802b3300b47378e1e0dce7045952ee4b0e3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 15:41:06 -0500 Subject: [PATCH 36/45] nit --- sae/evm/hooks.go | 8 ++++++++ sae/evm/txs.go | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sae/evm/hooks.go b/sae/evm/hooks.go index 5ed9997d83..fa5b60b53e 100644 --- a/sae/evm/hooks.go +++ b/sae/evm/hooks.go @@ -43,6 +43,14 @@ var ( errEmptyBlock = errors.New("empty block") ) +// txs supports building blocks from a sequence of atomic transactions. +type txs interface { + NextTx() (*atomic.Tx, bool) + CancelCurrentTx(txID ids.ID) + DiscardCurrentTx(txID ids.ID) + DiscardCurrentTxs() +} + type hooks struct { ctx *snow.Context chainConfig *params.ChainConfig diff --git a/sae/evm/txs.go b/sae/evm/txs.go index 59878ddb69..aaa5b361ee 100644 --- a/sae/evm/txs.go +++ b/sae/evm/txs.go @@ -19,13 +19,6 @@ import ( "github.com/holiman/uint256" ) -type txs interface { - NextTx() (*atomic.Tx, bool) - CancelCurrentTx(txID ids.ID) - DiscardCurrentTx(txID ids.ID) - DiscardCurrentTxs() -} - type txSlice []*atomic.Tx func (t *txSlice) NextTx() (*atomic.Tx, bool) { From b8b128c7b36383a3ffcf66e0186df91392caf289 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 15:54:31 -0500 Subject: [PATCH 37/45] nit --- sae/evm/vm.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sae/evm/vm.go b/sae/evm/vm.go index b79f790829..15413f568a 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -79,7 +79,11 @@ func (vm *VM) Initialize( SnowCtx: chainContext, }, ) - return err + if err != nil { + return err + } + + return nil } func (vm *VM) Shutdown(ctx context.Context) error { From 028b7435ccaead9f7b3c61decd7fb160f1ff531a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 12:53:13 -0500 Subject: [PATCH 38/45] register mempool gossip --- go.mod | 2 +- go.sum | 4 +- plugin/evm/atomic/vm/vm.go | 7 +- sae/evm/vm.go | 140 ++++++++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index d09338a65f..70b167238a 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 - github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26 + github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 github.com/fjl/gencodec v0.1.1 diff --git a/go.sum b/go.sum index bc307caa1c..1dace25110 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 h1:obPwnVCkF5+B2f8WbTepHj0Zg github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13/go.mod h1:gsGr1ICjokI9CyPaaRHMqDoDCaT1VguC/IyOTx6rJ14= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= -github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26 h1:IjN4TSjp0VSCmXkdvomyJZIr6Usnb8/sQyuvODC0B/I= -github.com/ava-labs/strevm v0.0.0-20251110173938-998dcaa22a26/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= +github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012 h1:hZpmqSZrTvHCTLMqToUdXb8d4rCxURZ5oPiA1Olqhds= +github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= 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/plugin/evm/atomic/vm/vm.go b/plugin/evm/atomic/vm/vm.go index 4dd3d1ef1a..6a2eb0d3c9 100644 --- a/plugin/evm/atomic/vm/vm.go +++ b/plugin/evm/atomic/vm/vm.go @@ -263,7 +263,10 @@ func (vm *VM) onNormalOperationsStarted() error { vm.cancel = cancel atomicTxGossipMarshaller := atomic.TxMarshaller{} atomicTxGossipClient := vm.InnerVM.NewClient(p2p.AtomicTxGossipHandlerID) - atomicTxGossipMetrics, err := avalanchegossip.NewMetrics(vm.InnerVM.MetricRegistry(), atomicTxGossipNamespace) + atomicTxGossipMetrics, err := avalanchegossip.NewMetrics( + vm.InnerVM.MetricRegistry(), + atomicTxGossipNamespace, + ) if err != nil { return fmt.Errorf("failed to initialize atomic tx gossip metrics: %w", err) } @@ -304,7 +307,7 @@ func (vm *VM) onNormalOperationsStarted() error { config.TxGossipRequestsPerPeer, vm.InnerVM.P2PValidators(), vm.MetricRegistry(), - "atomic_tx_gossip", + atomicTxGossipNamespace, ) if err != nil { return fmt.Errorf("failed to initialize atomic tx gossip handler: %w", err) diff --git a/sae/evm/vm.go b/sae/evm/vm.go index 15413f568a..a567609d88 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -7,23 +7,45 @@ import ( "context" "encoding/json" "fmt" + "sync" + "time" avalanchedb "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/network/p2p" + avalanchegossip "github.com/ava-labs/avalanchego/network/p2p/gossip" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/evm/acp176" corethdb "github.com/ava-labs/avalanchego/vms/evm/database" + "github.com/ava-labs/coreth/plugin/evm/atomic" "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" + "github.com/ava-labs/coreth/plugin/evm/config" + "github.com/ava-labs/coreth/plugin/evm/gossip" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/state" sae "github.com/ava-labs/strevm" + "github.com/prometheus/client_golang/prometheus" ) -const atomicMempoolSize = 4096 // number of transactions +const ( + atomicMempoolSize = 4096 // number of transactions + atomicGossipNamespace = "atomic_tx_gossip" + atomicRegossipFrequency = 30 * time.Second + atomicPushFrequency = 100 * time.Millisecond + atomicPullFrequency = 1 * time.Second +) + +var atomicTxMarshaller = &atomic.TxMarshaller{} type VM struct { - *sae.VM // Populated by [vm.Initialize] + *sae.VM // Populated by [vm.Initialize] + ctx context.Context // Cancelled when onShutdown is called + onShutdown context.CancelFunc + wg sync.WaitGroup + + chainContext *snow.Context + atomicMempool *txpool.Mempool } func (vm *VM) Initialize( @@ -82,6 +104,117 @@ func (vm *VM) Initialize( if err != nil { return err } + vm.ctx, vm.onShutdown = context.WithCancel(context.Background()) + vm.chainContext = chainContext + + metrics := prometheus.NewRegistry() + vm.atomicMempool, err = txpool.NewMempool(mempoolTxs, metrics, vm.verifyTxAtTip) + if err != nil { + return fmt.Errorf("failed to initialize mempool: %w", err) + } + + return nil +} + +// TODO: Implement me +func (*VM) verifyTxAtTip(*atomic.Tx) error { + return nil +} + +func (vm *VM) SetState(ctx context.Context, state snow.State) error { + if state != snow.NormalOp { + return nil + } + + return vm.registerAtomicMempoolGossip() +} + +func (vm *VM) registerAtomicMempoolGossip() error { + // TODO: Don't make a new registry + metrics := prometheus.NewRegistry() + gossipMetrics, err := avalanchegossip.NewMetrics(metrics, atomicGossipNamespace) + if err != nil { + return fmt.Errorf("failed to initialize atomic tx gossip metrics: %w", err) + } + + handler, err := gossip.NewTxGossipHandler[*atomic.Tx]( + vm.chainContext.Log, + atomicTxMarshaller, + vm.atomicMempool, + gossipMetrics, + config.TxGossipTargetMessageSize, + config.TxGossipThrottlingPeriod, + config.TxGossipRequestsPerPeer, + vm.P2PValidators, + metrics, + atomicGossipNamespace, + ) + if err != nil { + return fmt.Errorf("failed to initialize atomic tx gossip handler: %w", err) + } + + // By registering the handler, we allow inbound network traffic for the + // [p2p.AtomicTxGossipHandlerID] protocol. + if err := vm.Network.AddHandler(p2p.AtomicTxGossipHandlerID, handler); err != nil { + return fmt.Errorf("failed to add atomic tx gossip handler: %w", err) + } + + client := vm.Network.NewClient(p2p.AtomicTxGossipHandlerID, vm.P2PValidators) + + // Start push gossip to disseminate any transactions issued by this node to + // a large percentage of the network. + { + pushGossiper, err := avalanchegossip.NewPushGossiper[*atomic.Tx]( + atomicTxMarshaller, + vm.atomicMempool, + vm.P2PValidators, + client, + gossipMetrics, + avalanchegossip.BranchingFactor{ + StakePercentage: .9, + }, + avalanchegossip.BranchingFactor{ + Validators: 1, + }, + config.PushGossipDiscardedElements, + config.TxGossipTargetMessageSize, + atomicRegossipFrequency, + ) + if err != nil { + return fmt.Errorf("failed to initialize atomic tx push gossiper: %w", err) + } + + vm.wg.Add(1) + go func() { + avalanchegossip.Every(vm.ctx, vm.chainContext.Log, pushGossiper, atomicPushFrequency) + vm.wg.Done() + }() + } + + // Start pull gossip to ensure this node quickly learns of transactions that + // have already been distributed to a large percentage of the network. + { + pullGossiper := avalanchegossip.NewPullGossiper[*atomic.Tx]( + vm.chainContext.Log, + atomicTxMarshaller, + vm.atomicMempool, + client, + gossipMetrics, + config.TxGossipPollSize, + ) + + pullGossiperWhenValidator := &avalanchegossip.ValidatorGossiper{ + Gossiper: pullGossiper, + NodeID: vm.chainContext.NodeID, + Validators: vm.P2PValidators, + } + + vm.wg.Add(1) + go func() { + avalanchegossip.Every(vm.ctx, vm.chainContext.Log, pullGossiperWhenValidator, atomicPullFrequency) + vm.wg.Done() + }() + } return nil } @@ -90,5 +223,8 @@ func (vm *VM) Shutdown(ctx context.Context) error { if vm.VM == nil { return nil } + vm.onShutdown() + defer vm.wg.Wait() + return vm.VM.Shutdown(ctx) } From 26f1854baa0a7dd1b9567f506d83388da5878c3a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 12:58:22 -0500 Subject: [PATCH 39/45] nit --- sae/evm/vm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sae/evm/vm.go b/sae/evm/vm.go index a567609d88..ba51d20b33 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -172,9 +172,10 @@ func (vm *VM) registerAtomicMempoolGossip() error { gossipMetrics, avalanchegossip.BranchingFactor{ StakePercentage: .9, + Validators: 100, }, avalanchegossip.BranchingFactor{ - Validators: 1, + Validators: 10, }, config.PushGossipDiscardedElements, config.TxGossipTargetMessageSize, From d3268a6e695459bd451bf929eaf1a548fed5559f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 16:18:46 -0500 Subject: [PATCH 40/45] wip --- plugin/evm/atomic/vm/api.go | 151 +++++++++++++++++++------------- plugin/evm/atomic/vm/vm.go | 29 ++---- plugin/evm/atomic/vm/vm_test.go | 16 +++- sae/evm/vm.go | 146 +++++++++++++++++++----------- 4 files changed, 202 insertions(+), 140 deletions(-) diff --git a/plugin/evm/atomic/vm/api.go b/plugin/evm/atomic/vm/api.go index ad1bc4974c..519fe77b8d 100644 --- a/plugin/evm/atomic/vm/api.go +++ b/plugin/evm/atomic/vm/api.go @@ -8,14 +8,19 @@ import ( "net/http" "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + avalanchegossip "github.com/ava-labs/avalanchego/network/p2p/gossip" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/json" + "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/libevm/log" + "go.uber.org/zap" "github.com/ava-labs/coreth/plugin/evm/atomic" + atomicstate "github.com/ava-labs/coreth/plugin/evm/atomic/state" "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" "github.com/ava-labs/coreth/plugin/evm/client" ) @@ -33,11 +38,22 @@ var ( ) // AvaxAPI offers Avalanche network related API methods -type AvaxAPI struct{ vm *VM } +type AvaxAPI struct { + vm *VM + + Context *snow.Context + Mempool *txpool.Mempool + PushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] + AcceptedTxs *atomicstate.AtomicRepository +} -// GetUTXOs gets all utxos for passed in addresses func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply *api.GetUTXOsReply) error { - log.Info("EVM: GetUTXOs called", "Addresses", args.Addresses) + service.Context.Log.Debug("API called", + zap.String("service", "avax"), + zap.String("method", "getUTXOs"), + logging.UserStrings("addresses", args.Addresses), + zap.Stringer("encoding", args.Encoding), + ) if len(args.Addresses) == 0 { return errNoAddresses @@ -50,24 +66,24 @@ func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply return errNoSourceChain } - sourceChainID, err := service.vm.Ctx.BCLookup.Lookup(args.SourceChain) + sourceChainID, err := service.Context.BCLookup.Lookup(args.SourceChain) if err != nil { return fmt.Errorf("problem parsing source chainID %q: %w", args.SourceChain, err) } - addrSet := set.Set[ids.ShortID]{} + var addrs set.Set[ids.ShortID] for _, addrStr := range args.Addresses { - addr, err := ParseServiceAddress(service.vm.Ctx, addrStr) + addr, err := ParseServiceAddress(service.Context, addrStr) if err != nil { return fmt.Errorf("couldn't parse address %q: %w", addrStr, err) } - addrSet.Add(addr) + addrs.Add(addr) } startAddr := ids.ShortEmpty startUTXO := ids.Empty if args.StartIndex.Address != "" || args.StartIndex.UTXO != "" { - startAddr, err = ParseServiceAddress(service.vm.Ctx, args.StartIndex.Address) + startAddr, err = ParseServiceAddress(service.Context, args.StartIndex.Address) if err != nil { return fmt.Errorf("couldn't parse start index address %q: %w", args.StartIndex.Address, err) } @@ -77,8 +93,8 @@ func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply } } - service.vm.Ctx.Lock.Lock() - defer service.vm.Ctx.Lock.Unlock() + service.Context.Lock.Lock() + defer service.Context.Lock.Unlock() limit := int(args.Limit) @@ -87,10 +103,10 @@ func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply } utxos, endAddr, endUTXOID, err := avax.GetAtomicUTXOs( - service.vm.Ctx.SharedMemory, + service.Context.SharedMemory, atomic.Codec, sourceChainID, - addrSet, + addrs, startAddr, startUTXO, limit, @@ -112,7 +128,7 @@ func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply reply.UTXOs[i] = str } - endAddress, err := FormatLocalAddress(service.vm.Ctx, endAddr) + endAddress, err := FormatLocalAddress(service.Context, endAddr) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } @@ -125,7 +141,12 @@ func (service *AvaxAPI) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply } func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response *api.JSONTxID) error { - log.Info("EVM: IssueTx called") + service.Context.Log.Debug("API called", + zap.String("service", "avax"), + zap.String("method", "getUTXOs"), + logging.UserString("tx", args.Tx), + zap.Stringer("encoding", args.Encoding), + ) txBytes, err := formatting.Decode(args.Encoding, args.Tx) if err != nil { @@ -142,10 +163,10 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response response.TxID = tx.ID() - service.vm.Ctx.Lock.Lock() - defer service.vm.Ctx.Lock.Unlock() + service.Context.Lock.Lock() + defer service.Context.Lock.Unlock() - err = service.vm.AtomicMempool.AddLocalTx(tx) + err = service.Mempool.AddLocalTx(tx) if err != nil && !errors.Is(err, txpool.ErrAlreadyKnown) { return err } @@ -154,38 +175,27 @@ func (service *AvaxAPI) IssueTx(_ *http.Request, args *api.FormattedTx, response // we push it to the network for inclusion. If the tx was previously added // to the mempool through p2p gossip, this will ensure this node also pushes // it to the network. - service.vm.AtomicTxPushGossiper.Add(tx) + service.PushGossiper.Add(tx) return nil } -// GetAtomicTxStatus returns the status of the specified transaction func (service *AvaxAPI) GetAtomicTxStatus(_ *http.Request, args *api.JSONTxID, reply *client.GetAtomicTxStatusReply) error { - log.Info("EVM: GetAtomicTxStatus called", "txID", args.TxID) + service.Context.Log.Debug("API called", + zap.String("service", "avax"), + zap.String("method", "getAtomicTxStatus"), + zap.Stringer("txID", args.TxID), + ) if args.TxID == ids.Empty { return errNilTxID } - service.vm.Ctx.Lock.Lock() - defer service.vm.Ctx.Lock.Unlock() - - _, status, height, _ := service.vm.GetAtomicTx(args.TxID) + service.Context.Lock.Lock() + defer service.Context.Lock.Unlock() - reply.Status = status - if status == atomic.Accepted { - // Since chain state updates run asynchronously with VM block acceptance, - // avoid returning [Accepted] until the chain state reaches the block - // containing the atomic tx. - lastAccepted := service.vm.InnerVM.Ethereum().BlockChain().LastAcceptedBlock() - if height > lastAccepted.NumberU64() { - reply.Status = atomic.Processing - return nil - } - - jsonHeight := json.Uint64(height) - reply.BlockHeight = &jsonHeight - } - return nil + var err error + _, reply.Status, reply.BlockHeight, err = service.getAtomicTx(args.TxID) + return err } type FormattedTx struct { @@ -193,43 +203,62 @@ type FormattedTx struct { BlockHeight *json.Uint64 `json:"blockHeight,omitempty"` } -// GetAtomicTx returns the specified transaction func (service *AvaxAPI) GetAtomicTx(_ *http.Request, args *api.GetTxArgs, reply *FormattedTx) error { - log.Info("EVM: GetAtomicTx called", "txID", args.TxID) + service.Context.Log.Debug("API called", + zap.String("service", "avax"), + zap.String("method", "getAtomicTx"), + zap.Stringer("txID", args.TxID), + zap.Stringer("encoding", args.Encoding), + ) if args.TxID == ids.Empty { return errNilTxID } - service.vm.Ctx.Lock.Lock() - defer service.vm.Ctx.Lock.Unlock() + service.Context.Lock.Lock() + defer service.Context.Lock.Unlock() - tx, status, height, err := service.vm.GetAtomicTx(args.TxID) + tx, status, height, err := service.getAtomicTx(args.TxID) if err != nil { return err } - if status == atomic.Unknown { return fmt.Errorf("could not find tx %s", args.TxID) } - txBytes, err := formatting.Encode(args.Encoding, tx.SignedBytes()) - if err != nil { - return err - } - reply.Tx = txBytes + reply.Tx, err = formatting.Encode(args.Encoding, tx.SignedBytes()) reply.Encoding = args.Encoding - if status == atomic.Accepted { - // Since chain state updates run asynchronously with VM block acceptance, - // avoid returning [Accepted] until the chain state reaches the block - // containing the atomic tx. - lastAccepted := service.vm.InnerVM.Ethereum().BlockChain().LastAcceptedBlock() - if height > lastAccepted.NumberU64() { - return nil + reply.BlockHeight = height + return err +} + +// TODO: these should be unexported after test refactor is done + +// getAtomicTx returns the requested transaction, status, and height. +// If the status is Unknown, then the returned transaction will be nil. +func (service *AvaxAPI) getAtomicTx(txID ids.ID) (*atomic.Tx, atomic.Status, *json.Uint64, error) { + tx, height, err := service.vm.AtomicTxRepository.GetByTxID(txID) + if err == nil { + // Since chain state updates run asynchronously with VM block + // acceptance, avoid returning [Accepted] until the chain state reaches + // the block containing the atomic tx. + if service.vm != nil && height > service.vm.InnerVM.Ethereum().BlockChain().LastAcceptedBlock().NumberU64() { + return tx, atomic.Processing, nil, nil } - jsonHeight := json.Uint64(height) - reply.BlockHeight = &jsonHeight + return tx, atomic.Accepted, (*json.Uint64)(&height), nil + } + if err != database.ErrNotFound { + return nil, atomic.Unknown, nil, err + } + + tx, dropped, found := service.Mempool.GetTx(txID) + switch { + case found && dropped: + return tx, atomic.Dropped, nil, nil + case found: + return tx, atomic.Processing, nil, nil + default: + return nil, atomic.Unknown, nil, nil } - return nil } diff --git a/plugin/evm/atomic/vm/vm.go b/plugin/evm/atomic/vm/vm.go index 6a2eb0d3c9..b4b07865e1 100644 --- a/plugin/evm/atomic/vm/vm.go +++ b/plugin/evm/atomic/vm/vm.go @@ -366,7 +366,13 @@ func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, erro if err != nil { return nil, err } - avaxAPI, err := rpc.NewHandler("avax", &AvaxAPI{vm}) + avaxAPI, err := rpc.NewHandler("avax", &AvaxAPI{ + vm: vm, + Context: vm.Ctx, + Mempool: vm.AtomicMempool, + PushGossiper: vm.AtomicTxPushGossiper, + AcceptedTxs: vm.AtomicTxRepository, + }) if err != nil { return nil, fmt.Errorf("failed to register service for AVAX API due to %w", err) } @@ -767,24 +773,3 @@ func (vm *VM) CurrentRules() extras.Rules { header := vm.InnerVM.Ethereum().BlockChain().CurrentHeader() return vm.rules(header.Number, header.Time) } - -// TODO: these should be unexported after test refactor is done - -// getAtomicTx returns the requested transaction, status, and height. -// If the status is Unknown, then the returned transaction will be nil. -func (vm *VM) GetAtomicTx(txID ids.ID) (*atomic.Tx, atomic.Status, uint64, error) { - if tx, height, err := vm.AtomicTxRepository.GetByTxID(txID); err == nil { - return tx, atomic.Accepted, height, nil - } else if err != avalanchedatabase.ErrNotFound { - return nil, atomic.Unknown, 0, err - } - tx, dropped, found := vm.AtomicMempool.GetTx(txID) - switch { - case found && dropped: - return tx, atomic.Dropped, 0, nil - case found: - return tx, atomic.Processing, 0, nil - default: - return nil, atomic.Unknown, 0, nil - } -} diff --git a/plugin/evm/atomic/vm/vm_test.go b/plugin/evm/atomic/vm/vm_test.go index 5102d57f69..d14abc7754 100644 --- a/plugin/evm/atomic/vm/vm_test.go +++ b/plugin/evm/atomic/vm/vm_test.go @@ -17,6 +17,7 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -240,17 +241,24 @@ func testIssueAtomicTxs(t *testing.T, scheme string) { require.NoError(err) require.Equal(lastAcceptedID, blk2.ID()) + s := AvaxAPI{ + Context: vm.Ctx, + Mempool: vm.AtomicMempool, + PushGossiper: vm.AtomicTxPushGossiper, + AcceptedTxs: vm.AtomicTxRepository, + } + // Check that both atomic transactions were indexed as expected. - indexedImportTx, status, height, err := vm.GetAtomicTx(importTx.ID()) + indexedImportTx, status, height, err := s.getAtomicTx(importTx.ID()) require.NoError(err) require.Equal(atomic.Accepted, status) - require.Equal(uint64(1), height, "expected height of indexed import tx to be 1") + require.Equal(common.PointerTo[json.Uint64](1), height, "expected height of indexed import tx to be 1") require.Equal(indexedImportTx.ID(), importTx.ID(), "expected ID of indexed import tx to match original txID") - indexedExportTx, status, height, err := vm.GetAtomicTx(exportTx.ID()) + indexedExportTx, status, height, err := s.getAtomicTx(exportTx.ID()) require.NoError(err) require.Equal(atomic.Accepted, status) - require.Equal(uint64(2), height, "expected height of indexed export tx to be 2") + require.Equal(common.PointerTo[json.Uint64](2), height, "expected height of indexed export tx to be 2") require.Equal(indexedExportTx.ID(), exportTx.ID(), "expected ID of indexed import tx to match original txID") } diff --git a/sae/evm/vm.go b/sae/evm/vm.go index ba51d20b33..e429f923de 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -7,10 +7,13 @@ import ( "context" "encoding/json" "fmt" + "net/http" "sync" "time" avalanchedb "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/versiondb" "github.com/ava-labs/avalanchego/network/p2p" avalanchegossip "github.com/ava-labs/avalanchego/network/p2p/gossip" "github.com/ava-labs/avalanchego/snow" @@ -18,9 +21,12 @@ import ( "github.com/ava-labs/avalanchego/vms/evm/acp176" corethdb "github.com/ava-labs/avalanchego/vms/evm/database" "github.com/ava-labs/coreth/plugin/evm/atomic" + atomicstate "github.com/ava-labs/coreth/plugin/evm/atomic/state" "github.com/ava-labs/coreth/plugin/evm/atomic/txpool" + atomicvm "github.com/ava-labs/coreth/plugin/evm/atomic/vm" "github.com/ava-labs/coreth/plugin/evm/config" "github.com/ava-labs/coreth/plugin/evm/gossip" + "github.com/ava-labs/coreth/utils/rpc" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/state" @@ -29,8 +35,10 @@ import ( ) const ( - atomicMempoolSize = 4096 // number of transactions - atomicGossipNamespace = "atomic_tx_gossip" + atomicMempoolSize = 4096 // number of transactions + atomicGossipNamespace = "atomic_tx_gossip" + avaxEndpoint = "/avax" + atomicRegossipFrequency = 30 * time.Second atomicPushFrequency = 100 * time.Millisecond atomicPullFrequency = 1 * time.Second @@ -44,8 +52,12 @@ type VM struct { onShutdown context.CancelFunc wg sync.WaitGroup - chainContext *snow.Context + chainContext *snow.Context + atomicMempool *txpool.Mempool + gossipMetrics avalanchegossip.Metrics + pushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] + acceptedTxs *atomicstate.AtomicRepository } func (vm *VM) Initialize( @@ -113,6 +125,41 @@ func (vm *VM) Initialize( return fmt.Errorf("failed to initialize mempool: %w", err) } + vm.gossipMetrics, err = avalanchegossip.NewMetrics(metrics, atomicGossipNamespace) + if err != nil { + return fmt.Errorf("failed to initialize atomic tx gossip metrics: %w", err) + } + + vm.pushGossiper, err = avalanchegossip.NewPushGossiper[*atomic.Tx]( + atomicTxMarshaller, + vm.atomicMempool, + vm.P2PValidators, + vm.Network.NewClient(p2p.AtomicTxGossipHandlerID, vm.P2PValidators), + vm.gossipMetrics, + avalanchegossip.BranchingFactor{ + StakePercentage: .9, + Validators: 100, + }, + avalanchegossip.BranchingFactor{ + Validators: 10, + }, + config.PushGossipDiscardedElements, + config.TxGossipTargetMessageSize, + atomicRegossipFrequency, + ) + if err != nil { + return fmt.Errorf("failed to initialize atomic tx push gossiper: %w", err) + } + + vm.acceptedTxs, err = atomicstate.NewAtomicTxRepository( + versiondb.New(memdb.New()), // TODO + atomic.Codec, + 0, // TODO + ) + if err != nil { + return fmt.Errorf("failed to create atomic repository: %w", err) + } + return nil } @@ -130,64 +177,38 @@ func (vm *VM) SetState(ctx context.Context, state snow.State) error { } func (vm *VM) registerAtomicMempoolGossip() error { - // TODO: Don't make a new registry - metrics := prometheus.NewRegistry() - gossipMetrics, err := avalanchegossip.NewMetrics(metrics, atomicGossipNamespace) - if err != nil { - return fmt.Errorf("failed to initialize atomic tx gossip metrics: %w", err) - } - - handler, err := gossip.NewTxGossipHandler[*atomic.Tx]( - vm.chainContext.Log, - atomicTxMarshaller, - vm.atomicMempool, - gossipMetrics, - config.TxGossipTargetMessageSize, - config.TxGossipThrottlingPeriod, - config.TxGossipRequestsPerPeer, - vm.P2PValidators, - metrics, - atomicGossipNamespace, - ) - if err != nil { - return fmt.Errorf("failed to initialize atomic tx gossip handler: %w", err) - } - - // By registering the handler, we allow inbound network traffic for the - // [p2p.AtomicTxGossipHandlerID] protocol. - if err := vm.Network.AddHandler(p2p.AtomicTxGossipHandlerID, handler); err != nil { - return fmt.Errorf("failed to add atomic tx gossip handler: %w", err) - } - - client := vm.Network.NewClient(p2p.AtomicTxGossipHandlerID, vm.P2PValidators) - - // Start push gossip to disseminate any transactions issued by this node to - // a large percentage of the network. { - pushGossiper, err := avalanchegossip.NewPushGossiper[*atomic.Tx]( + // TODO: Don't make a new registry + metrics := prometheus.NewRegistry() + handler, err := gossip.NewTxGossipHandler[*atomic.Tx]( + vm.chainContext.Log, atomicTxMarshaller, vm.atomicMempool, - vm.P2PValidators, - client, - gossipMetrics, - avalanchegossip.BranchingFactor{ - StakePercentage: .9, - Validators: 100, - }, - avalanchegossip.BranchingFactor{ - Validators: 10, - }, - config.PushGossipDiscardedElements, + vm.gossipMetrics, config.TxGossipTargetMessageSize, - atomicRegossipFrequency, + config.TxGossipThrottlingPeriod, + config.TxGossipRequestsPerPeer, + vm.P2PValidators, + metrics, + atomicGossipNamespace, ) if err != nil { - return fmt.Errorf("failed to initialize atomic tx push gossiper: %w", err) + return fmt.Errorf("failed to initialize atomic tx gossip handler: %w", err) + } + + // By registering the handler, we allow inbound network traffic for the + // [p2p.AtomicTxGossipHandlerID] protocol. + if err := vm.Network.AddHandler(p2p.AtomicTxGossipHandlerID, handler); err != nil { + return fmt.Errorf("failed to add atomic tx gossip handler: %w", err) } + } + // Start push gossip to disseminate any transactions issued by this node to + // a large percentage of the network. + { vm.wg.Add(1) go func() { - avalanchegossip.Every(vm.ctx, vm.chainContext.Log, pushGossiper, atomicPushFrequency) + avalanchegossip.Every(vm.ctx, vm.chainContext.Log, vm.pushGossiper, atomicPushFrequency) vm.wg.Done() }() } @@ -199,8 +220,8 @@ func (vm *VM) registerAtomicMempoolGossip() error { vm.chainContext.Log, atomicTxMarshaller, vm.atomicMempool, - client, - gossipMetrics, + vm.Network.NewClient(p2p.AtomicTxGossipHandlerID, vm.P2PValidators), + vm.gossipMetrics, config.TxGossipPollSize, ) @@ -220,6 +241,25 @@ func (vm *VM) registerAtomicMempoolGossip() error { return nil } +func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) { + apis, err := vm.VM.CreateHandlers(ctx) + if err != nil { + return nil, err + } + avaxAPI, err := rpc.NewHandler("avax", &atomicvm.AvaxAPI{ + Context: vm.chainContext, + Mempool: vm.atomicMempool, + PushGossiper: vm.pushGossiper, + AcceptedTxs: vm.acceptedTxs, + }) + if err != nil { + return nil, fmt.Errorf("making AVAX handler: %w", err) + } + vm.chainContext.Log.Info("AVAX API enabled") + apis[avaxEndpoint] = avaxAPI + return apis, nil +} + func (vm *VM) Shutdown(ctx context.Context) error { if vm.VM == nil { return nil From 7021dc14ffba7461e94daeb7c8244f10a0e7035f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 16:26:46 -0500 Subject: [PATCH 41/45] wip --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 70b167238a..a5098cfba3 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 - github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012 + github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 github.com/fjl/gencodec v0.1.1 diff --git a/go.sum b/go.sum index 1dace25110..f4dd3186c8 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 h1:obPwnVCkF5+B2f8WbTepHj0Zg github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13/go.mod h1:gsGr1ICjokI9CyPaaRHMqDoDCaT1VguC/IyOTx6rJ14= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4= github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= -github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012 h1:hZpmqSZrTvHCTLMqToUdXb8d4rCxURZ5oPiA1Olqhds= -github.com/ava-labs/strevm v0.0.0-20251112224440-07f63fde6012/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= +github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696 h1:M6UK92gkxAOgjGze8ZaRdSVWYR4DXgDHYLKW0VOyvdc= +github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= 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= From 0320d24ff42915eef1e959d53c66e804e75ec5ae Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 17:01:27 -0500 Subject: [PATCH 42/45] expose more apis --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a5098cfba3..adbc83799f 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/VictoriaMetrics/fastcache v1.12.1 github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 - github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 - github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696 + github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df + github.com/ava-labs/strevm v0.0.0-20251113220039-249188903424 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 github.com/fjl/gencodec v0.1.1 diff --git a/go.sum b/go.sum index f4dd3186c8..c3c85f6805 100644 --- a/go.sum +++ b/go.sum @@ -32,10 +32,10 @@ github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86 h1:Amek9uS github.com/ava-labs/avalanchego v1.13.6-0.20251028023847-6afe371e3b86/go.mod h1:wEiDa5Lc3oKm9l2AxJOXmLz00Wg7b3hUttgkfzgRoDA= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13 h1:obPwnVCkF5+B2f8WbTepHj0ZgiW21vKUgFCtATuAYNY= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.13/go.mod h1:gsGr1ICjokI9CyPaaRHMqDoDCaT1VguC/IyOTx6rJ14= -github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4= -github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= -github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696 h1:M6UK92gkxAOgjGze8ZaRdSVWYR4DXgDHYLKW0VOyvdc= -github.com/ava-labs/strevm v0.0.0-20251113212607-8f95b2a99696/go.mod h1:PaVuS0mBjmwCvNh/Vs0G/jM4hFgeKn21pzDBQXgIRxY= +github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df h1:kljCS+Ya/Ay0UaP/M4UVEJh/a+OOxU3EPfTjv6PV9LM= +github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/strevm v0.0.0-20251113220039-249188903424 h1:bUhLGvkp8WEZmcyrwujELgN/STu8vEGgYwNQz+mVuYE= +github.com/ava-labs/strevm v0.0.0-20251113220039-249188903424/go.mod h1:Hazz6GqJ+6vJVP3LgzBImvoZorMEqgZP4jv8YEwzYoQ= 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= From 9b25881198e0157bd86b1ef067714f3d8861d48c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 17:04:57 -0500 Subject: [PATCH 43/45] remove usage of vm --- plugin/evm/atomic/vm/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/evm/atomic/vm/api.go b/plugin/evm/atomic/vm/api.go index 519fe77b8d..5d87d83b3c 100644 --- a/plugin/evm/atomic/vm/api.go +++ b/plugin/evm/atomic/vm/api.go @@ -237,7 +237,7 @@ func (service *AvaxAPI) GetAtomicTx(_ *http.Request, args *api.GetTxArgs, reply // getAtomicTx returns the requested transaction, status, and height. // If the status is Unknown, then the returned transaction will be nil. func (service *AvaxAPI) getAtomicTx(txID ids.ID) (*atomic.Tx, atomic.Status, *json.Uint64, error) { - tx, height, err := service.vm.AtomicTxRepository.GetByTxID(txID) + tx, height, err := service.AcceptedTxs.GetByTxID(txID) if err == nil { // Since chain state updates run asynchronously with VM block // acceptance, avoid returning [Accepted] until the chain state reaches From 6f79a9ffa8a5fd22efad20935859ca1c1e22dc1b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 17:35:30 -0500 Subject: [PATCH 44/45] wip --- sae/evm/vm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sae/evm/vm.go b/sae/evm/vm.go index e429f923de..8b01ed2bca 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -110,7 +110,8 @@ func (vm *VM) Initialize( Target: acp176.MinTargetPerSecond, ExcessAfter: 0, }, - SnowCtx: chainContext, + SnowCtx: chainContext, + AppSender: appSender, }, ) if err != nil { From 84b5d4cf25bb0477068dac129084c961ec47847e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 18:49:45 -0500 Subject: [PATCH 45/45] Get ready to build blocks people --- sae/evm/vm.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sae/evm/vm.go b/sae/evm/vm.go index 8b01ed2bca..23cb945e54 100644 --- a/sae/evm/vm.go +++ b/sae/evm/vm.go @@ -261,6 +261,15 @@ func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, erro return apis, nil } +func (*VM) WaitForEvent(ctx context.Context) (common.Message, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-time.After(time.Second): + return common.PendingTxs, nil + } +} + func (vm *VM) Shutdown(ctx context.Context) error { if vm.VM == nil { return nil