diff --git a/action/protocol/context.go b/action/protocol/context.go index 0d14b3f119..cabec30fab 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -142,6 +142,7 @@ type ( EnableDynamicFeeTx bool EnableBlobTransaction bool SufficentBalanceGuarantee bool + EnableCancunEVM bool } // FeatureWithHeightCtx provides feature check functions. @@ -297,6 +298,7 @@ func WithFeatureCtx(ctx context.Context) context.Context { EnableDynamicFeeTx: g.IsVanuatu(height), EnableBlobTransaction: g.IsVanuatu(height), SufficentBalanceGuarantee: g.IsVanuatu(height), + EnableCancunEVM: g.IsVanuatu(height), }, ) } diff --git a/action/protocol/execution/evm/evm.go b/action/protocol/execution/evm/evm.go index 5fd1cf609d..312c3de5f2 100644 --- a/action/protocol/execution/evm/evm.go +++ b/action/protocol/execution/evm/evm.go @@ -373,6 +373,9 @@ func prepareStateDB(ctx context.Context, sm protocol.StateManager) (*StateDBAdap if featureCtx.PanicUnrecoverableError { opts = append(opts, PanicUnrecoverableErrorOption()) } + if featureCtx.EnableCancunEVM { + opts = append(opts, EnableCancunEVMOption()) + } return NewStateDBAdapter( sm, diff --git a/action/protocol/execution/evm/evmstatedbadapter.go b/action/protocol/execution/evm/evmstatedbadapter.go index bf526afa8d..d829488cd2 100644 --- a/action/protocol/execution/evm/evmstatedbadapter.go +++ b/action/protocol/execution/evm/evmstatedbadapter.go @@ -35,6 +35,9 @@ type ( // deleteAccount records the account/contract to be deleted deleteAccount map[common.Address]struct{} + // createdAccount contains new accounts created in this tx + createdAccount map[common.Address]struct{} + // contractMap records the contracts being changed contractMap map[common.Address]Contract @@ -43,27 +46,28 @@ type ( // StateDBAdapter represents the state db adapter for evm to access iotx blockchain StateDBAdapter struct { - sm protocol.StateManager - logs []*action.Log - transactionLogs []*action.TransactionLog - err error - blockHeight uint64 - executionHash hash.Hash256 - lastAddBalanceAddr string - lastAddBalanceAmount *big.Int - refund uint64 - refundSnapshot map[int]uint64 - cachedContract contractMap - contractSnapshot map[int]contractMap // snapshots of contracts - selfDestructed deleteAccount // account/contract calling SelfDestruct - selfDestructedSnapshot map[int]deleteAccount // snapshots of SelfDestruct accounts - preimages preimageMap - preimageSnapshot map[int]preimageMap - accessList *accessList // per-transaction access list - accessListSnapshot map[int]*accessList - // Transient storage - transientStorage transientStorage + sm protocol.StateManager + logs []*action.Log + transactionLogs []*action.TransactionLog + err error + blockHeight uint64 + executionHash hash.Hash256 + lastAddBalanceAddr string + lastAddBalanceAmount *big.Int + refund uint64 + refundSnapshot map[int]uint64 + cachedContract contractMap + contractSnapshot map[int]contractMap // snapshots of contracts + selfDestructed deleteAccount // account/contract calling SelfDestruct + selfDestructedSnapshot map[int]deleteAccount // snapshots of SelfDestruct accounts + preimages preimageMap + preimageSnapshot map[int]preimageMap + accessList *accessList // per-transaction access list + accessListSnapshot map[int]*accessList + transientStorage transientStorage // Transient storage transientStorageSnapshot map[int]transientStorage + createdAccount createdAccount + createdAccountSnapshot map[int]createdAccount logsSnapshot map[int]int // logs is an array, save len(logs) at time of snapshot suffices txLogsSnapshot map[int]int notFixTopicCopyBug bool @@ -77,6 +81,7 @@ type ( suicideTxLogMismatchPanic bool zeroNonceForFreshAccount bool panicUnrecoverableError bool + enableCancun bool } ) @@ -174,6 +179,14 @@ func PanicUnrecoverableErrorOption() StateDBAdapterOption { } } +// EnableCancunEVMOption indicates that Cancun EVM is activated +func EnableCancunEVMOption() StateDBAdapterOption { + return func(adapter *StateDBAdapter) error { + adapter.enableCancun = true + return nil + } +} + // NewStateDBAdapter creates a new state db with iotex blockchain func NewStateDBAdapter( sm protocol.StateManager, @@ -182,25 +195,23 @@ func NewStateDBAdapter( opts ...StateDBAdapterOption, ) (*StateDBAdapter, error) { s := &StateDBAdapter{ - sm: sm, - logs: []*action.Log{}, - err: nil, - blockHeight: blockHeight, - executionHash: executionHash, - lastAddBalanceAmount: new(big.Int), - refundSnapshot: make(map[int]uint64), - cachedContract: make(contractMap), - contractSnapshot: make(map[int]contractMap), - selfDestructed: make(deleteAccount), - selfDestructedSnapshot: make(map[int]deleteAccount), - preimages: make(preimageMap), - preimageSnapshot: make(map[int]preimageMap), - accessList: newAccessList(), - accessListSnapshot: make(map[int]*accessList), - transientStorage: newTransientStorage(), - transientStorageSnapshot: make(map[int]transientStorage), - logsSnapshot: make(map[int]int), - txLogsSnapshot: make(map[int]int), + sm: sm, + logs: []*action.Log{}, + err: nil, + blockHeight: blockHeight, + executionHash: executionHash, + lastAddBalanceAmount: new(big.Int), + refundSnapshot: make(map[int]uint64), + cachedContract: make(contractMap), + contractSnapshot: make(map[int]contractMap), + selfDestructed: make(deleteAccount), + selfDestructedSnapshot: make(map[int]deleteAccount), + preimages: make(preimageMap), + preimageSnapshot: make(map[int]preimageMap), + accessList: newAccessList(), + accessListSnapshot: make(map[int]*accessList), + logsSnapshot: make(map[int]int), + txLogsSnapshot: make(map[int]int), } for _, opt := range opts { if err := opt(s); err != nil { @@ -211,6 +222,12 @@ func NewStateDBAdapter( if !s.legacyNonceAccount && s.useConfirmedNonce { return nil, errors.New("invalid parameter combination") } + if s.enableCancun { + s.transientStorage = newTransientStorage() + s.transientStorageSnapshot = make(map[int]transientStorage) + s.createdAccount = make(createdAccount) + s.createdAccountSnapshot = make(map[int]createdAccount) + } return s, nil } @@ -250,9 +267,15 @@ func (stateDB *StateDBAdapter) CreateAccount(evmAddr common.Address) { if stateDB.assertError(err, "Failed to convert evm address.", zap.Error(err)) { return } - _, err = accountutil.LoadOrCreateAccount(stateDB.sm, addr, stateDB.accountCreationOpts()...) - if stateDB.assertError(err, "Failed to create account.", zap.Error(err), zap.String("address", evmAddr.Hex())) { - return + // TODO: possibly remove the enableCancun flag after hard-fork + if _, ok := stateDB.cachedContract[evmAddr]; !ok || !stateDB.enableCancun { + _, err = accountutil.LoadOrCreateAccount(stateDB.sm, addr, stateDB.accountCreationOpts()...) + if stateDB.assertError(err, "Failed to create account.", zap.Error(err), zap.String("address", evmAddr.Hex())) { + return + } + } + if stateDB.enableCancun { + stateDB.createdAccount[evmAddr] = struct{}{} } log.L().Debug("Called CreateAccount.", log.Hex("addrHash", evmAddr[:])) } @@ -443,6 +466,7 @@ func (stateDB *StateDBAdapter) SelfDestruct(evmAddr common.Address) { } // clears the account balance actBalance := new(big.Int).Set(s.Balance) + log.L().Info("SelfDestruct contract", zap.String("Balance", actBalance.String())) err = s.SubBalance(s.Balance) if stateDB.assertError(err, "Failed to clear balance.", zap.Error(err), zap.String("address", evmAddr.Hex())) { return @@ -451,28 +475,10 @@ func (stateDB *StateDBAdapter) SelfDestruct(evmAddr common.Address) { if stateDB.assertError(err, "Failed to kill contract.", zap.Error(err), zap.String("address", evmAddr.Hex())) { return } - // To ensure data consistency, generate this log after the hard-fork - // a separate patch file will be created later to provide missing logs before the hard-fork - // TODO: remove this gating once the hard-fork has passed - if stateDB.suicideTxLogMismatchPanic { - // before calling SelfDestruct, EVM will transfer the contract's balance to beneficiary - // need to create a transaction log on successful SelfDestruct - if stateDB.lastAddBalanceAmount.Cmp(actBalance) == 0 { - if stateDB.lastAddBalanceAmount.Cmp(big.NewInt(0)) > 0 { - from, _ := address.FromBytes(evmAddr[:]) - stateDB.addTransactionLogs(&action.TransactionLog{ - Type: iotextypes.TransactionLogType_IN_CONTRACT_TRANSFER, - Sender: from.String(), - Recipient: stateDB.lastAddBalanceAddr, - Amount: stateDB.lastAddBalanceAmount, - }) - } - } else { - log.L().Panic("SelfDestruct contract's balance does not match", - zap.String("SelfDestruct", actBalance.String()), - zap.String("beneficiary", stateDB.lastAddBalanceAmount.String())) - } - } + // before calling SelfDestruct, EVM will transfer the contract's balance to beneficiary + // need to create a transaction log on successful SelfDestruct + from, _ := address.FromBytes(evmAddr[:]) + stateDB.generateSelfDestructTransferLog(from.String(), stateDB.lastAddBalanceAmount.Cmp(actBalance) == 0) // mark it as deleted stateDB.selfDestructed[evmAddr] = struct{}{} } @@ -483,6 +489,22 @@ func (stateDB *StateDBAdapter) HasSelfDestructed(evmAddr common.Address) bool { return ok } +// Selfdestruct6780 implements EIP-6780 +func (stateDB *StateDBAdapter) Selfdestruct6780(evmAddr common.Address) { + if !stateDB.Exist(evmAddr) { + log.L().Debug("Account does not exist.", zap.String("address", evmAddr.Hex())) + return + } + // opSelfdestruct6780 has already subtracted the contract's balance + // so create a transaction log + from, _ := address.FromBytes(evmAddr[:]) + stateDB.generateSelfDestructTransferLog(from.String(), true) + // per EIP-6780, delete the account only if it is created in the same transaction + if _, ok := stateDB.createdAccount[evmAddr]; ok { + stateDB.selfDestructed[evmAddr] = struct{}{} + } +} + // SetTransientState sets transient storage for a given account func (stateDB *StateDBAdapter) SetTransientState(addr common.Address, key, value common.Hash) { prev := stateDB.transientStorage.Get(addr, key) @@ -497,12 +519,6 @@ func (stateDB *StateDBAdapter) GetTransientState(addr common.Address, key common return stateDB.transientStorage.Get(addr, key) } -// Selfdestruct6780 implements EIP-6780 -func (stateDB *StateDBAdapter) Selfdestruct6780(evmAddr common.Address) { - //Todo: implement EIP-6780 - log.S().Panic("Selfdestruct6780 not implemented") -} - // Exist checks the existence of an address func (stateDB *StateDBAdapter) Exist(evmAddr common.Address) bool { addr, err := address.FromBytes(evmAddr.Bytes()) @@ -632,14 +648,27 @@ func (stateDB *StateDBAdapter) RevertToSnapshot(snapshot int) { } } } - //restore transientStorage - stateDB.transientStorage = stateDB.transientStorageSnapshot[snapshot] - { - for i := snapshot; ; i++ { - if _, ok := stateDB.transientStorageSnapshot[i]; ok { - delete(stateDB.transientStorageSnapshot, i) - } else { - break + if stateDB.enableCancun { + //restore transientStorage + stateDB.transientStorage = stateDB.transientStorageSnapshot[snapshot] + { + for i := snapshot; ; i++ { + if _, ok := stateDB.transientStorageSnapshot[i]; ok { + delete(stateDB.transientStorageSnapshot, i) + } else { + break + } + } + } + // restore created accounts + stateDB.createdAccount = stateDB.createdAccountSnapshot[snapshot] + { + for i := snapshot; ; i++ { + if _, ok := stateDB.createdAccountSnapshot[i]; ok { + delete(stateDB.createdAccountSnapshot, i) + } else { + break + } } } } @@ -762,8 +791,16 @@ func (stateDB *StateDBAdapter) Snapshot() int { stateDB.preimageSnapshot[sn] = p // save a copy of access list stateDB.accessListSnapshot[sn] = stateDB.accessList.Copy() - // save a copy of transient storage - stateDB.transientStorageSnapshot[sn] = stateDB.transientStorage.Copy() + if stateDB.enableCancun { + // save a copy of transient storage + stateDB.transientStorageSnapshot[sn] = stateDB.transientStorage.Copy() + // save a copy of created account map + ca := make(createdAccount) + for k, v := range stateDB.createdAccount { + ca[k] = v + } + stateDB.createdAccountSnapshot[sn] = ca + } return sn } @@ -864,6 +901,27 @@ func (stateDB *StateDBAdapter) accountState(evmAddr common.Address) (*state.Acco return accountutil.LoadAccountByHash160(stateDB.sm, hash.BytesToHash160(evmAddr[:]), stateDB.accountCreationOpts()...) } +func (stateDB *StateDBAdapter) generateSelfDestructTransferLog(sender string, amountMatch bool) { + // To ensure data consistency, generate this log after the hard-fork + // a separate patch file will be created later to provide missing logs before the hard-fork + // TODO: remove this gating once the hard-fork has passed + if stateDB.suicideTxLogMismatchPanic { + if amountMatch { + if stateDB.lastAddBalanceAmount.Cmp(big.NewInt(0)) > 0 { + stateDB.addTransactionLogs(&action.TransactionLog{ + Type: iotextypes.TransactionLogType_IN_CONTRACT_TRANSFER, + Sender: sender, + Recipient: stateDB.lastAddBalanceAddr, + Amount: stateDB.lastAddBalanceAmount, + }) + } + } else { + log.L().Panic("SelfDestruct contract's balance does not match", + zap.String("beneficiary", stateDB.lastAddBalanceAmount.String())) + } + } +} + func (stateDB *StateDBAdapter) addTransactionLogs(tlog *action.TransactionLog) { stateDB.transactionLogs = append(stateDB.transactionLogs, tlog) } @@ -1068,10 +1126,14 @@ func (stateDB *StateDBAdapter) clear() { stateDB.preimageSnapshot = make(map[int]preimageMap) stateDB.accessList = newAccessList() stateDB.accessListSnapshot = make(map[int]*accessList) - stateDB.transientStorage = newTransientStorage() - stateDB.transientStorageSnapshot = make(map[int]transientStorage) stateDB.logsSnapshot = make(map[int]int) stateDB.txLogsSnapshot = make(map[int]int) stateDB.logs = []*action.Log{} stateDB.transactionLogs = []*action.TransactionLog{} + if stateDB.enableCancun { + stateDB.transientStorage = newTransientStorage() + stateDB.transientStorageSnapshot = make(map[int]transientStorage) + stateDB.createdAccount = make(createdAccount) + stateDB.createdAccountSnapshot = make(map[int]createdAccount) + } } diff --git a/action/protocol/execution/evm/evmstatedbadapter_test.go b/action/protocol/execution/evm/evmstatedbadapter_test.go index 960c5f5432..4098f82190 100644 --- a/action/protocol/execution/evm/evmstatedbadapter_test.go +++ b/action/protocol/execution/evm/evmstatedbadapter_test.go @@ -22,8 +22,10 @@ import ( "github.com/stretchr/testify/require" "github.com/iotexproject/iotex-core/action/protocol" + accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util" "github.com/iotexproject/iotex-core/blockchain/genesis" "github.com/iotexproject/iotex-core/db/batch" + . "github.com/iotexproject/iotex-core/pkg/util/assertions" "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/test/mock/mock_chainmanager" @@ -427,6 +429,7 @@ func TestSnapshotRevertAndCommit(t *testing.T) { opt := []StateDBAdapterOption{ NotFixTopicCopyBugOption(), SuicideTxLogMismatchPanicOption(), + EnableCancunEVMOption(), } if async { opt = append(opt, AsyncContractTrieOption()) @@ -982,11 +985,10 @@ func TestStateDBTransientStorage(t *testing.T) { opts = append(opts, NotFixTopicCopyBugOption(), FixSnapshotOrderOption(), + EnableCancunEVMOption(), ) state, err := NewStateDBAdapter(sm, 1, hash.ZeroHash256, opts...) - if err != nil { - t.Fatal(err) - } + require.NoError(err) var ( addr0 = common.Address{} addr1 = common.HexToAddress("1234567890") @@ -1022,3 +1024,56 @@ func TestStateDBTransientStorage(t *testing.T) { } } + +func TestSelfdestruct6780(t *testing.T) { + r := require.New(t) + ctrl := gomock.NewController(t) + sm, err := initMockStateManager(ctrl) + r.NoError(err) + var opts []StateDBAdapterOption + opts = append(opts, + FixSnapshotOrderOption(), + EnableCancunEVMOption(), + ) + state := MustNoErrorV(NewStateDBAdapter(sm, 1, hash.ZeroHash256, opts...)) + r.NoError(err) + _, err = accountutil.LoadOrCreateAccount(state.sm, _c4) + state.AddBalance(_c4, uint256.NewInt(100)) + r.NoError(state.CommitContracts()) + state.clear() + acc := MustNoErrorV(accountutil.LoadOrCreateAccount(state.sm, _c4)) + r.Equal(big.NewInt(100), acc.Balance) + r.False(acc.IsContract()) + r.NoError(err) + addrs := []common.Address{_c1, _c2, _c3, _c4} + for _, addr := range addrs { + if addr == _c4 { + state.SetCode(addr, []byte{1, 2}) + } else { + state.CreateAccount(addr) + } + state.Selfdestruct6780(addr) + r.True(state.Exist(addr)) + state.Snapshot() + } + r.NoError(state.CommitContracts()) + for _, addr := range addrs[:3] { + r.False(state.Exist(addr)) + } + r.True(state.Exist(_c4)) // pre-existing account gets overwritten, but not deleted + acc = MustNoErrorV(accountutil.LoadOrCreateAccount(state.sm, _c4)) + r.Equal(big.NewInt(100), acc.Balance) + r.True(acc.IsContract()) + state.RevertToSnapshot(3) + r.Equal(createdAccount{ + _c1: struct{}{}, _c2: struct{}{}, _c3: struct{}{}}, state.createdAccount) + state.RevertToSnapshot(2) + r.Equal(createdAccount{ + _c1: struct{}{}, _c2: struct{}{}, _c3: struct{}{}}, state.createdAccount) + state.RevertToSnapshot(1) + r.Equal(createdAccount{ + _c1: struct{}{}, _c2: struct{}{}}, state.createdAccount) + state.RevertToSnapshot(0) + r.Equal(createdAccount{ + _c1: struct{}{}}, state.createdAccount) +}