diff --git a/.gitignore b/.gitignore index 22c1168b29..5dea350248 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,4 @@ cmd/simulator/.simulator/* # goreleaser dist/ -# generator rpc file for e2e tests -contract-examples/dynamic_rpc.json \ No newline at end of file +node_modules diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index ba8561eac1..6dd0e7e76f 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -95,6 +95,7 @@ func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { // Returns the topics for the event including the event signature (if non-anonymous event) and // hashes derived from indexed arguments and the packed data of non-indexed args according to // the event ABI specification. +// The order of arguments must match the order of the event definition. // https://docs.soliditylang.org/en/v0.8.17/abi-spec.html#indexed-event-encoding. // Note: PackEvent does not support array (fixed or dynamic-size) or struct types. func (abi ABI) PackEvent(name string, args ...interface{}) ([]common.Hash, []byte, error) { diff --git a/core/predicate_check.go b/core/predicate_check.go index dbbc8eab04..8399dccb6c 100644 --- a/core/predicate_check.go +++ b/core/predicate_check.go @@ -73,4 +73,4 @@ func checkProposerPrecompilePredicates(rules params.Rules, predicateContext *pre } return nil -} \ No newline at end of file +} diff --git a/core/predicate_check_test.go b/core/predicate_check_test.go index b3f3778a58..af06422b9f 100644 --- a/core/predicate_check_test.go +++ b/core/predicate_check_test.go @@ -15,33 +15,34 @@ import ( "github.com/stretchr/testify/require" ) - var ( +var ( _ precompileconfig.PrecompilePredicater = (*mockPredicater)(nil) - _ precompileconfig.ProposerPredicater = (*mockProposerPredicater)(nil) - ) + _ precompileconfig.ProposerPredicater = (*mockProposerPredicater)(nil) +) - type mockPredicater struct { +type mockPredicater struct { predicateFunc func(*precompileconfig.PrecompilePredicateContext, []byte) error } -func (m *mockPredicater) VerifyPredicate(predicateContext *precompileconfig.PrecompilePredicateContext, b []byte) error { return m.predicateFunc(predicateContext, b) } - +func (m *mockPredicater) VerifyPredicate(predicateContext *precompileconfig.PrecompilePredicateContext, b []byte) error { + return m.predicateFunc(predicateContext, b) +} type mockProposerPredicater struct { predicateFunc func(*precompileconfig.ProposerPredicateContext, []byte) error } -func (m *mockProposerPredicater) VerifyPredicate(predicateContext *precompileconfig.ProposerPredicateContext, b []byte) error { return m.predicateFunc(predicateContext, b) } - - +func (m *mockProposerPredicater) VerifyPredicate(predicateContext *precompileconfig.ProposerPredicateContext, b []byte) error { + return m.predicateFunc(predicateContext, b) +} type predicateCheckTest struct { - address common.Address - predicater precompileconfig.PrecompilePredicater - proposerPredicater precompileconfig.ProposerPredicater - accessList types.AccessList + address common.Address + predicater precompileconfig.PrecompilePredicater + proposerPredicater precompileconfig.ProposerPredicater + accessList types.AccessList proposerPredicateContext precompileconfig.ProposerPredicateContext - expectedErr error + expectedErr error } func TestCheckPredicate(t *testing.T) { @@ -61,13 +62,13 @@ func TestCheckPredicate(t *testing.T) { expectedErr: nil, }, "proposer predicate, no access list passes": { - address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), proposerPredicater: &mockProposerPredicater{predicateFunc: func(*precompileconfig.ProposerPredicateContext, []byte) error { return nil }}, - expectedErr: nil, + expectedErr: nil, }, "predicate, no access list passes": { - address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), - predicater: &mockPredicater{predicateFunc: func(*precompileconfig.PrecompilePredicateContext, []byte) error { return nil }}, + address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), + predicater: &mockPredicater{predicateFunc: func(*precompileconfig.PrecompilePredicateContext, []byte) error { return nil }}, expectedErr: nil, }, "predicate with valid access list passes": { @@ -78,7 +79,7 @@ func TestCheckPredicate(t *testing.T) { } else { return fmt.Errorf("unexpected bytes: 0x%x", b) } - }}, + }}, accessList: types.AccessList([]types.AccessTuple{ { Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), @@ -116,7 +117,7 @@ func TestCheckPredicate(t *testing.T) { } else { return fmt.Errorf("unexpected bytes: 0x%x", b) } - }}, + }}, accessList: types.AccessList([]types.AccessTuple{ { Address: common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"), @@ -157,7 +158,7 @@ func TestCheckPredicate(t *testing.T) { if test.predicater != nil { rules.PredicatePrecompiles[test.address] = test.predicater } - + // Specify only the access list, since this test should not depend on any other values tx := types.NewTx(&types.DynamicFeeTx{ AccessList: test.accessList, diff --git a/plugin/evm/block.go b/plugin/evm/block.go index e782c74315..4384316442 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,23 +61,35 @@ func (b *Block) Accept(context.Context) error { if err := vm.blockChain.Accept(b.ethBlock); err != nil { return fmt.Errorf("chain could not accept %s: %w", b.ID(), err) } + // Call Accept for relevant precompile logs. This should apply DB operations to the VM's versionDB // to be committed atomically with marking this block as accepted. - if err := b.handlePrecompileAccept(); err != nil { + sharedMemoryWriter := NewSharedMemoryWriter() + if err := b.handlePrecompileAccept(sharedMemoryWriter); err != nil { return err } if err := vm.acceptedBlockDB.Put(lastAcceptedKey, b.id[:]); err != nil { return fmt.Errorf("failed to put %s as the last accepted block: %w", b.ID(), err) } - return vm.db.Commit() + // Get pending operations on the vm's versionDB so we can apply them atomically + // with the shared memory requests. + vdbBatch, err := vm.db.CommitBatch() + if err != nil { + return fmt.Errorf("failed to get commit batch: %w", err) + } + + // Apply any shared memory requests that accumulated from processing the logs + // of the accepted block (generated by precompiles) atomically with other pending + // changes to the vm's versionDB. + return vm.ctx.SharedMemory.Apply(sharedMemoryWriter.requests, vdbBatch) } // handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements // contract.Accepter // This function assumes that the Accept function will ONLY operate on state maintained in the VM's versiondb. // This ensures that any DB operations are performed atomically with marking the block as accepted. -func (b *Block) handlePrecompileAccept() error { +func (b *Block) handlePrecompileAccept(sharedMemoryWriter *sharedMemoryWriter) error { rules := b.vm.chainConfig.AvalancheRules(b.ethBlock.Number(), b.ethBlock.Timestamp()) // Short circuit early if there are no precompile accepters to execute if len(rules.AccepterPrecompiles) == 0 { @@ -97,7 +109,11 @@ func (b *Block) handlePrecompileAccept() error { continue } - if err := accepter.Accept(log.TxHash, txIndex, log.Topics, log.Data); err != nil { + acceptCtx := &precompileconfig.AcceptContext{ + SnowCtx: b.vm.ctx, + SharedMemory: sharedMemoryWriter, + } + if err := accepter.Accept(acceptCtx, log.TxHash, txIndex, log.Topics, log.Data); err != nil { return err } } diff --git a/plugin/evm/shared_memory_writer.go b/plugin/evm/shared_memory_writer.go new file mode 100644 index 0000000000..88589720ee --- /dev/null +++ b/plugin/evm/shared_memory_writer.go @@ -0,0 +1,37 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" +) + +var _ precompileconfig.SharedMemoryWriter = &sharedMemoryWriter{} + +type sharedMemoryWriter struct { + requests map[ids.ID]*atomic.Requests +} + +func NewSharedMemoryWriter() *sharedMemoryWriter { + return &sharedMemoryWriter{ + requests: make(map[ids.ID]*atomic.Requests), + } +} + +func (s *sharedMemoryWriter) AddSharedMemoryRequests(chainID ids.ID, requests *atomic.Requests) { + mergeAtomicOpsToMap(s.requests, chainID, requests) +} + +// mergeAtomicOps merges atomic ops for [chainID] represented by [requests] +// to the [output] map provided. +func mergeAtomicOpsToMap(output map[ids.ID]*atomic.Requests, chainID ids.ID, requests *atomic.Requests) { + if request, exists := output[chainID]; exists { + request.PutRequests = append(request.PutRequests, requests.PutRequests...) + request.RemoveRequests = append(request.RemoveRequests, requests.RemoveRequests...) + } else { + output[chainID] = requests + } +} diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 29a6a52937..b65a99b02f 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -36,7 +36,9 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api/keystore" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database/manager" + "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/choices" @@ -145,12 +147,14 @@ func NewContext() *snow.Context { } // If [genesisJSON] is empty, defaults to using [genesisJSONLatest] -func setupGenesis(t *testing.T, +func setupGenesis( + t *testing.T, genesisJSON string, ) (*snow.Context, manager.Manager, []byte, chan engCommon.Message, + *atomic.Memory, ) { if len(genesisJSON) == 0 { genesisJSON = genesisJSONLatest @@ -164,6 +168,10 @@ func setupGenesis(t *testing.T, Patch: 5, }) + // initialize the atomic memory + atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDBManager.Current().Database)) + ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID) + // NB: this lock is intentionally left locked when this function returns. // The caller of this function is responsible for unlocking. ctx.Lock.Lock() @@ -180,7 +188,7 @@ func setupGenesis(t *testing.T, issuer := make(chan engCommon.Message, 1) prefixedDBManager := baseDBManager.NewPrefixDBManager([]byte{1}) - return ctx, prefixedDBManager, genesisBytes, issuer + return ctx, prefixedDBManager, genesisBytes, issuer, atomicMemory } // GenesisVM creates a VM instance with the genesis test bytes and returns @@ -197,7 +205,7 @@ func GenesisVM(t *testing.T, *engCommon.SenderTest, ) { vm := &VM{} - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, genesisJSON) appSender := &engCommon.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } @@ -406,7 +414,7 @@ func TestSubnetEVMUpgradeRequiredAtGenesis(t *testing.T) { } for _, test := range genesisTests { - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, test.genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, test.genesisJSON) vm := &VM{} err := vm.Initialize( context.Background(), @@ -2030,7 +2038,7 @@ func TestConfigureLogLevel(t *testing.T) { for _, test := range configTests { t.Run(test.name, func(t *testing.T) { vm := &VM{} - ctx, dbManager, genesisBytes, issuer := setupGenesis(t, test.genesisJSON) + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, test.genesisJSON) appSender := &engCommon.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 4880f0e348..7ff7e7dc66 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -81,9 +81,14 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { } // restart the vm - ctx := NewContext() + // Hack: registering metrics uses global variables, so we need to disable metrics here so that we + // can initialize the VM twice. + metrics.Enabled = false + defer func() { + metrics.Enabled = true + }() if err := vm.Initialize( - context.Background(), ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*common.Fx{}, appSender, + context.Background(), vm.ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*common.Fx{}, appSender, ); err != nil { t.Fatal(err) } diff --git a/precompile/precompileconfig/config.go b/precompile/precompileconfig/config.go index 47ad278523..58c7e8c807 100644 --- a/precompile/precompileconfig/config.go +++ b/precompile/precompileconfig/config.go @@ -7,6 +7,8 @@ package precompileconfig import ( "math/big" + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ethereum/go-ethereum/common" @@ -72,11 +74,23 @@ type ProposerPredicater interface { VerifyPredicate(proposerPredicateContext *ProposerPredicateContext, storageSlots []byte) error } +// SharedMemoryWriter defines an interface to allow a precompile's Accepter to write operations +// into shared memory to be committed atomically on block accept. +type SharedMemoryWriter interface { + AddSharedMemoryRequests(chainID ids.ID, requests *atomic.Requests) +} + +// AcceptContext defines the context passed in to a precompileconfig's Accepter +type AcceptContext struct { + SnowCtx *snow.Context + SharedMemory SharedMemoryWriter +} + // Accepter is an optional interface for StatefulPrecompiledContracts to implement. // If implemented, Accept will be called for every log with the address of the precompile when the block is accepted. // WARNING: If you are implementing a custom precompile, beware that subnet-evm // will not maintain backwards compatibility of this interface and your code should not // rely on this. Designed for use only by precompiles that ship with subnet-evm. type Accepter interface { - Accept(txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error + Accept(acceptCtx *AcceptContext, txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error }