diff --git a/chain33.fork.toml b/chain33.fork.toml index 4a1a9dc215..2f383a629a 100644 --- a/chain33.fork.toml +++ b/chain33.fork.toml @@ -29,6 +29,9 @@ ForkCheckEthTxSort=0 ForkProxyExec=0 ForkMaxTxFeeV1=0 ForkEthAddressFormat=0 +ForkParaFee=-1 +# 账户黑名单启用高度,命中名单的交易在此高度后视为非法交易,所在区块整块拒绝;-1 表示关闭 +ForkAccountBlacklist=-1 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/chain33.para.toml b/chain33.para.toml index 331476641a..b92a88a691 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -325,6 +325,9 @@ ForkCheckEthTxSort=0 ForkProxyExec=0 ForkMaxTxFeeV1=0 ForkEthAddressFormat=0 +ForkParaFee=-1 +# 账户黑名单启用高度,命中名单的交易在此高度后视为非法交易,所在区块整块拒绝;-1 表示关闭 +ForkAccountBlacklist=-1 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index d89fc67618..a4f7788bf5 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -522,6 +522,9 @@ ForkCheckEthTxSort=0 ForkProxyExec=0 ForkMaxTxFeeV1=0 ForkEthAddressFormat=0 +ForkParaFee=-1 +# 账户黑名单启用高度,命中名单的交易在此高度后视为非法交易,所在区块整块拒绝;-1 表示关闭 +ForkAccountBlacklist=-1 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/plugin/dapp/evm/executor/evm.go b/plugin/dapp/evm/executor/evm.go index fac4f7f902..485b8863fd 100644 --- a/plugin/dapp/evm/executor/evm.go +++ b/plugin/dapp/evm/executor/evm.go @@ -6,6 +6,7 @@ package executor import ( "bytes" + "encoding/hex" "fmt" "math/big" "os" @@ -217,6 +218,12 @@ func (evm *EVMExecutor) CheckTx(tx *types.Transaction, index int) error { return fmt.Errorf("tx empty") } + // 账户黑名单入口检查(fork 门控) + if err := checkEvmBlockedAccount(evm.GetAPI().GetConfig(), evm.GetHeight(), tx.From()); err != nil { + elog.Error("evm CheckTx blocked account", "txhash", hex.EncodeToString(tx.Hash()), "from", tx.From(), "err", err) + return err + } + return state.ProcessCheck(evm.GetMainHeight(), tx.Hash()) } diff --git a/plugin/dapp/evm/executor/exec.go b/plugin/dapp/evm/executor/exec.go index 1b4c62a24e..fa9d250810 100644 --- a/plugin/dapp/evm/executor/exec.go +++ b/plugin/dapp/evm/executor/exec.go @@ -102,6 +102,12 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in receiver = common.BytesToAddress(msg.Para()) } + // 账户黑名单:拦截收发双方命中名单的转账,保持 ExecPack 语义(返回 error 由上层包装) + if err := checkEvmBlockedAccount(cfg, evm.GetHeight(), caller.String(), receiver.String()); err != nil { + log.Error("innerExec blocked account transfer", "caller", caller.String(), "receiver", receiver.String(), "value", msg.Value(), "err", err) + return nil, err + } + if !evm.mStateDB.CanTransfer(caller.String(), msg.Value()) { log.Error("innerExec", "Not enough balance to be transferred from", caller.String(), "amout", msg.Value()) return nil, types.ErrNoBalance @@ -141,6 +147,13 @@ func (evm *EVMExecutor) innerExec(msg *common.Message, txHash []byte, sigType in // evm // 状态机中设置当前交易状态 evm.mStateDB.Prepare(common.BytesToHash(txHash), index) + + // 账户黑名单:合约调用/创建时拦截发送方与目标合约地址命中名单的情况 + if err := checkEvmBlockedAccount(cfg, evm.GetHeight(), msg.From().String(), contractAddrStr); err != nil { + log.Error("innerExec blocked account call/create", "from", msg.From().String(), "contractAddr", contractAddrStr, "err", err) + return nil, err + } + if isCreate { ret, snapshot, leftOverGas, vmerr = env.Create(runtime.AccountRef(msg.From()), contractAddr, msg.Data(), context.GasLimit, execName, msg.Alias(), msg.Value()) } else { @@ -425,6 +438,25 @@ func getCaller(tx *types.Transaction) common.Address { return *common.StringToAddress(tx.From()) } +// checkEvmBlockedAccount 在 EVM 执行内部拦截命中黑名单的地址(发送方/接收方/合约地址)。 +// 与 types.CheckTxBlockedAccount 共用同一黑名单,但按地址维度检查(此时地址已解析为字符串)。 +// 这里走 fork 门控:仅在 ForkAccountBlacklist 高度后生效。 +// 返回 error 后由调用方按 ExecPack 语义处理(保持 revert + 扣费),不升级为 ExecErr。 +func checkEvmBlockedAccount(cfg *types.Chain33Config, height int64, addrs ...string) error { + if cfg == nil || !cfg.IsFork(height, types.ForkAccountBlacklist) { + return nil + } + for _, addr := range addrs { + if addr == "" { + continue + } + if types.IsBlockedAccount(addr) { + return fmt.Errorf("%w: %s", types.ErrBlockedAccount, addr) + } + } + return nil +} + // 从交易信息中获取交易目标地址,在创建合约交易中,此地址为空 func getReceiver(action *evmtypes.EVMContractAction, mixAddressFork bool) *common.Address { if action.ContractAddr == "" { diff --git a/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go b/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go new file mode 100644 index 0000000000..9424058dd9 --- /dev/null +++ b/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go @@ -0,0 +1,72 @@ +// 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 runtime + +import ( + "errors" + "math/big" + "testing" + + "github.com/33cn/chain33/types" + "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" + "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/state" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const blockedRuntimeAddr = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" + +func newBlockedEVM(t *testing.T, blockedAddrs []string) *EVM { + t.Helper() + cfg := types.NewChain33Config(types.GetDefaultCfgstring()) + // local 标题下 SetAllFork(0),ForkAccountBlacklist 从高度 0 启用 + restore := types.SetBlockedAccountsForTest(blockedAddrs) + t.Cleanup(restore) + ctx := Context{BlockNumber: big.NewInt(1)} + return NewEVM(ctx, &state.MemoryStateDB{}, Config{}, cfg) +} + +func TestCheckBlockedAccount(t *testing.T) { + blocked := common.BytesToAddress(common.FromHex(blockedRuntimeAddr)) + normal := common.BytesToAddress(common.FromHex("0x0000000000000000000000000000000000000001")) + + t.Run("hit caller", func(t *testing.T) { + evm := newBlockedEVM(t, []string{blockedRuntimeAddr}) + err := checkBlockedAccount(evm, blocked, normal) + require.Error(t, err) + assert.True(t, errors.Is(err, types.ErrBlockedAccount)) + }) + + t.Run("hit target", func(t *testing.T) { + evm := newBlockedEVM(t, []string{blockedRuntimeAddr}) + err := checkBlockedAccount(evm, normal, blocked) + require.Error(t, err) + assert.True(t, errors.Is(err, types.ErrBlockedAccount)) + }) + + t.Run("normal pass", func(t *testing.T) { + evm := newBlockedEVM(t, []string{blockedRuntimeAddr}) + assert.NoError(t, checkBlockedAccount(evm, normal, normal)) + }) + + t.Run("empty blocklist pass", func(t *testing.T) { + cfg := types.NewChain33Config(types.GetDefaultCfgstring()) + restore := types.SetBlockedAccountsForTest([]string{}) + defer restore() + evm := NewEVM(Context{BlockNumber: big.NewInt(1)}, &state.MemoryStateDB{}, Config{}, cfg) + assert.NoError(t, checkBlockedAccount(evm, blocked, blocked)) + }) +} + +// TestCallBlockedAccount 验证 EVM.Call 在黑名单地址下返回 error(触发上层 revert) +func TestCallBlockedAccount(t *testing.T) { + evm := newBlockedEVM(t, []string{blockedRuntimeAddr}) + caller := AccountRef(common.BytesToAddress(common.FromHex(blockedRuntimeAddr))) + target := common.BytesToAddress(common.FromHex("0x0000000000000000000000000000000000000001")) + + _, _, _, err := evm.Call(caller, target, nil, 100000, 0) + require.Error(t, err) + assert.True(t, errors.Is(err, types.ErrBlockedAccount)) +} diff --git a/plugin/dapp/evm/executor/vm/runtime/evm.go b/plugin/dapp/evm/executor/vm/runtime/evm.go index abc5b5e8db..bb17e1c0c7 100644 --- a/plugin/dapp/evm/executor/vm/runtime/evm.go +++ b/plugin/dapp/evm/executor/vm/runtime/evm.go @@ -5,6 +5,7 @@ package runtime import ( + "fmt" "math/big" "sync/atomic" @@ -19,6 +20,22 @@ import ( evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types" ) +// checkBlockedAccount 合约内部调用/创建的黑名单拦截(fork 门控)。 +// 命中返回包装后的 types.ErrBlockedAccount,由调用方返回 error 触发 RevertToSnapshot。 +// 与 executor 层共用 types 黑名单,按 EVM 地址(0x 字符串)检查。 +func checkBlockedAccount(evm *EVM, addrs ...common.Address) error { + cfg := evm.cfg + if cfg == nil || evm.BlockNumber == nil || !cfg.IsFork(evm.BlockNumber.Int64(), types.ForkAccountBlacklist) { + return nil + } + for _, addr := range addrs { + if types.IsBlockedAccount(addr.String()) { + return fmt.Errorf("%w: %s", types.ErrBlockedAccount, addr.String()) + } + } + return nil +} + type ( // CanTransferFunc 检查制定账户是否有足够的金额进行转账 CanTransferFunc func(state.EVMStateDB, common.Address, uint64) bool @@ -200,6 +217,12 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas return nil, -1, gas, err } + // 账户黑名单:合约内部调用拦截,返回 error 触发 RevertToSnapshot(ExecPack 语义) + if berr := checkBlockedAccount(evm, caller.Address(), addr); berr != nil { + log.Error("Call blocked account", "caller", caller.Address().String(), "addr", addr.String(), "err", berr) + return nil, -1, gas, berr + } + p, sp, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr.String()) { // 合约地址在自定义合约和预编译合约中都不存在时,可能为外部账户 @@ -490,6 +513,12 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b return nil, -1, gas, err } + // 账户黑名单:合约创建拦截,返回 error 触发 RevertToSnapshot(ExecPack 语义) + if berr := checkBlockedAccount(evm, caller.Address(), contractAddr); berr != nil { + log.Error("Create blocked account", "caller", caller.Address().String(), "contractAddr", contractAddr.String(), "err", berr) + return nil, -1, gas, berr + } + // 向合约地址转账 cfg := evm.StateDB.GetConfig() if cfg.IsDappFork(evm.BlockNumber.Int64(), "evm", evmtypes.ForkEVMFixOverflow) { diff --git a/plugin/dapp/evm/executor/vm/state/statedb.go b/plugin/dapp/evm/executor/vm/state/statedb.go index 704aed0311..6734806a6b 100644 --- a/plugin/dapp/evm/executor/vm/state/statedb.go +++ b/plugin/dapp/evm/executor/vm/state/statedb.go @@ -97,6 +97,24 @@ func NewMemoryStateDB(StateDB db.KV, LocalDB db.KVDB, CoinsAccount *account.DB, return mdb } +// isBlockedAccount 账户黑名单兜底判定,带 ForkAccountBlacklist 门控。 +// statedb 的转账结果直接进入状态计算,未到分叉高度时不得改变执行结果,否则会与未升级节点分链。 +func (mdb *MemoryStateDB) isBlockedAccount(addrs ...string) bool { + if mdb.api == nil { + return false + } + cfg := mdb.api.GetConfig() + if cfg == nil || !cfg.IsFork(mdb.blockHeight, types.ForkAccountBlacklist) { + return false + } + for _, addr := range addrs { + if types.IsBlockedAccount(addr) { + return true + } + } + return false +} + // Prepare 每一个交易执行之前调用此方法,设置此交易的上下文信息 // 目前的上下文中包含交易哈希以及交易在区块中的序号 func (mdb *MemoryStateDB) Prepare(txHash common.Hash, txIndex int) { @@ -444,6 +462,11 @@ func (mdb *MemoryStateDB) GetChangedData(version int) (kvSet []*types.KeyValue, // CanTransfer 借助coins执行器进行转账相关操作 func (mdb *MemoryStateDB) CanTransfer(sender string, amount uint64) bool { + // 账户黑名单兜底:命中名单的发送方一律视为不可转账 + if mdb.isBlockedAccount(sender) { + log15.Error("CanTransfer blocked account", "sender", sender, "height", mdb.blockHeight) + return false + } var senderAcc *types.Account conf := types.ConfSub(mdb.api.GetConfig(), evmtypes.ExecutorName) ethMapFromExecutor := conf.GStr("ethMapFromExecutor") @@ -488,6 +511,12 @@ const ( // Transfer 借助coins执行器进行转账相关操作 func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool { log15.Debug("transfer from contract to external(contract)", "sender", sender, "recipient", recipient, "amount", amount) + // 账户黑名单兜底:收发任一方命中名单即拒绝转账(返回 false,由上层触发 revert) + if mdb.isBlockedAccount(sender, recipient) { + log15.Error("Transfer blocked account", "sender", sender, "recipient", recipient, + "amount", amount, "height", mdb.blockHeight) + return false + } var ( ret *types.Receipt err error @@ -533,6 +562,12 @@ func (mdb *MemoryStateDB) Transfer(sender, recipient string, amount uint64) bool //TransferToToken evm call token func (mdb *MemoryStateDB) TransferToToken(from, recipient, symbol string, amount int64) (bool, error) { + // 账户黑名单兜底:收发任一方命中名单即拒绝 token 转账 + if mdb.isBlockedAccount(from, recipient) { + log15.Error("TransferToToken blocked account", "from", from, "recipient", recipient, + "symbol", symbol, "amount", amount, "height", mdb.blockHeight) + return false, fmt.Errorf("%w: token transfer %s -> %s", types.ErrBlockedAccount, from, recipient) + } tokenInfo, err := mdb.tokenStatus(symbol) if err != nil { return false, err diff --git a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml index 3ea1726f6d..5be99391b9 100644 --- a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml +++ b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml @@ -220,6 +220,9 @@ ForkTicketFundAddrV1=-1 #fork6.3 ForkRootHash =1 #地址key格式化, 主要针对eth地址 ForkFormatAddressKey=0 +ForkParaFee=-1 +# 账户黑名单启用高度;-1 表示关闭 +ForkAccountBlacklist=-1 [fork.sub.coins] Enable=0