diff --git a/plugin/dapp/evm/executor/attack_integration_test.go b/plugin/dapp/evm/executor/attack_integration_test.go index f316a38f5..c411228b6 100644 --- a/plugin/dapp/evm/executor/attack_integration_test.go +++ b/plugin/dapp/evm/executor/attack_integration_test.go @@ -82,9 +82,10 @@ func privKey(idx int) crypto.PrivKey { return util.TestPrivkeyList[idx] } func fundAddr(t *testing.T, exec *EVMExecutor, addr string, amount int64) { t.Helper() - acc := exec.mStateDB.CoinsAccount.LoadAccount(addr) + // 写到执行器的 StateDB:mStateDB 的写缓冲每笔交易重建,写进去的余额不会留到下一次 Exec + acc := exec.GetCoinsAccount().LoadAccount(addr) acc.Balance = amount - exec.mStateDB.CoinsAccount.SaveAccount(acc) + exec.GetCoinsAccount().SaveAccount(acc) } func addrFromRole(cfg *ctypes.Chain33Config, roleIdx int) string { diff --git a/plugin/dapp/evm/executor/exec.go b/plugin/dapp/evm/executor/exec.go index d51ac1773..8a57e3721 100644 --- a/plugin/dapp/evm/executor/exec.go +++ b/plugin/dapp/evm/executor/exec.go @@ -58,7 +58,6 @@ func (evm *EVMExecutor) Exec(tx *types.Transaction, index int) (*types.Receipt, func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType int32, index int, txFee uint64, readOnly bool) (receipt *types.Receipt, err error) { cfg := evm.GetAPI().GetConfig() - evm.mStateDB.ResetRevertError() // 获取当前区块的上下文信息构造EVM上下文 context := evm.NewEVMContext(msg, txHash) execAddr := evm.getEvmExecAddress() @@ -192,14 +191,6 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in return receipt, vmerr } - // A snapshot revert that failed leaves the account cache and the receipt describing - // different state, which chain33's checkKV can no longer validate. Fail the transaction - // (fee-only receipt, state rolled back by the framework) instead of committing it. - if rerr := evm.mStateDB.RevertError(); rerr != nil { - log.Error("innerExec snapshot revert failed", "error info", rerr) - return receipt, rerr - } - // 计算消耗了多少费用(实际消耗的费用) usedFee, overflow := common.SafeMul(usedGas, uint64(msg.GasPrice())) // 费用消耗溢出,执行失败 diff --git a/plugin/dapp/evm/executor/snapshot_revert_failure_test.go b/plugin/dapp/evm/executor/snapshot_revert_failure_test.go deleted file mode 100644 index 883efb731..000000000 --- a/plugin/dapp/evm/executor/snapshot_revert_failure_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright Fuzamei Corp. 2018 All Rights Reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package executor - -// Executor-level test for the guard in innerExec that turns a failed snapshot revert into a -// failed transaction. A failed revert leaves the account cache and the receipt describing -// different state, so the transaction must not be committed. - -import ( - "errors" - "testing" - - "github.com/33cn/chain33/common/db" - ctypes "github.com/33cn/chain33/types" - vcomm "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" - "github.com/stretchr/testify/require" -) - -// failingRevertKV wraps the production StateDB and fails every RevertSet, standing in for a -// store fault during the undo of a reverted value transfer. -type failingRevertKV struct { - db.KV - err error -} - -func (f failingRevertKV) RevertSet(key, originalValue []byte) error { - return f.err -} - -// TestInnerExecFailsWhenSnapshotRevertFails drives the same CALL{value} revert shape as the -// transfer-revert integration tests, but with a KV whose RevertSet always fails. The inner call -// is popped by the caller contract, so the outer frame reports success; only the RevertError -// guard can fail the transaction. The next execution must still succeed: ResetRevertError clears -// the recorded error at the start of every innerExec call. -func TestInnerExecFailsWhenSnapshotRevertFails(t *testing.T) { - const ( - topUp = uint64(1000) - moved = uint64(400) - ) - - cfg, st, localDB := productionStateDBFixture(t) - revertErr := errors.New("injected RevertSet failure") - - // Wrap the production StateDB so the snapshot revert of the value transfer fails. SetStateDB - // also points the coins account at the wrapper, and CheckInit rebuilds the MemoryStateDB. - exec := newProductionExec(t, cfg, st, localDB) - exec.SetStateDB(failingRevertKV{KV: st, err: revertErr}) - exec.CheckInit() - - target := vcomm.BytesToAddress([]byte{0xd0}) - forwarder := deployCode(t, cfg, exec, buildForwarderCode(target, moved)) - caller := deployCode(t, cfg, exec, buildCallerCode(*vcomm.StringToAddress(forwarder), moved)) - noop := deployCode(t, cfg, exec, []byte{0x00}) - - user := addrFromRole(cfg, roleLegitUser) - fundRole(t, exec, cfg, roleLegitUser, int64(topUp)*2) - initial := balanceOf(exec, user) - - // First transaction: the forwarder aborts on the value call, the reverted transfer fails to - // restore its account keys, and the outer contract continues to success. - st.Begin() - receipt, err := exec.Exec(makeCallTx(cfg, roleLegitUser, caller, []byte{0x01}, topUp), 0) - require.ErrorIs(t, err, revertErr, "innerExec must fail the transaction on a failed snapshot revert") - require.Empty(t, receipt.GetKV(), "the failed transaction must carry no state to commit") - require.ErrorIs(t, exec.GetMStateDB().RevertError(), revertErr, - "the guard must observe the recorded revert error") - st.Rollback() - - require.Equal(t, initial, balanceOf(exec, user), "no balance may move out of the user") - require.Zero(t, balanceOf(exec, forwarder), "the reverted credit must not be committed") - require.Zero(t, balanceOf(exec, target.String()), "the phantom credit must not be committed") - - // Second execution on the same MemoryStateDB (Exec would recreate it through CheckInit). - // Nothing reverts here, so a stale error would fail at the guard; ResetRevertError at the - // start of innerExec must clear it before execution starts. - tx := makeCallTx(cfg, roleLegitUser, noop, nil, 0) - msg, err := exec.GetMessage(tx, 0, nil) - require.NoError(t, err) - - st.Begin() - receipt, err = exec.innerExec(msg, tx.Hash(), tx.GetSignature().GetTy(), 0, msg.GasLimit(), false) - require.NoError(t, err, "a failed revert must not poison the next execution") - require.Equal(t, int32(ctypes.ExecOk), receipt.GetTy()) - require.NoError(t, exec.GetMStateDB().RevertError(), "ResetRevertError must clear the recorded error") - st.Rollback() -} diff --git a/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go b/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go index b81d07f4c..77f4a37dd 100644 --- a/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go +++ b/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go @@ -89,8 +89,9 @@ func (e *gapEnv) fund(addr vmcommon.Address, amount int64) { e.coins.SaveAccount(acc) } +// balance 经由 mdb 读取:执行期间的转账只在 mdb 的写缓冲中,直接读 StateDB 看不到 func (e *gapEnv) balance(addr vmcommon.Address) int64 { - return e.coins.LoadAccount(addr.String()).Balance + return e.mdb.CoinsAccount.LoadAccount(addr.String()).Balance } func (e *gapEnv) deploy(addr, creator vmcommon.Address, code []byte) { diff --git a/plugin/dapp/evm/executor/vm/runtime/revert_mint_poc_test.go b/plugin/dapp/evm/executor/vm/runtime/revert_mint_poc_test.go new file mode 100644 index 000000000..68ea8f50d --- /dev/null +++ b/plugin/dapp/evm/executor/vm/runtime/revert_mint_poc_test.go @@ -0,0 +1,88 @@ +package runtime + +// 复现安全报告:回滚子调用的转账留在 coins 缓存里,被同一交易后续转账提交,造成增发。 +// 提交态 = 交易前状态 + 回执 KV(GetChangedData),与 execTxOne/ExecKVMemSet 一致。 + +import ( + "math/big" + "testing" + + ctypes "github.com/33cn/chain33/types" + vmcommon "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" + "github.com/stretchr/testify/require" +) + +func zeroArgs4() []byte { return []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00} } + +// Q:把收到的 value 原样 CALL 回 caller,然后 STOP。 +func buildRefundCode() []byte { + code := zeroArgs4() + return append(code, 0x34, 0x33, 0x5a, 0xf1, 0x00) // CALLVALUE CALLER GAS CALL STOP +} + +// P:无 calldata 则 REVERT;有 calldata 则先把 value 转给 q,再把同额转给 payout。 +func buildTrapCode(q, payout vmcommon.Address) []byte { + code := []byte{0x36, 0x60, 0x09, 0x57, 0x60, 0x00, 0x60, 0x00, 0xfd, 0x5b} + code = append(code, zeroArgs4()...) + code = append(code, 0x34, 0x73) + code = append(code, q.Bytes()...) + code = append(code, 0x5a, 0xf1, 0x50) + code = append(code, zeroArgs4()...) + code = append(code, 0x34, 0x73) + code = append(code, payout.Bytes()...) + return append(code, 0x5a, 0xf1, 0x00) +} + +func TestRevertedValueTransferMints(t *testing.T) { + env := newGapEnv(t, nil) + attacker, p, q, payout := addr(0xa1), addr(0xa2), addr(0xa3), addr(0xa4) + env.deploy(q, attacker, buildRefundCode()) + env.deploy(p, attacker, buildTrapCode(q, payout)) + const value = 100 * 1e8 + env.fund(attacker, 1000*1e8) + + addrs := []vmcommon.Address{attacker, p, q, payout} + committed := map[string]int64{} + var preSum int64 + for _, a := range addrs { + committed[string(env.coins.AccountKey(a.String()))] = env.balance(a) + preSum += env.balance(a) + } + + _, _, _, err := env.evm.Call(AccountRef(attacker), p, []byte{0x01}, 5_000_000, value) + require.NoError(t, err) + + kvs, _ := env.mdb.GetChangedData(env.mdb.GetLastSnapshot().GetID()) + for _, kv := range kvs { + if _, ok := committed[string(kv.Key)]; ok { + var acc ctypes.Account + require.NoError(t, ctypes.Decode(kv.Value, &acc)) + committed[string(kv.Key)] = acc.Balance + } + } + var postSum int64 + for _, a := range addrs { + b := committed[string(env.coins.AccountKey(a.String()))] + t.Logf("%s committed=%d cache=%d", a, b, env.balance(a)) + postSum += b + } + require.Equal(t, preSum, postSum, "coin supply changed: minted %d", postSum-preSum) +} + +// 同一帧内两次 SSTORE 同一槽位后 REVERT:Snapshot.revert 正序重放 prevalue, +// 槽位停在第一次写入的值而不是原值,父帧能读到被回滚帧写过的数据。 +func TestRevertedDoubleSStoreLeaks(t *testing.T) { + env := newGapEnv(t, nil) + user, s := addr(0xb1), addr(0xb2) + code := []byte{0x36, 0x60, 0x19, 0x57, // 有 calldata 跳到内层 + 0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0x60, 0x00, 0x60, 0x00, 0x30, 0x5a, 0xf1, 0x50, // CALL self(1 字节 calldata) + 0x60, 0x00, 0x54, 0x60, 0x01, 0x55, 0x00, // slot1 = slot0 + 0x5b, 0x60, 0x01, 0x60, 0x00, 0x55, 0x60, 0x02, 0x60, 0x00, 0x55, 0x60, 0x00, 0x60, 0x00, 0xfd} // slot0=1; slot0=2; REVERT + env.deploy(s, user, code) + _, _, _, err := env.evm.Call(AccountRef(user), s, nil, 5_000_000, 0) + require.NoError(t, err) + slot0 := env.mdb.GetState(s.String(), vmcommon.Hash{}) + slot1 := env.mdb.GetState(s.String(), vmcommon.BigToHash(new(big.Int).SetInt64(1))) + t.Logf("slot0=%x slot1=%x", slot0.Big(), slot1.Big()) + require.Equal(t, vmcommon.Hash{}, slot1, "parent frame observed a value written by a reverted frame") +} diff --git a/plugin/dapp/evm/executor/vm/state/journal_kv.go b/plugin/dapp/evm/executor/vm/state/journal_kv.go new file mode 100644 index 000000000..a9db1ffbc --- /dev/null +++ b/plugin/dapp/evm/executor/vm/state/journal_kv.go @@ -0,0 +1,103 @@ +// Copyright Fuzamei Corp. 2018 All Rights Reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package state + +import ( + "sort" + + "github.com/33cn/chain33/common/db" + "github.com/33cn/chain33/types" +) + +// journalKV 是 MemoryStateDB 与执行器 StateDB 之间的写缓冲,作用等同 chain33 的 txcache, +// 但支持与 Snapshot 版本一一对应的嵌套回滚。 +// +// EVM 执行期间所有经由 MemoryStateDB 的状态写入(coins/token 账户等)只进入本缓冲: +// 回滚某个快照时,按 undo 日志倒序恢复写之前的值,调用方无需为每种写入单独实现 revert。 +// 执行成功后由 GetChangedData 把留下的写入一次性交给回执,并写回底层 StateDB。 +type journalKV struct { + parent db.KV + dirty map[string][]byte // nil 值表示该 key 在本交易内不存在 + undo []kvUndo + marks []int // marks[v] 为快照 v 创建时 undo 的长度 +} + +type kvUndo struct { + key string + prev []byte + hadPrev bool // 写之前 dirty 中是否已有该 key +} + +func newJournalKV(parent db.KV) *journalKV { + return &journalKV{parent: parent, dirty: make(map[string][]byte)} +} + +// Get 先读缓冲,再读底层 StateDB +func (j *journalKV) Get(key []byte) ([]byte, error) { + if v, ok := j.dirty[string(key)]; ok { + if v == nil { + return nil, types.ErrNotFound + } + return v, nil + } + return j.parent.Get(key) +} + +// Set 只写缓冲,并记录 undo +func (j *journalKV) Set(key []byte, value []byte) error { + k := string(key) + prev, had := j.dirty[k] + j.undo = append(j.undo, kvUndo{key: k, prev: prev, hadPrev: had}) + if value != nil { + value = append([]byte(nil), value...) + } + j.dirty[k] = value + return nil +} + +// Begin/Commit/Rollback 满足 db.KV 接口;交易级的提交与回滚由执行器框架负责 +func (j *journalKV) Begin() {} +func (j *journalKV) Commit() error { return nil } +func (j *journalKV) Rollback() { j.revert(0) } + +// snapshot 记录快照 version 的回滚点,version 必须等于当前快照数 +func (j *journalKV) snapshot(version int) { + j.marks = append(j.marks[:version], len(j.undo)) +} + +// revertTo 把缓冲恢复到快照 version 创建时的状态 +func (j *journalKV) revertTo(version int) { + if version < 0 || version >= len(j.marks) { + return + } + j.revert(j.marks[version]) + j.marks = j.marks[:version] +} + +func (j *journalKV) revert(n int) { + for i := len(j.undo) - 1; i >= n; i-- { + u := j.undo[i] + if u.hadPrev { + j.dirty[u.key] = u.prev + } else { + delete(j.dirty, u.key) + } + } + j.undo = j.undo[:n] +} + +// changes 返回缓冲中的全部写入,按 key 排序保证各节点一致 +func (j *journalKV) changes() []*types.KeyValue { + keys := make([]string, 0, len(j.dirty)) + for k := range j.dirty { + keys = append(keys, k) + } + sort.Strings(keys) + kvs := make([]*types.KeyValue, 0, len(keys)) + for _, k := range keys { + kvs = append(kvs, &types.KeyValue{Key: []byte(k), Value: j.dirty[k]}) + } + return kvs +} diff --git a/plugin/dapp/evm/executor/vm/state/journal_kv_test.go b/plugin/dapp/evm/executor/vm/state/journal_kv_test.go new file mode 100644 index 000000000..a54e1a7ab --- /dev/null +++ b/plugin/dapp/evm/executor/vm/state/journal_kv_test.go @@ -0,0 +1,102 @@ +package state + +import ( + "strings" + "testing" + + "github.com/33cn/chain33/account" + apimock "github.com/33cn/chain33/client/mocks" + "github.com/33cn/chain33/common/db" + ctypes "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + "github.com/stretchr/testify/require" +) + +// recordKV 记录写入底层 StateDB 的 key,模拟 chain33 StateDB.GetSetKeys +type recordKV struct { + db.KV + keys []string +} + +func (r *recordKV) Set(key, value []byte) error { + r.keys = append(r.keys, string(key)) + return r.KV.Set(key, value) +} + +func newJournalTestDB(t *testing.T) (*MemoryStateDB, *recordKV, *account.DB) { + cfgStr := ctypes.GetDefaultCfgstring() + if !strings.Contains(cfgStr, "[exec.sub.evm]") { + cfgStr += "\n[exec.sub.evm]\nethMapFromExecutor=\"coins\"\nethMapFromSymbol=\"bty\"\n" + } + cfg := ctypes.NewChain33Config(cfgStr) + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig").Return(cfg) + + dbDir, stateDB, localDB := util.CreateTestDB() + t.Cleanup(func() { util.CloseTestDB(dbDir, stateDB) }) + + raw, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), stateDB) + require.NoError(t, err) + rec := &recordKV{KV: stateDB} + coins, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), rec) + require.NoError(t, err) + return NewMemoryStateDB(rec, localDB, coins, 1, api), rec, raw +} + +// 被回滚的转账:缓冲恢复原值,不写底层 StateDB,也不进回执; +// 写入底层的 key 必须都在回执里(chain33 checkKV 的约束)。 +func TestJournalKVRevertedTransfer(t *testing.T) { + mdb, rec, raw := newJournalTestDB(t) + a, b, c := "1JRNjdEqp4LJ5fqycUBm9ayCKSeeskgMKR", "1NLHPEcbTWWxxU3dGUZBhayjrCHD3psX7k", "1MCftFynyvG2F4ED5mdHYgziDxx6vDrScs" + acc := raw.LoadAccount(a) + acc.Balance = 1000 + raw.SaveAccount(acc) + rec.keys = nil + + mdb.Snapshot() + v := mdb.Snapshot() + require.True(t, mdb.Transfer(a, b, 600)) + require.EqualValues(t, 600, mdb.GetBalance(b)) + mdb.RevertToSnapshot(v) + require.EqualValues(t, 1000, mdb.GetBalance(a)) + require.EqualValues(t, 0, mdb.GetBalance(b)) + require.Empty(t, rec.keys, "reverted transfer leaked into the underlying StateDB") + + // 回滚后余额已恢复,后续转账不能再花被回滚的钱 + require.False(t, mdb.Transfer(b, c, 600)) + require.True(t, mdb.Transfer(a, c, 300)) + + kvs, _ := mdb.GetChangedData(mdb.GetLastSnapshot().GetID()) + inReceipt := map[string]bool{} + for _, kv := range kvs { + inReceipt[string(kv.Key)] = true + } + require.NotContains(t, inReceipt, string(raw.AccountKey(b))) + for _, k := range rec.keys { + require.True(t, inReceipt[k], "key %s written to StateDB but missing from receipt", k) + } + require.EqualValues(t, 700, raw.LoadAccount(a).Balance) + require.EqualValues(t, 0, raw.LoadAccount(b).Balance) + require.EqualValues(t, 300, raw.LoadAccount(c).Balance) +} + +// 嵌套快照:内层回滚只撤销内层写入,外层回滚撤销全部 +func TestJournalKVNestedRevert(t *testing.T) { + mem, err := db.NewGoMemDB("t", "", 0) + require.NoError(t, err) + j := newJournalKV(mem) + k := []byte("k") + j.snapshot(0) + require.NoError(t, j.Set(k, []byte("1"))) + j.snapshot(1) + require.NoError(t, j.Set(k, []byte("2"))) + require.NoError(t, j.Set(k, []byte("3"))) + j.revertTo(1) + v, err := j.Get(k) + require.NoError(t, err) + require.Equal(t, "1", string(v)) + j.revertTo(0) + _, err = j.Get(k) + require.Equal(t, ctypes.ErrNotFound, err) + require.Empty(t, j.changes()) +} diff --git a/plugin/dapp/evm/executor/vm/state/snapshot.go b/plugin/dapp/evm/executor/vm/state/snapshot.go index 8151837a4..b940c141c 100644 --- a/plugin/dapp/evm/executor/vm/state/snapshot.go +++ b/plugin/dapp/evm/executor/vm/state/snapshot.go @@ -5,12 +5,9 @@ package state import ( - "errors" - "fmt" "sort" "github.com/33cn/chain33/common/log/log15" - cexecutor "github.com/33cn/chain33/executor" "github.com/33cn/chain33/types" "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" @@ -23,7 +20,7 @@ import ( // 如果合约执行出错,会按生成顺序的倒序,依次调用变更对象的回滚接口进行数据回滚,并同步删除变更对象缓存 // 如果合约执行成功,会按生成顺序的正序,依次调用变更对象的数据和日志变更记录,回传给区块链 type DataChange interface { - revert(mdb *MemoryStateDB) error + revert(mdb *MemoryStateDB) getData(mdb *MemoryStateDB) []*types.KeyValue getLog(mdb *MemoryStateDB) []*types.ReceiptLog } @@ -41,27 +38,22 @@ func (ver *Snapshot) GetID() int { } // 回滚当前版本 -func (ver *Snapshot) revert() error { +func (ver *Snapshot) revert() bool { if ver.entries == nil { - return nil + return true } - // The interface contract requires undoing changes in reverse creation order. Applying two - // writes to the same slot forward would restore the intermediate value instead of the - // pre-frame one. Legacy behaviour is kept before ForkEVMTransferRevert. - if ver.statedb != nil && ver.statedb.isForkActive(evmtypes.ForkEVMTransferRevert) { + // ForkEVMTransferRevert 之后按生成顺序倒序回滚:同一版本内多次修改同一数据时,才能恢复到最早的值。 + // 分叉之前保留正序,历史区块按原结果重放。 + if ver.statedb.kv != nil { for i := len(ver.entries) - 1; i >= 0; i-- { - if err := ver.entries[i].revert(ver.statedb); err != nil { - return err - } + ver.entries[i].revert(ver.statedb) } - return nil + return true } for _, entry := range ver.entries { - if err := entry.revert(ver.statedb); err != nil { - return err - } + entry.revert(ver.statedb) } - return nil + return true } // 添加变更数据 @@ -154,23 +146,13 @@ type ( } // 转账事件 - // 合约转账动作不执行回滚,失败后数据不会写入区块 + // ForkEVMTransferRevert 之前:data 携带回执 KV,回滚时不处理(缓存中的转账不会撤销); + // 之后:data 为空,账户余额的回滚与回执数据都由 journalKV 负责,这里只记录日志 transferChange struct { baseChange amount int64 data []*types.KeyValue logs []*types.ReceiptLog - // preState carries the raw account KV read before the transfer. It is only populated - // when ForkEVMTransferRevert is active; empty keeps the legacy no-op revert. - preState []kvPreState - } - - // kvPreState is the raw value of one account key before a transfer wrote to it. - // existed=false means the key was absent and revert must remove it again. - kvPreState struct { - key []byte - value []byte - existed bool } // 合约生成日志事件 @@ -188,8 +170,7 @@ type ( ) // 在baseChang中定义三个基本操作,子对象中只需要实现必要的操作 -func (ch baseChange) revert(s *MemoryStateDB) error { - return nil +func (ch baseChange) revert(s *MemoryStateDB) { } func (ch baseChange) getData(s *MemoryStateDB) (kvset []*types.KeyValue) { @@ -201,9 +182,8 @@ func (ch baseChange) getLog(s *MemoryStateDB) (logs []*types.ReceiptLog) { } // 创建账户对象的回滚,需要删除缓存中的账户和变更标记 -func (ch createAccountChange) revert(s *MemoryStateDB) error { +func (ch createAccountChange) revert(s *MemoryStateDB) { delete(s.accounts, ch.account) - return nil } // 创建账户对象的数据集 @@ -217,16 +197,15 @@ func (ch createAccountChange) getData(s *MemoryStateDB) (kvset []*types.KeyValue return nil } -func (ch suicideChange) revert(mdb *MemoryStateDB) error { +func (ch suicideChange) revert(mdb *MemoryStateDB) { // 如果已经自杀过了,不处理 if ch.prev { - return nil + return } acc := mdb.accounts[ch.account] if acc != nil { acc.State.Suicided = ch.prev } - return nil } func (ch suicideChange) getData(mdb *MemoryStateDB) []*types.KeyValue { @@ -241,12 +220,11 @@ func (ch suicideChange) getData(mdb *MemoryStateDB) []*types.KeyValue { return nil } -func (ch nonceChange) revert(mdb *MemoryStateDB) error { +func (ch nonceChange) revert(mdb *MemoryStateDB) { acc := mdb.accounts[ch.account] if acc != nil { acc.State.Nonce = ch.prev } - return nil } func (ch nonceChange) getData(mdb *MemoryStateDB) []*types.KeyValue { @@ -258,13 +236,12 @@ func (ch nonceChange) getData(mdb *MemoryStateDB) []*types.KeyValue { return nil } -func (ch codeChange) revert(mdb *MemoryStateDB) error { +func (ch codeChange) revert(mdb *MemoryStateDB) { acc := mdb.accounts[ch.account] if acc != nil { acc.Data.Code = ch.prevcode acc.Data.CodeHash = ch.prevhash } - return nil } func (ch codeChange) getData(mdb *MemoryStateDB) (kvset []*types.KeyValue) { @@ -277,12 +254,11 @@ func (ch codeChange) getData(mdb *MemoryStateDB) (kvset []*types.KeyValue) { return nil } -func (ch abiChange) revert(mdb *MemoryStateDB) error { +func (ch abiChange) revert(mdb *MemoryStateDB) { acc := mdb.accounts[ch.account] if acc != nil { acc.Data.Abi = ch.prevabi } - return nil } func (ch abiChange) getData(mdb *MemoryStateDB) (kvset []*types.KeyValue) { @@ -294,12 +270,11 @@ func (ch abiChange) getData(mdb *MemoryStateDB) (kvset []*types.KeyValue) { return nil } -func (ch storageChange) revert(mdb *MemoryStateDB) error { +func (ch storageChange) revert(mdb *MemoryStateDB) { acc := mdb.accounts[ch.account] if acc != nil { acc.SetState(ch.key, ch.prevalue) } - return nil } func (ch storageChange) getData(mdb *MemoryStateDB) []*types.KeyValue { @@ -323,12 +298,11 @@ func (ch storageChange) getLog(mdb *MemoryStateDB) []*types.ReceiptLog { return nil } -func (ch refundChange) revert(mdb *MemoryStateDB) error { +func (ch refundChange) revert(mdb *MemoryStateDB) { mdb.refund = ch.prev - return nil } -func (ch addLogChange) revert(mdb *MemoryStateDB) error { +func (ch addLogChange) revert(mdb *MemoryStateDB) { logs := mdb.logs[ch.txhash] if len(logs) == 1 { delete(mdb.logs, ch.txhash) @@ -338,7 +312,6 @@ func (ch addLogChange) revert(mdb *MemoryStateDB) error { mdb.logSize-- log15.Info("addLogChange::revert", "mdb.logSize", mdb.logSize) - return nil } func (ch addLogChange) getLog(mdb *MemoryStateDB) []*types.ReceiptLog { @@ -350,9 +323,8 @@ func (ch addLogChange) getLog(mdb *MemoryStateDB) []*types.ReceiptLog { return ch.logs } -func (ch addPreimageChange) revert(mdb *MemoryStateDB) error { +func (ch addPreimageChange) revert(mdb *MemoryStateDB) { delete(mdb.preimages, ch.hash) - return nil } func (ch transferChange) getData(mdb *MemoryStateDB) []*types.KeyValue { @@ -361,51 +333,3 @@ func (ch transferChange) getData(mdb *MemoryStateDB) []*types.KeyValue { func (ch transferChange) getLog(mdb *MemoryStateDB) []*types.ReceiptLog { return ch.logs } - -// revert restores the account KV captured before the transfer. Before ForkEVMTransferRevert -// preState is empty, so a reverted transfer keeps its old behaviour and leaves the cache as is. -// -// executor.StateDB, the KV used in production, must undo the write through RevertSet rather -// than Set. Under ForkExecRollback Set records the key in GetSetKeys, and every recorded key -// has to show up in the receipt (chain33's checkKV); a restored write that the surviving -// receipt no longer carries would reject the whole transaction. RevertSet removes the key from -// that list as well. A StateDB without RevertSet (the goleveldb used in some tests) still gets -// the direct writes. -func (ch transferChange) revert(mdb *MemoryStateDB) error { - for _, pre := range ch.preState { - var original []byte - if pre.existed { - original = pre.value - } - if err := mdb.revertAccountKey(pre.key, original); err != nil { - log15.Error("transferChange revert failed", "key", string(pre.key), "err", err) - return err - } - } - return nil -} - -// revertAccountKey undoes one write made by the transfer. original=nil means the key did not -// exist before the transfer and must read as absent afterwards. -func (mdb *MemoryStateDB) revertAccountKey(key, original []byte) error { - if reverter, ok := mdb.StateDB.(interface { - RevertSet(key []byte, originalValue []byte) error - }); ok { - err := reverter.RevertSet(key, original) - if err == nil { - return nil - } - if !errors.Is(err, cexecutor.ErrStateDBRevertWithoutTx) { - return fmt.Errorf("revert state key %x: %w", key, err) - } - // No transaction is open, so there is no key list to repair: write through as the - // legacy path does (the fork that records Set keys is also off). - } - if original == nil { - if deleter, ok := mdb.StateDB.(interface{ Delete([]byte) error }); ok { - return deleter.Delete(key) - } - return mdb.StateDB.Set(key, nil) - } - return mdb.StateDB.Set(key, original) -} diff --git a/plugin/dapp/evm/executor/vm/state/statedb.go b/plugin/dapp/evm/executor/vm/state/statedb.go index 2b120d021..f8aa0fc70 100644 --- a/plugin/dapp/evm/executor/vm/state/statedb.go +++ b/plugin/dapp/evm/executor/vm/state/statedb.go @@ -33,13 +33,16 @@ import ( // 执行器的Exec阶段会返回:交易收据、合约账户(包含合约地址、合约代码、合约存储信息) // 执行器的ExecLocal阶段会返回:合约创建人和合约的关联信息 type MemoryStateDB struct { - // StateDB 状态DB,从执行器框架传入 + // StateDB 状态DB,从执行器框架传入;ForkEVMTransferRevert 之后外包一层 journalKV, + // 执行期间的写入只进缓冲,随快照自动回滚 StateDB db.KV + // kv 为 nil 表示分叉之前的旧行为:写入直达 StateDB,transferChange 自带回执 KV + kv *journalKV // LocalDB 本地DB,从执行器框架传入 LocalDB db.KVDB - // CoinsAccount Coins账户操作对象,从执行器框架传入 + // CoinsAccount Coins账户操作对象,从执行器框架传入;分叉之后为绑定 journalKV 的副本 CoinsAccount *account.DB //evm 平台账户地址 @@ -72,19 +75,25 @@ type MemoryStateDB struct { stateDirty map[string]interface{} dataDirty map[string]interface{} api client.QueueProtocolAPI - - // revertErr records the first error a snapshot revert hit in the current transaction. - // RevertToSnapshot cannot return it to the EVM (its callers are already unwinding an - // error), so innerExec checks RevertError before accepting a successful execution. - revertErr error } // NewMemoryStateDB 基于执行器框架的三个DB构建内存状态机对象 // 此对象的生命周期对应一个区块,在同一个区块内的多个交易执行时共享同一个DB对象 // 开始执行下一个区块时(执行器框架调用setEnv设置的区块高度发生变更时),会重新创建此DB对象 func NewMemoryStateDB(StateDB db.KV, LocalDB db.KVDB, CoinsAccount *account.DB, blockHeight int64, api client.QueueProtocolAPI) *MemoryStateDB { + var kv *journalKV + if api.GetConfig().IsDappFork(blockHeight, evmtypes.ExecutorName, evmtypes.ForkEVMTransferRevert) { + kv = newJournalKV(StateDB) + StateDB = kv + if CoinsAccount != nil { + // 复制一份再换 DB,不改动执行器框架共享的 coins 账户对象 + coins := *CoinsAccount + CoinsAccount = coins.SetDB(kv) + } + } mdb := &MemoryStateDB{ StateDB: StateDB, + kv: kv, LocalDB: LocalDB, CoinsAccount: CoinsAccount, evmPlatformAddr: address.ExecAddress(api.GetConfig().ExecName("evm")), @@ -405,10 +414,11 @@ func (mdb *MemoryStateDB) RevertToSnapshot(version int) { // 从最近版本开始回滚 for index := len(mdb.snapshots) - 1; index >= version; index-- { - if err := mdb.snapshots[index].revert(); err != nil && mdb.revertErr == nil { - log15.Error("RevertToSnapshot failed", "snapshot", index, "err", err) - mdb.revertErr = err - } + mdb.snapshots[index].revert() + } + + if mdb.kv != nil { + mdb.kv.revertTo(version) } // 只保留回滚版本之前的版本数据 @@ -422,24 +432,15 @@ func (mdb *MemoryStateDB) RevertToSnapshot(version int) { } -// RevertError returns the first error a snapshot revert recorded in the current transaction, -// or nil when every revert succeeded. innerExec turns a non-nil value into a failed -// transaction: the account cache and the receipt no longer describe the same state. -func (mdb *MemoryStateDB) RevertError() error { - return mdb.revertErr -} - -// ResetRevertError clears the recorded revert error at the start of an execution. -func (mdb *MemoryStateDB) ResetRevertError() { - mdb.revertErr = nil -} - // Snapshot 对当前的数据状态打快照,并生成快照版本号,方便后面回滚数据 func (mdb *MemoryStateDB) Snapshot() int { id := mdb.versionID mdb.versionID++ mdb.currentVer = &Snapshot{id: id, statedb: mdb} mdb.snapshots = append(mdb.snapshots, mdb.currentVer) + if mdb.kv != nil { + mdb.kv.snapshot(id) + } log15.Debug("MemoryStateDB::Snapshot", "mdb.versionID", mdb.versionID) return id } @@ -475,6 +476,9 @@ func (mdb *MemoryStateDB) GetReceiptLogs(addr string) (logs []*types.ReceiptLog) // 因为目前执行器每次执行都是一个新的MemoryStateDB,所以,所有的快照都是从0开始的, // 这里获取的应该是从0到目前快照的所有变更; // 另外,因为合约内部会调用其它合约,也会产生数据变更,所以这里返回的数据,不止是一个合约的数据。 +// +// 分叉之后,coins/token 等经由 journalKV 的写入取缓冲中的最终值,同时写回底层 StateDB, +// 使同一区块内后续交易可见;本方法只应在执行成功、不再回滚时调用。 func (mdb *MemoryStateDB) GetChangedData(version int) (kvSet []*types.KeyValue, logs []*types.ReceiptLog) { if version < 0 || version >= len(mdb.snapshots) { return @@ -490,6 +494,15 @@ func (mdb *MemoryStateDB) GetChangedData(version int) (kvSet []*types.KeyValue, } } + if mdb.kv == nil { + return + } + for _, kv := range mdb.kv.changes() { + if err := mdb.kv.parent.Set(kv.Key, kv.Value); err != nil { + panic(err) + } + kvSet = append(kvSet, kv) + } return } @@ -544,61 +557,12 @@ const ( Error ) -// isForkActive reports whether fork is active at the current block height. -func (mdb *MemoryStateDB) isForkActive(fork string) bool { - if mdb.api == nil { - return false - } - cfg := mdb.api.GetConfig() - if cfg == nil { - return false - } - return cfg.IsDappFork(mdb.blockHeight, evmtypes.ExecutorName, fork) -} - -// transferPreState captures the raw account KV touched by Transfer, keyed the same way the -// coins account DB stores them. -func (mdb *MemoryStateDB) transferPreState(sender, recipient string) ([]kvPreState, error) { - conf := types.ConfSub(mdb.api.GetConfig(), evmtypes.ExecutorName) - ethMapFromExecutor := conf.GStr("ethMapFromExecutor") - if bytes.Equal(types.GetRealExecName([]byte(ethMapFromExecutor)), []byte("coins")) { - return mdb.capturePreState([][]byte{ - mdb.CoinsAccount.AccountKey(sender), - mdb.CoinsAccount.AccountKey(recipient), - }) - } - // paracross: balances live in the exec account of the mapped executor - return mdb.capturePreState([][]byte{ - mdb.execAccountKey(sender), - mdb.execAccountKey(recipient), - }) -} - -func (mdb *MemoryStateDB) execAccountKey(addr string) []byte { - return mdb.CoinsAccount.GetExecKVSet(mdb.evmPlatformAddr, &types.Account{Addr: addr})[0].Key -} - -// capturePreState reads the raw values behind keys before a transfer overwrites them. Only -// ErrNotFound means the key did not exist: any other error leaves us without a value to -// restore, so the transfer must fail instead of recording a state that was never read. -func (mdb *MemoryStateDB) capturePreState(keys [][]byte) ([]kvPreState, error) { - states := make([]kvPreState, 0, len(keys)) - for _, key := range keys { - pre := kvPreState{key: append([]byte(nil), key...)} - value, err := mdb.StateDB.Get(key) - switch { - case err == nil: - pre.value = append([]byte(nil), value...) - pre.existed = true - case errors.Is(err, types.ErrNotFound): - // The key did not exist yet; revert must make it absent again. - default: - log15.Error("capturePreState failed", "key", string(key), "err", err) - return nil, fmt.Errorf("read state key %x before transfer: %w", key, err) - } - states = append(states, pre) +// legacyTransferKV 分叉之前转账的回执 KV 由 transferChange 携带;分叉之后由 journalKV 提供 +func (mdb *MemoryStateDB) legacyTransferKV(kvs []*types.KeyValue) []*types.KeyValue { + if mdb.kv != nil { + return nil } - return states, nil + return kvs } // Transfer 借助coins执行器进行转账相关操作 @@ -627,15 +591,6 @@ func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool conf := types.ConfSub(mdb.api.GetConfig(), evmtypes.ExecutorName) ethMapFromExecutor := conf.GStr("ethMapFromExecutor") - var preState []kvPreState - if mdb.isForkActive(evmtypes.ForkEVMTransferRevert) { - preState, err = mdb.transferPreState(sender, recipient) - if err != nil { - log15.Error("transfer capture pre-state failed", "sender", sender, "recipient", recipient, - "amount", amount, "err", err) - return false - } - } if bytes.Equal(types.GetRealExecName([]byte(ethMapFromExecutor)), []byte("coins")) { ret, err = mdb.CoinsAccount.Transfer(sender, recipient, value) } else { //paracross @@ -651,9 +606,8 @@ func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool mdb.addChange(transferChange{ baseChange: baseChange{}, amount: value, - data: ret.KV, + data: mdb.legacyTransferKV(ret.KV), logs: ret.Logs, - preState: preState, }) } @@ -691,19 +645,6 @@ func (mdb *MemoryStateDB) TransferToToken(from, recipient, symbol string, amount if recipient == execaddress { return false, errors.New("not allow") } - // Token accounts use the same non-exec account keys as tokendb.Transfer. - var preState []kvPreState - if mdb.isForkActive(evmtypes.ForkEVMTransferRevert) { - preState, err = mdb.capturePreState([][]byte{ - tokendb.AccountKey(from), - tokendb.AccountKey(recipient), - }) - if err != nil { - log15.Error("TransferToToken capture pre-state failed", "from", from, "recipient", recipient, - "symbol", symbol, "amount", amount, "err", err) - return false, err - } - } receipt, err := tokendb.Transfer(from, recipient, amount) if err != nil { return false, err @@ -712,9 +653,8 @@ func (mdb *MemoryStateDB) TransferToToken(from, recipient, symbol string, amount mdb.addChange(transferChange{ baseChange: baseChange{}, amount: amount, - data: receipt.GetKV(), + data: mdb.legacyTransferKV(receipt.GetKV()), logs: receipt.GetLogs(), - preState: preState, }) return true, nil diff --git a/plugin/dapp/evm/executor/vm/state/transfer_revert_test.go b/plugin/dapp/evm/executor/vm/state/transfer_revert_test.go index cfb1f8529..e6023584b 100644 --- a/plugin/dapp/evm/executor/vm/state/transfer_revert_test.go +++ b/plugin/dapp/evm/executor/vm/state/transfer_revert_test.go @@ -5,8 +5,6 @@ package state import ( - "bytes" - "errors" "strings" "testing" @@ -14,7 +12,6 @@ import ( apimock "github.com/33cn/chain33/client/mocks" "github.com/33cn/chain33/common/address" "github.com/33cn/chain33/common/db" - cexecutor "github.com/33cn/chain33/executor" ctypes "github.com/33cn/chain33/types" "github.com/33cn/chain33/util" "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" @@ -40,8 +37,7 @@ func newTransferRevertConfig(t *testing.T, forkHeight int64) *ctypes.Chain33Conf return cfg } -// newTransferRevertMDBWith builds a MemoryStateDB over kv, so tests can pick a KV without -// Delete to reach transferChange.revert's fallback branch. +// newTransferRevertMDBWith builds a MemoryStateDB over kv. func newTransferRevertMDBWith(t *testing.T, cfg *ctypes.Chain33Config, kv db.KV) *MemoryStateDB { t.Helper() api := new(apimock.QueueProtocolAPI) @@ -151,119 +147,3 @@ func TestSnapshotRevertSameSlotReverseOrder(t *testing.T) { require.Equal(t, pre, mdb.GetState(contract.String(), slot), "reverse revert restores the pre-frame value") }) } - -// TestTransferRevertWithoutDeleteHidesKey covers transferChange.revert's fallback when the KV -// has no transaction open: executor.StateDB exposes RevertSet, but outside Begin there is no -// key list to repair, so revert writes directly. A key that did not exist before the transfer -// is then shadowed with a nil value, which Get reports as absent. The goleveldb used by the -// other tests has Delete and never reaches this branch. -func TestTransferRevertWithoutDeleteHidesKey(t *testing.T) { - const ( - sender = "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB" - recipient = "1PUiGcbsccfxW3zuvHXZBJfznziph5miAo" - initial = int64(1000) - moved = uint64(400) - ) - - st := cexecutor.NewStateDB(nil, nil, nil, nil).(*cexecutor.StateDB) - _, hasDelete := interface{}(st).(interface{ Delete([]byte) error }) - require.False(t, hasDelete, "executor.StateDB must not expose Delete or the fallback stays untested") - _, hasRevertSet := interface{}(st).(interface{ RevertSet([]byte, []byte) error }) - require.True(t, hasRevertSet, "executor.StateDB should undo writes through RevertSet inside a tx") - - mdb := newTransferRevertMDBWith(t, newTransferRevertConfig(t, 0), st) - fundCoinAccount(mdb, sender, initial) - recipientKey := mdb.CoinsAccount.AccountKey(recipient) - - snapshot := mdb.Snapshot() - require.True(t, mdb.Transfer(sender, recipient, moved)) - _, err := mdb.StateDB.Get(recipientKey) - require.NoError(t, err, "the transfer must have written the recipient key") - - mdb.RevertToSnapshot(snapshot) - _, err = mdb.StateDB.Get(recipientKey) - require.Equal(t, ctypes.ErrNotFound, err, "the nil Set must shadow the key as absent") - require.Equal(t, initial, coinBalance(mdb, sender)) -} - -// faultyKV wraps a KV and fails the read of one key, standing in for a store timeout. -type faultyKV struct { - db.KV - failKey []byte - err error -} - -func (f faultyKV) Get(key []byte) ([]byte, error) { - if bytes.Equal(key, f.failKey) { - return nil, f.err - } - return f.KV.Get(key) -} - -// revertFailingKV is a KV whose RevertSet always fails, so the error path of the producer can -// be observed without a real storage fault. -type revertFailingKV struct { - db.KV - err error -} - -func (f revertFailingKV) RevertSet(key, originalValue []byte) error { - return f.err -} - -// TestRevertErrorIsRecorded pins the propagation of a failed RevertSet: the error reaches the -// MemoryStateDB instead of being swallowed, and the next execution can clear it. -func TestRevertErrorIsRecorded(t *testing.T) { - const ( - sender = "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB" - recipient = "1PUiGcbsccfxW3zuvHXZBJfznziph5miAo" - initial = int64(1000) - ) - - dbDir, stateDB, _ := util.CreateTestDB() - t.Cleanup(func() { util.CloseTestDB(dbDir, stateDB) }) - revertErr := errors.New("revert failed") - mdb := newTransferRevertMDBWith(t, newTransferRevertConfig(t, 0), revertFailingKV{KV: stateDB, err: revertErr}) - fundCoinAccount(mdb, sender, initial) - - snapshot := mdb.Snapshot() - require.True(t, mdb.Transfer(sender, recipient, 400)) - mdb.RevertToSnapshot(snapshot) - require.ErrorIs(t, mdb.RevertError(), revertErr, "the producer error must reach the caller") - - mdb.ResetRevertError() - require.NoError(t, mdb.RevertError()) -} - -// TestCapturePreStateFailsTransferOnReadError pins the reviewer's Suggestion: only ErrNotFound -// may mean "the key did not exist". Any other read error leaves the pre-transfer value unknown, -// so the transfer must fail instead of recording a state that was never read. -func TestCapturePreStateFailsTransferOnReadError(t *testing.T) { - const ( - sender = "14KEKbY3kNFLfQEGJbNweV4whre7NpqzuB" - recipient = "1PUiGcbsccfxW3zuvHXZBJfznziph5miAo" - initial = int64(1000) - moved = uint64(400) - ) - - cfg := newTransferRevertConfig(t, 0) - dbDir, stateDB, _ := util.CreateTestDB() - t.Cleanup(func() { util.CloseTestDB(dbDir, stateDB) }) - - // The key is built from the same config the coins DB uses. - coinsAccount, err := account.NewAccountDB(cfg, "coins", cfg.GetCoinSymbol(), stateDB) - require.NoError(t, err) - storeErr := errors.New("store read timeout") - mdb := newTransferRevertMDBWith(t, cfg, faultyKV{ - KV: stateDB, - failKey: coinsAccount.AccountKey(recipient), - err: storeErr, - }) - fundCoinAccount(mdb, sender, initial) - - snapshot := mdb.Snapshot() - require.False(t, mdb.Transfer(sender, recipient, moved), "a pre-state read error must fail the transfer") - mdb.RevertToSnapshot(snapshot) - require.Equal(t, initial, coinBalance(mdb, sender)) - require.Zero(t, coinBalance(mdb, recipient)) -} diff --git a/plugin/dapp/evm/types/types.go b/plugin/dapp/evm/types/types.go index d831621b8..f86e8efd7 100644 --- a/plugin/dapp/evm/types/types.go +++ b/plugin/dapp/evm/types/types.go @@ -60,11 +60,12 @@ const ( // ForkEVMSuicidePayer SELFDESTRUCT pays from the executing account (Address()) instead of // CodeAddr, so that reading the balance, debiting it and suiciding all use the same account ForkEVMSuicidePayer = "ForkEVMSuicidePayer" - // ForkEVMTransferRevert makes value transfers revertible: transferChange restores the - // pre-transfer account KV, a failed CREATE rolls back its value, and Snapshot.revert - // unwinds entries in reverse order. Without it a reverted transfer stays in the account - // cache while its receipt KV is dropped, so a later call in the same tx can spend the - // phantom balance. Registered in InitFork at MaxHeight, so chains must opt in explicitly. + // ForkEVMTransferRevert makes EVM state writes revertible: MemoryStateDB buffers them in + // journalKV, which rolls back with each snapshot, a failed CREATE rolls back its value, and + // Snapshot.revert unwinds entries in reverse order. Without it a reverted transfer stays in + // the account cache while its receipt KV is dropped, so a later call in the same tx can + // spend the phantom balance. Registered in InitFork at MaxHeight, so chains must opt in + // explicitly. ForkEVMTransferRevert = "ForkEVMTransferRevert" )