From 50906b585e8bda60de5dd47dc47321d6cef60fca Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 13 Nov 2024 12:53:14 +0300 Subject: [PATCH 1/4] add regression test --- plugin/evm/vm_test.go | 58 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 13a22a8e81..9dc1eeed0b 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api/keystore" @@ -206,8 +207,7 @@ func setupGenesis( ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID) issuer := make(chan commonEng.Message, 1) - prefixedDB := prefixdb.New([]byte{1}, baseDB) - return ctx, prefixedDB, genesisBytes, issuer, atomicMemory + return ctx, baseDB, genesisBytes, issuer, atomicMemory } // GenesisVM creates a VM instance with the genesis test bytes and returns @@ -3151,3 +3151,57 @@ func TestParentBeaconRootBlock(t *testing.T) { }) } } + +func TestStandaloneDB(t *testing.T) { + vm := &VM{} + ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, genesisJSONLatest) + // alter network ID to use standalone database + ctx.NetworkID = 123456 + appSender := &enginetest.Sender{T: t} + appSender.CantSendAppGossip = true + appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil } + configJSON := `{"database-type": "memdb"}` + isDBEmpty := func(db database.Database) bool { + it := db.NewIterator() + defer it.Release() + return !it.Next() + } + // Ensure that the database is empty + require.True(t, isDBEmpty(dbManager)) + err := vm.Initialize( + context.Background(), + ctx, + dbManager, + genesisBytes, + nil, + []byte(configJSON), + issuer, + []*commonEng.Fx{}, + appSender, + ) + defer vm.Shutdown(context.Background()) + require.NoError(t, err, "error initializing VM") + + require.NoError(t, vm.SetState(context.Background(), snow.Bootstrapping)) + require.NoError(t, vm.SetState(context.Background(), snow.NormalOp)) + + // Issue a block + acceptedBlockEvent := make(chan core.ChainEvent, 1) + vm.blockChain.SubscribeChainAcceptedEvent(acceptedBlockEvent) + tx0 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil) + signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) + require.NoError(t, err) + errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0}) + require.NoError(t, errs[0]) + + // accept block + blk := issueAndAccept(t, issuer, vm) + newBlock := <-acceptedBlockEvent + require.Equal(t, newBlock.Block.Hash(), common.Hash(blk.ID())) + + // Ensure that the shared database is empty + assert.True(t, isDBEmpty(dbManager)) + // Ensure that the standalone database is not empty + assert.False(t, isDBEmpty(vm.db)) + assert.False(t, isDBEmpty(vm.acceptedBlockDB)) +} From ffe90d0b1ae52843abe0060f03b2404bab98caed Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 13 Nov 2024 13:13:25 +0300 Subject: [PATCH 2/4] remove shared memory from accept and precompile interface --- plugin/evm/block.go | 28 +++++--------------- plugin/evm/block_test.go | 2 +- plugin/evm/shared_memory_writer.go | 37 --------------------------- plugin/evm/syncervm_client.go | 4 +-- plugin/evm/vm.go | 3 +-- precompile/precompileconfig/config.go | 13 ++-------- 6 files changed, 11 insertions(+), 76 deletions(-) delete mode 100644 plugin/evm/shared_memory_writer.go diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 9d3d238d02..9e7d96855a 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,12 +61,9 @@ func (b *Block) Accept(context.Context) error { // Call Accept for relevant precompile logs. Note we do this prior to // calling Accept on the blockChain so any side effects (eg warp signatures) - // take place before the accepted log is emitted to subscribers. Use of the - // sharedMemoryWriter ensures shared memory requests generated by - // precompiles are committed atomically with the vm's lastAcceptedKey. + // take place before the accepted log is emitted to subscribers. rules := b.vm.chainConfig.Rules(b.ethBlock.Number(), b.ethBlock.Timestamp()) - sharedMemoryWriter := NewSharedMemoryWriter() - if err := b.handlePrecompileAccept(rules, sharedMemoryWriter); err != nil { + if err := b.handlePrecompileAccept(rules); err != nil { return err } if err := vm.blockChain.Accept(b.ethBlock); err != nil { @@ -77,24 +74,12 @@ func (b *Block) Accept(context.Context) error { return fmt.Errorf("failed to put %s as the last accepted block: %w", b.ID(), err) } - // Get pending operations on the vm's versionDB so we can apply them atomically - // with the shared memory requests. - vdbBatch, err := b.vm.db.CommitBatch() - if err != nil { - return fmt.Errorf("could not create commit batch processing block[%s]: %w", b.ID(), 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) + return b.vm.db.Commit() } // 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(rules params.Rules, sharedMemoryWriter *sharedMemoryWriter) error { +func (b *Block) handlePrecompileAccept(rules params.Rules) error { // Short circuit early if there are no precompile accepters to execute if len(rules.AccepterPrecompiles) == 0 { return nil @@ -108,9 +93,8 @@ func (b *Block) handlePrecompileAccept(rules params.Rules, sharedMemoryWriter *s return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64()) } acceptCtx := &precompileconfig.AcceptContext{ - SnowCtx: b.vm.ctx, - SharedMemory: sharedMemoryWriter, - Warp: b.vm.warpBackend, + SnowCtx: b.vm.ctx, + Warp: b.vm.warpBackend, } for _, receipt := range receipts { for logIdx, log := range receipt.Logs { diff --git a/plugin/evm/block_test.go b/plugin/evm/block_test.go index f30cc4ceae..638f551b91 100644 --- a/plugin/evm/block_test.go +++ b/plugin/evm/block_test.go @@ -93,5 +93,5 @@ func TestHandlePrecompileAccept(t *testing.T) { precompileAddr: mockAccepter, }, } - require.NoError(blk.handlePrecompileAccept(rules, nil)) + require.NoError(blk.handlePrecompileAccept(rules)) } diff --git a/plugin/evm/shared_memory_writer.go b/plugin/evm/shared_memory_writer.go deleted file mode 100644 index 88589720ee..0000000000 --- a/plugin/evm/shared_memory_writer.go +++ /dev/null @@ -1,37 +0,0 @@ -// (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/syncervm_client.go b/plugin/evm/syncervm_client.go index f18372995e..be4da4108a 100644 --- a/plugin/evm/syncervm_client.go +++ b/plugin/evm/syncervm_client.go @@ -299,7 +299,7 @@ func (client *stateSyncerClient) Shutdown() error { } // finishSync is responsible for updating disk and memory pointers so the VM is prepared -// for bootstrapping. Executes any shared memory operations from the atomic trie to shared memory. +// for bootstrapping. func (client *stateSyncerClient) finishSync() error { stateBlock, err := client.state.GetBlock(context.TODO(), ids.ID(client.syncSummary.BlockHash)) if err != nil { @@ -349,8 +349,6 @@ func (client *stateSyncerClient) finishSync() error { // updateVMMarkers updates the following markers in the VM's database // and commits them atomically: -// - updates atomic trie so it will have necessary metadata for the last committed root -// - updates atomic trie so it will resume applying operations to shared memory on initialize // - updates lastAcceptedKey // - removes state sync progress markers func (client *stateSyncerClient) updateVMMarkers() error { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 865042c19b..8e169f6d4b 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -1255,8 +1255,7 @@ func (vm *VM) initializeDBs(avaDB database.Database) error { // skip standalone database initialization if we are running in unit tests if vm.ctx.NetworkID != avalancheconstants.UnitTestID { // first initialize the accepted block database to check if we need to use a standalone database - verDB := versiondb.New(avaDB) - acceptedDB := prefixdb.New(acceptedPrefix, verDB) + acceptedDB := prefixdb.New(acceptedPrefix, avaDB) useStandAloneDB, err := vm.useStandaloneDatabase(acceptedDB) if err != nil { return err diff --git a/precompile/precompileconfig/config.go b/precompile/precompileconfig/config.go index 05d204de45..289549b4e4 100644 --- a/precompile/precompileconfig/config.go +++ b/precompile/precompileconfig/config.go @@ -5,8 +5,6 @@ package precompileconfig import ( - "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/ava-labs/avalanchego/vms/platformvm/warp" @@ -54,21 +52,14 @@ type Predicater interface { VerifyPredicate(predicateContext *PredicateContext, predicateBytes []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) -} - type WarpMessageWriter interface { AddMessage(unsignedMessage *warp.UnsignedMessage) error } // AcceptContext defines the context passed in to a precompileconfig's Accepter type AcceptContext struct { - SnowCtx *snow.Context - SharedMemory SharedMemoryWriter - Warp WarpMessageWriter + SnowCtx *snow.Context + Warp WarpMessageWriter } // Accepter is an optional interface for StatefulPrecompiledContracts to implement. From 9f55c81da86811c5922a77cca60bf8f2b5f30e5f Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 13 Nov 2024 18:04:15 +0300 Subject: [PATCH 3/4] Update plugin/evm/vm_test.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur --- plugin/evm/vm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 9dc1eeed0b..d58bf9246c 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -3155,7 +3155,7 @@ func TestParentBeaconRootBlock(t *testing.T) { func TestStandaloneDB(t *testing.T) { vm := &VM{} ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, genesisJSONLatest) - // alter network ID to use standalone database + // alter network ID to use standalone database, as by default unit test network does not use it. ctx.NetworkID = 123456 appSender := &enginetest.Sender{T: t} appSender.CantSendAppGossip = true From aa95fa1e6ceca7f35e08459f7026205fd329ed54 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 13 Nov 2024 19:04:45 +0300 Subject: [PATCH 4/4] revert changes to test helper --- plugin/evm/vm_test.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index d58bf9246c..6f1753e62e 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -207,7 +207,8 @@ func setupGenesis( ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID) issuer := make(chan commonEng.Message, 1) - return ctx, baseDB, genesisBytes, issuer, atomicMemory + prefixedDB := prefixdb.New([]byte{1}, baseDB) + return ctx, prefixedDB, genesisBytes, issuer, atomicMemory } // GenesisVM creates a VM instance with the genesis test bytes and returns @@ -3154,24 +3155,32 @@ func TestParentBeaconRootBlock(t *testing.T) { func TestStandaloneDB(t *testing.T) { vm := &VM{} - ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, genesisJSONLatest) - // alter network ID to use standalone database, as by default unit test network does not use it. + ctx := NewContext() + baseDB := memdb.New() + atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB)) + ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID) + issuer := make(chan commonEng.Message, 1) + sharedDB := prefixdb.New([]byte{1}, baseDB) + genesisBytes := buildGenesisTest(t, genesisJSONLatest) + // alter network ID to use standalone database ctx.NetworkID = 123456 appSender := &enginetest.Sender{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil } configJSON := `{"database-type": "memdb"}` + isDBEmpty := func(db database.Database) bool { it := db.NewIterator() defer it.Release() return !it.Next() } // Ensure that the database is empty - require.True(t, isDBEmpty(dbManager)) + require.True(t, isDBEmpty(baseDB)) + err := vm.Initialize( context.Background(), ctx, - dbManager, + sharedDB, genesisBytes, nil, []byte(configJSON), @@ -3181,7 +3190,6 @@ func TestStandaloneDB(t *testing.T) { ) defer vm.Shutdown(context.Background()) require.NoError(t, err, "error initializing VM") - require.NoError(t, vm.SetState(context.Background(), snow.Bootstrapping)) require.NoError(t, vm.SetState(context.Background(), snow.NormalOp)) @@ -3200,7 +3208,7 @@ func TestStandaloneDB(t *testing.T) { require.Equal(t, newBlock.Block.Hash(), common.Hash(blk.ID())) // Ensure that the shared database is empty - assert.True(t, isDBEmpty(dbManager)) + assert.True(t, isDBEmpty(baseDB)) // Ensure that the standalone database is not empty assert.False(t, isDBEmpty(vm.db)) assert.False(t, isDBEmpty(vm.acceptedBlockDB))