diff --git a/go.mod b/go.mod index 749117abe2..04e87fa1a6 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +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/libevm v1.13.15-0.20251114170401-749b6cefda28 + github.com/ava-labs/strevm v0.0.0-20251114233539-0a61685c723b 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 @@ -55,15 +56,14 @@ require ( require ( connectrpc.com/connect v1.18.1 // indirect connectrpc.com/grpcreflect v1.3.0 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/StephenButtolph/canoto v0.17.2 // 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.20.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -79,10 +79,12 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.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/emicklei/go-restful/v3 v3.11.0 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/garslo/gogen v0.0.0-20230926014519-f497ca02dd4c // indirect + github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/getsentry/sentry-go v0.35.0 // indirect @@ -110,6 +112,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -142,6 +145,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.14 // indirect diff --git a/go.sum b/go.sum index 5c14d93ff6..9b098e82d7 100644 --- a/go.sum +++ b/go.sum @@ -26,12 +26,16 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 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.6-0.20251028023847-6afe371e3b86 h1:Amek9uS/P+Vbso0Sg3QnuLxRQZFBHqtm1AMyKD6UlYQ= 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/libevm v1.13.15-0.20251114170401-749b6cefda28 h1:7WTOLtDzlI+L1cU2PLc++xPxliDzfkJWkaxpXYmuaZM= +github.com/ava-labs/libevm v1.13.15-0.20251114170401-749b6cefda28/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/strevm v0.0.0-20251114233539-0a61685c723b h1:769mlxR0zAfCeleq0PGLk5P9p+UghjJJ6PU68qhfzpo= +github.com/ava-labs/strevm v0.0.0-20251114233539-0a61685c723b/go.mod h1:HKy8UC/JTPzTYA3RGPv/DHps3fGlWG1ODsq9GdI1WSk= 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= @@ -39,6 +43,7 @@ github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3M github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= @@ -131,6 +136,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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -144,6 +150,8 @@ github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHE github.com/ethereum/c-kzg-4844 v1.0.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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -153,8 +161,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/garslo/gogen v0.0.0-20230926014519-f497ca02dd4c h1:40KPNY+PW1YryU4J3aX9kNAgjXIXXyfloGA732AnOQc= -github.com/garslo/gogen v0.0.0-20230926014519-f497ca02dd4c/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= @@ -207,7 +215,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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -289,6 +300,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 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= @@ -468,6 +481,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= diff --git a/plugin/evm/atomic/vm/api.go b/plugin/evm/atomic/vm/api.go index ad1bc4974c..5d87d83b3c 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.AcceptedTxs.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/tx_semantic_verifier.go b/plugin/evm/atomic/vm/tx_semantic_verifier.go index 31cea11319..c01061ecae 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/secp256k1fx" "github.com/ava-labs/coreth/params/extras" @@ -82,86 +84,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("%w: (%d vs. %d)", errIncorrectNumCredentials, 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("%w from %s due to: %w", errFailedToFetchImportUTXOs, 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("%w: %w", errFailedToUnmarshalUTXO, 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] @@ -207,49 +147,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 *secp256k1fx.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 *secp256k1fx.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 *secp256k1fx.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), + ) + } - if len(utx.Ins) != len(stx.Creds) { - return fmt.Errorf("export tx contained %w want %d got %d", errIncorrectNumCredentials, len(utx.Ins), len(stx.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 +} + +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) } @@ -260,7 +334,7 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { if len(cred.Sigs) != 1 { return fmt.Errorf("%w want 1 signature for EVM Input Credential, but got %d", errIncorrectNumSignatures, 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 } @@ -268,6 +342,5 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { return errPublicKeySignatureMismatch } } - return nil } diff --git a/plugin/evm/atomic/vm/vm.go b/plugin/evm/atomic/vm/vm.go index 4dd3d1ef1a..b4b07865e1 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) @@ -363,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) } @@ -764,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/hooks.go b/sae/evm/hooks.go new file mode 100644 index 0000000000..fa5b60b53e --- /dev/null +++ b/sae/evm/hooks.go @@ -0,0 +1,348 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +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/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/evm/acp176" + "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/precompile/precompileconfig" + "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" + "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 + +var ( + _ hook.Points = &hooks{} + + 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 + 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 { + // 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) { + 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 { + // 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) { + 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 { + return nil, err + } + + rules := h.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time) + rulesExtra := params.GetRulesExtra(rules) + atomicTxs, err := packAtomicTxs( + ctx, + h.ctx, + &h.fx, + h.cache, + rulesExtra, + h.bootstrapped, + state, + header.BaseFee, + ancestorInputUTXOs, + potentialAtomicTxs, + ) + 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), + ) + potentialAtomicTxs.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) + // } + + predicateResults, err := core.CheckBlockPredicates( + rules, + &precompileconfig.PredicateContext{ + SnowCtx: h.ctx, + ProposerVMBlockCtx: blockContext, + }, + txs, + ) + if err != nil { + return nil, fmt.Errorf("CheckBlockPredicates: %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) 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 packAtomicTxs( + ctx context.Context, + snowContext *snow.Context, + fx *secp256k1fx.Fx, + cache *secp256k1.RecoverCache, + rules *extras.Rules, + bootstrapped bool, + state hook.State, + baseFee *big.Int, + ancestorInputUTXOs set.Set[ids.ID], + txs txs, +) ([]*atomic.Tx, error) { + var ( + cumulativeSize int + atomicTxs []*atomic.Tx + ) + for { + tx, exists := txs.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 { + txs.CancelCurrentTx(tx.ID()) + 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) { + txID := tx.ID() + snowContext.Log.Debug("discarding tx due to overlapping input utxos", + zap.Stringer("txID", txID), + ) + txs.DiscardCurrentTx(txID) + continue + } + + // 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 + } + + err = state.Apply(op) + if errors.Is(err, worstcase.ErrBlockTooFull) || errors.Is(err, worstcase.ErrQueueTooFull) { + // Send [tx] back to the mempool's tx heap. + txs.CancelCurrentTx(tx.ID()) + break + } + if err != nil { + txID := tx.ID() + snowContext.Log.Debug("discarding tx from mempool due to failed verification", + zap.Stringer("txID", txID), + zap.Error(err), + ) + txs.DiscardCurrentTx(txID) + continue + } + + atomicTxs = append(atomicTxs, tx) + ancestorInputUTXOs.Union(inputUTXOs) + + cumulativeSize += txSize + } + return atomicTxs, nil +} diff --git a/sae/evm/txs.go b/sae/evm/txs.go new file mode 100644 index 0000000000..aaa5b361ee --- /dev/null +++ b/sae/evm/txs.go @@ -0,0 +1,121 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +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 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) { + // 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 + } + gasPrice, err := atomic.EffectiveGasPrice(tx.UnsignedAtomicTx, avaxAssetID, true) + if err != nil { + return hook.Op{}, err + } + + 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 + } + + // TODO: This implementation assumes that the addresses are unique. + 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 + } + + // TODO: This implementation assumes that the addresses are unique. + 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) +} diff --git a/sae/evm/vm.go b/sae/evm/vm.go new file mode 100644 index 0000000000..7c9a047b11 --- /dev/null +++ b/sae/evm/vm.go @@ -0,0 +1,283 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +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" + "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" + 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" + sae "github.com/ava-labs/strevm" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + atomicMempoolSize = 4096 // number of transactions + atomicGossipNamespace = "atomic_tx_gossip" + avaxEndpoint = "/avax" + + 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] + ctx context.Context // Cancelled when onShutdown is called + onShutdown context.CancelFunc + wg sync.WaitGroup + + chainContext *snow.Context + + atomicMempool *txpool.Mempool + gossipMetrics avalanchegossip.Metrics + pushGossiper *avalanchegossip.PushGossiper[*atomic.Tx] + acceptedTxs *atomicstate.AtomicRepository +} + +func (vm *VM) Initialize( + ctx context.Context, + chainContext *snow.Context, + db avalanchedb.Database, + genesisBytes []byte, + configBytes []byte, + _ []byte, + _ []*common.Fx, + appSender common.AppSender, +) error { + ethDB := rawdb.NewDatabase(corethdb.New(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 + } + + mempoolTxs := txpool.NewTxs(chainContext, 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: mempoolTxs, + }, + ChainConfig: chainConfig, + DB: ethDB, + LastSynchronousBlock: sae.LastSynchronousBlock{ + Hash: genesisHash, + Target: acp176.MinTargetPerSecond, + ExcessAfter: 0, + }, + SnowCtx: chainContext, + AppSender: appSender, + }, + ) + 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) + } + + 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 +} + +// 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() + handler, err := gossip.NewTxGossipHandler[*atomic.Tx]( + vm.chainContext.Log, + atomicTxMarshaller, + vm.atomicMempool, + vm.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) + } + } + + // 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, vm.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, + vm.Network.NewClient(p2p.AtomicTxGossipHandlerID, vm.P2PValidators), + vm.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 +} + +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 +} + +// TODO: Correctly block until either the atomic mempool or the evm mempool has +// txs. +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 + } + vm.onShutdown() + defer vm.wg.Wait() + + return vm.VM.Shutdown(ctx) +} 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 new file mode 100644 index 0000000000..4757598e97 --- /dev/null +++ b/sae/main.go @@ -0,0 +1,17 @@ +// 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/vms/rpcchainvm" + "github.com/ava-labs/coreth/sae/evm" + "github.com/ava-labs/strevm/adaptor" +) + +func main() { + vm := adaptor.Convert(&evm.VM{}) + rpcchainvm.Serve(context.Background(), vm) +}