diff --git a/go.mod b/go.mod index b3207118f0..6f2cc84b87 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ava-labs/avalanchego v1.13.5-rc.4 github.com/ava-labs/firewood-go-ethhash/ffi v0.0.12 github.com/ava-labs/libevm v1.13.15-0.20250904180142-72e9ad796212 + 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 @@ -42,15 +43,14 @@ require ( ) require ( - 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.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 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.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.9.1 // indirect @@ -66,8 +66,10 @@ 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/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.6.0 // indirect github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect @@ -97,6 +99,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // 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 @@ -130,6 +133,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 @@ -173,3 +177,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/ava-labs/strevm => /Users/stephen/go/src/github.com/ava-labs/strevm diff --git a/go.sum b/go.sum index 81c1ce31c0..225e4687e9 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ 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.5-rc.4 h1:5aPlOFQFbKBLvUzsxLgybGhOCqEyi74x1qcgntVtzww= github.com/ava-labs/avalanchego v1.13.5-rc.4/go.mod h1:6bXxADKsAkU/f9Xme0gFJGRALp3IVzwq8NMDyx6ucRs= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.12 h1:aMcrLbpJ/dyu2kZDf/Di/4JIWsUcYPyTDKymiHpejt0= @@ -73,6 +75,7 @@ github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsy github.com/bits-and-blooms/bitset v1.10.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= @@ -165,6 +168,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= @@ -181,6 +185,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.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -245,7 +251,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= @@ -369,6 +378,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= @@ -555,6 +566,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/atomic/vm/tx_semantic_verifier.go b/plugin/evm/atomic/vm/tx_semantic_verifier.go index 886bc78f26..8a76e1c7f9 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" @@ -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 *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), + ) + } + + 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 } diff --git a/plugin/evm/vm_atomic.go b/plugin/evm/vm_atomic.go new file mode 100644 index 0000000000..3db27e328a --- /dev/null +++ b/plugin/evm/vm_atomic.go @@ -0,0 +1,144 @@ +// 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 new file mode 100644 index 0000000000..c0dd756177 --- /dev/null +++ b/plugin/evm/vm_sae.go @@ -0,0 +1,162 @@ +// 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/coreth/plugin/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.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.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) +} diff --git a/sae/hooks.go b/sae/hooks.go new file mode 100644 index 0000000000..8acda25340 --- /dev/null +++ b/sae/hooks.go @@ -0,0 +1,340 @@ +// 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/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/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" + "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") +) + +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/main.go b/sae/main.go new file mode 100644 index 0000000000..a4d20feee2 --- /dev/null +++ b/sae/main.go @@ -0,0 +1,16 @@ +// 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/strevm/adaptor" +) + +func main() { + vm := adaptor.Convert(&vm{}) + rpcchainvm.Serve(context.Background(), vm) +} diff --git a/sae/txs.go b/sae/txs.go new file mode 100644 index 0000000000..ee44674c17 --- /dev/null +++ b/sae/txs.go @@ -0,0 +1,128 @@ +// 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) { + // 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/vm.go b/sae/vm.go new file mode 100644 index 0000000000..6eabaa07a1 --- /dev/null +++ b/sae/vm.go @@ -0,0 +1,83 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +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/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" + "github.com/ava-labs/libevm/core/state" + sae "github.com/ava-labs/strevm" +) + +const atomicMempoolSize = 4096 // number of transactions + +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, + _ []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 + } + + 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, + }, + ) + return err +}