diff --git a/go.mod b/go.mod index f95b9b2201f2..4298ce92ea84 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca github.com/ava-labs/avalanchego/graft/subnet-evm v0.8.1-0.20251201175023-067762d6ce7d github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 + github.com/ava-labs/strevm v0.0.0-20251114203810-ee4dcf3ef268 github.com/btcsuite/btcd/btcutil v1.1.3 github.com/cespare/xxhash/v2 v2.3.0 github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 @@ -95,11 +96,11 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b ) -require github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 // indirect - require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect + github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa // indirect + github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 // indirect github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 // indirect github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 github.com/beorn7/perks v1.0.1 // indirect @@ -122,6 +123,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.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 @@ -143,6 +145,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -216,8 +219,8 @@ tool ( go.uber.org/mock/mockgen ) -replace github.com/ava-labs/avalanchego/graft/coreth => ./graft/coreth - -replace github.com/ava-labs/avalanchego/graft/subnet-evm => ./graft/subnet-evm - -replace github.com/ava-labs/avalanchego/graft/evm => ./graft/evm +replace ( + github.com/ava-labs/avalanchego/graft/coreth => ./graft/coreth + github.com/ava-labs/avalanchego/graft/evm => ./graft/evm + github.com/ava-labs/avalanchego/graft/subnet-evm => ./graft/subnet-evm +) diff --git a/go.sum b/go.sum index 181c530d4056..a39959f6fc0f 100644 --- a/go.sum +++ b/go.sum @@ -71,12 +71,16 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 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/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= +github.com/ava-labs/strevm v0.0.0-20251114203810-ee4dcf3ef268 h1:+4tMZPJfdvFp5lmikOZp9YSX7hdElcIhUN1Cpe2GKeo= +github.com/ava-labs/strevm v0.0.0-20251114203810-ee4dcf3ef268/go.mod h1:sM/eCcmkKJ6KpJ219s6AbfuQsQytmAOXbr9ZvsK/HGg= 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= @@ -193,6 +197,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= diff --git a/graft/coreth/plugin/evm/atomic/vm/tx_semantic_verifier.go b/graft/coreth/plugin/evm/atomic/vm/tx_semantic_verifier.go index 699dafa25694..bbb9db741766 100644 --- a/graft/coreth/plugin/evm/atomic/vm/tx_semantic_verifier.go +++ b/graft/coreth/plugin/evm/atomic/vm/tx_semantic_verifier.go @@ -9,6 +9,7 @@ import ( "fmt" "math/big" + avagoatomic "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/graft/coreth/params/extras" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/extension" @@ -18,6 +19,7 @@ import ( "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" ) @@ -81,86 +83,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] @@ -206,49 +146,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) } @@ -259,7 +333,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 } @@ -267,6 +341,5 @@ func (s *semanticVerifier) ExportTx(utx *atomic.UnsignedExportTx) error { return errPublicKeySignatureMismatch } } - return nil } diff --git a/node/node.go b/node/node.go index 89ec9774bfce..61a6844a0398 100644 --- a/node/node.go +++ b/node/node.go @@ -84,9 +84,9 @@ import ( "github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime" databasefactory "github.com/ava-labs/avalanchego/database/factory" - coreth "github.com/ava-labs/avalanchego/graft/coreth/plugin/factory" avmconfig "github.com/ava-labs/avalanchego/vms/avm/config" platformconfig "github.com/ava-labs/avalanchego/vms/platformvm/config" + coreth "github.com/ava-labs/avalanchego/vms/saevm/factory" ) const ( diff --git a/vms/saevm/evm/hooks.go b/vms/saevm/evm/hooks.go new file mode 100644 index 000000000000..3b4a0f215b4d --- /dev/null +++ b/vms/saevm/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/graft/coreth/core" + "github.com/ava-labs/avalanchego/graft/coreth/params" + "github.com/ava-labs/avalanchego/graft/coreth/params/extras" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" + "github.com/ava-labs/avalanchego/graft/coreth/precompile/precompileconfig" + "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/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/avalanchego/graft/coreth/plugin/evm/atomic/txpool" + atomicvm "github.com/ava-labs/avalanchego/graft/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/vms/saevm/evm/txs.go b/vms/saevm/evm/txs.go new file mode 100644 index 000000000000..e54b458756de --- /dev/null +++ b/vms/saevm/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/graft/coreth/plugin/evm/atomic" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" + "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/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/vms/saevm/evm/vm.go b/vms/saevm/evm/vm.go new file mode 100644 index 000000000000..e3578b30ea17 --- /dev/null +++ b/vms/saevm/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/graft/coreth/plugin/evm/atomic" + atomicstate "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/state" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/txpool" + atomicvm "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic/vm" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/config" + "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/gossip" + "github.com/ava-labs/avalanchego/graft/coreth/utils/rpc" + "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/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/vms/saevm/factory/factory.go b/vms/saevm/factory/factory.go new file mode 100644 index 000000000000..4b1788525747 --- /dev/null +++ b/vms/saevm/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/avalanchego/vms/saevm/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/vms/saevm/main.go b/vms/saevm/main.go new file mode 100644 index 000000000000..8dfc149069aa --- /dev/null +++ b/vms/saevm/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/avalanchego/vms/saevm/evm" + "github.com/ava-labs/strevm/adaptor" +) + +func main() { + vm := adaptor.Convert(&evm.VM{}) + rpcchainvm.Serve(context.Background(), vm) +} diff --git a/wallet/chain/c/wallet.go b/wallet/chain/c/wallet.go index 59bd924b1090..1975019ea2aa 100644 --- a/wallet/chain/c/wallet.go +++ b/wallet/chain/c/wallet.go @@ -8,13 +8,13 @@ import ( "math/big" "time" - "github.com/ava-labs/avalanchego/graft/coreth/ethclient" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/client" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/rpc" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + "github.com/ava-labs/libevm/ethclient" ethcommon "github.com/ava-labs/libevm/common" ) @@ -195,7 +195,7 @@ func (w *wallet) baseFee(options []common.Option) (*big.Int, error) { } ctx := ops.Context() - return w.ethClient.EstimateBaseFee(ctx) + return w.ethClient.SuggestGasPrice(ctx) } // TODO: Upstream this function into coreth. diff --git a/wallet/subnet/primary/api.go b/wallet/subnet/primary/api.go index f50d13084653..314bdbba2304 100644 --- a/wallet/subnet/primary/api.go +++ b/wallet/subnet/primary/api.go @@ -9,7 +9,6 @@ import ( "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/codec" - "github.com/ava-labs/avalanchego/graft/coreth/ethclient" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/atomic" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/client" "github.com/ava-labs/avalanchego/ids" @@ -23,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/wallet/chain/c" "github.com/ava-labs/avalanchego/wallet/chain/p" "github.com/ava-labs/avalanchego/wallet/chain/x" + "github.com/ava-labs/libevm/ethclient" pbuilder "github.com/ava-labs/avalanchego/wallet/chain/p/builder" xbuilder "github.com/ava-labs/avalanchego/wallet/chain/x/builder" diff --git a/wallet/subnet/primary/examples/spam-evm/main.go b/wallet/subnet/primary/examples/spam-evm/main.go new file mode 100644 index 000000000000..36df7daad6ac --- /dev/null +++ b/wallet/subnet/primary/examples/spam-evm/main.go @@ -0,0 +1,89 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "errors" + "log" + "math/big" + "time" + + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/ethclient" + "github.com/ava-labs/libevm/params" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + + ethereum "github.com/ava-labs/libevm" +) + +// maxFeePerGas is the fee that transactions issued by this test will pay. +const maxFeePerGas = 1000 * params.GWei + +var gasPrice = big.NewInt(maxFeePerGas) + +func main() { + ctx := context.Background() + const ( + chainUUID = "C" + uri = primary.LocalAPIURI + "/ext/bc/" + chainUUID + "/rpc" + ) + c, err := ethclient.DialContext(ctx, uri) + if err != nil { + log.Fatal(err) + } + + chainID, err := c.ChainID(ctx) + if err != nil { + log.Fatal(err) + } + signer := types.NewLondonSigner(chainID) + + key := genesis.EWOQKey + ecdsaKey := key.ToECDSA() + eoa := crypto.PubkeyToAddress(ecdsaKey.PublicKey) + nonce, err := c.NonceAt(context.Background(), eoa, nil) + if err != nil { + log.Fatal(err) + } + + for { + tx := types.NewTx(&types.LegacyTx{ + Nonce: nonce, + GasPrice: gasPrice, + Gas: 1_000_000, // params.TxGas, + To: &eoa, + }) + + tx, err = types.SignTx(tx, signer, ecdsaKey) + if err != nil { + log.Fatal(err) + } + + txHash := tx.Hash() + log.Printf("sending tx %s with nonce %d\n", txHash, nonce) + + err = c.SendTransaction(ctx, tx) + if err != nil { + log.Fatal(err) + } + + for { + _, err = c.TransactionReceipt(ctx, txHash) + if err == nil { + break // Transaction was confirmed + } + if !errors.Is(err, ethereum.NotFound) { + log.Fatal(err) // Unexpected error + } + + time.Sleep(100 * time.Millisecond) + } + + nonce++ + } +} diff --git a/wallet/subnet/primary/wallet.go b/wallet/subnet/primary/wallet.go index 52951121c328..00f020075f0e 100644 --- a/wallet/subnet/primary/wallet.go +++ b/wallet/subnet/primary/wallet.go @@ -5,6 +5,7 @@ package primary import ( "context" + "fmt" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" @@ -88,18 +89,18 @@ func MakeWallet( avaxAddrs := avaxKeychain.Addresses() avaxState, err := FetchState(ctx, uri, avaxAddrs) if err != nil { - return nil, err + return nil, fmt.Errorf("fetching avax state: %w", err) } ethAddrs := ethKeychain.EthAddresses() ethState, err := FetchEthState(ctx, uri, ethAddrs) if err != nil { - return nil, err + return nil, fmt.Errorf("fetching eth state: %w", err) } owners, err := platformvm.GetOwners(avaxState.PClient, ctx, config.SubnetIDs, config.ValidationIDs) if err != nil { - return nil, err + return nil, fmt.Errorf("fetching p-chain owners: %w", err) } pUTXOs := common.NewChainUTXOs(constants.PlatformChainID, avaxState.UTXOs)