diff --git a/chain33.para.toml b/chain33.para.toml index 9e6d017af..e0e04aef5 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -109,6 +109,18 @@ genesisBlockTime=1514533390 genesis="14KEKbYtKKQm4wMthSK9J4La4nAiidGozt" minerExecs=["paracross"] #配置挖矿合约 +# 账户黑名单:命中地址禁止收发任何交易,支持base58与0x两种地址格式 +# 名单按高度分版本演进,每个 [mver.blacklist.<分叉名>] 段是一份全量名单而非增量, +# 高度 h 生效的是「高度不大于 h 的最大分叉」那一份;下面的基线段在任何分叉生效前使用 +# 启用时须在 [fork.system] 把 ForkAccountBlacklist 配成真实高度,并取消下面 V1 段注释 +[mver.blacklist] +accountBlacklist=[] + +#[mver.blacklist.ForkAccountBlacklist] +#accountBlacklist=["1xxx", "0xyyy"] +#[mver.blacklist.ForkAccountBlacklistV2] +#accountBlacklist=["1xxx", "0xyyy", "0xzzz"] + [mver.consensus] fundKeyAddr = "1BQXS6TxaYYG5mADaWij4AxhZZUTpw95a5" powLimitBits = "0x1f00ffff" diff --git a/chain33.toml b/chain33.toml index c3d96315c..6585bfc56 100644 --- a/chain33.toml +++ b/chain33.toml @@ -155,6 +155,17 @@ genesis="14KEKbYtKKQm4wMthSK9J4La4nAiidGozt" minerExecs=["ticket", "autonomy"] enableBestBlockCmp=true +# 账户黑名单:命中地址禁止收发任何交易,支持base58与0x两种地址格式 +# 名单按高度分版本演进,每个 [mver.blacklist.<分叉名>] 段是一份全量名单而非增量, +# 高度 h 生效的是「高度不大于 h 的最大分叉」那一份;下面的基线段在任何分叉生效前使用 +# 启用时须在 [fork.system] 把 ForkAccountBlacklist 配成真实高度,并取消下面 V1 段注释 +[mver.blacklist] +accountBlacklist=[] + +#[mver.blacklist.ForkAccountBlacklist] +#accountBlacklist=["1xxx", "0xyyy"] +#[mver.blacklist.ForkAccountBlacklistV2] +#accountBlacklist=["1xxx", "0xyyy", "0xzzz"] [mver.consensus] fundKeyAddr = "1BQXS6TxaYYG5mADaWij4AxhZZUTpw95a5" diff --git a/docs/security/evm-account-blacklist.md b/docs/security/evm-account-blacklist.md index 82b1c75b7..462c52666 100644 --- a/docs/security/evm-account-blacklist.md +++ b/docs/security/evm-account-blacklist.md @@ -19,13 +19,16 @@ chain33 在框架层引入 `ForkAccountBlacklist` 与 `types.CheckTxBlockedAccou ## 1. chain33 框架层覆盖了什么 -三处入口最终都走 `types.checkTxBlockedAccountCore`(chain33 `types/account_blacklist.go`): +三处入口最终都走 `types.checkTxBlockedAccount`(chain33 `types/account_blacklist.go`)。 +启动时按 `[mver.blacklist.*]` 与分叉高度打成只读快照,运行期用 `blacklistAt(height)` 取 +「高度不大于 h 的最大版本」,**调用处不再写 `IsFork(height, ForkAccountBlacklist)`**。 +分叉高度之前取到的是空名单,效果等同未启用。 -| 层 | 位置 | fork 门控 | +| 层 | 位置 | 选版高度 | |---|---|---| -| mempool 入口 | `system/mempool/check.go:76`、`eventprocess.go:216/339` | 无,随升级立即生效 | -| 出块 / 验块 | `system/consensus/base.go:606` `AddTxsToBlock` | 有 | -| executor.checkTx | `executor/execenv.go:173` | 有 | +| mempool 入口 | `CheckTxBlockedAccountImmediate`(`mempool/check.go`、延时交易入口) | 即将打包的下一高度 `last+1` | +| 出块 / 验块 | `BaseClient.AddTxsToBlock` | 当前块高度 | +| executor.checkTx | `executor/execenv.go` `checkTx` / `checkTxGroup` | `e.height`(出块/回放=块高度;进池 EventCheckTx 目前仍是 last) | 判定四个维度:`tx.From()`、`tx.GetTo()`、`tx.GetRealToAddr()`、 EVM payload 中的 `ContractAddr` 与**恰好 20 字节**的 `Para`。 @@ -48,7 +51,7 @@ EVM payload 中的 `ContractAddr` 与**恰好 20 字节**的 `Para`。 框架只解析交易**信封**。合约跑起来之后内部 CALL 了谁、以谁的名义付款, 只存在于运行时栈和 calldata 里。以下每条路径在 `blacklist_gap_test.go` 都有对应用例, -用真实 coins 账户断言余额,并以 fork 关闭为对照证明路径确实是活的。 +用真实 coins 账户断言余额,并以名单尚未生效的高度为对照证明路径确实是活的。 ### B1. 黑名单合约被内部 CALL 唤醒后转出自身余额 @@ -151,8 +154,14 @@ chain33 的 `Para` 维度看不见,而 `BytesToAddress` 取后 20 字节仍能 在此之前,"纵深防御"只作为评审结论记录在源码注释与本文中,不在本 PR 内删除。 -## 6. fork 门控 +## 6. 按高度选版(不再单独 IsFork) -statedb / runtime 两层的检查都以 `cfg.IsFork(height, ForkAccountBlacklist)` 门控。 -未到分叉高度时不得改变执行结果,否则与未升级节点分链(用例 `TestGap_ForkGateBlocksNothingBeforeHeight`)。 -mempool 入口无门控是故意的:它是节点本地行为、不进状态计算,随二进制升级立即止血。 +statedb / runtime / `checkEvmBlockedAccount` 都走 `cfg.IsBlockedAccount(addr, height)`, +与框架共用同一份快照。未到对应分叉高度时该版本名单为空,不得改变执行结果, +否则与未升级节点分链(用例 `TestGap_ForkGateBlocksNothingBeforeHeight` 用 +`SetBlockedAccountsForTest(fromHeight, addrs)` 把名单推到未来高度来验证)。 + +不要在 EVM 侧再加一层 `IsFork(height, ForkAccountBlacklist)`:门控已经固化在快照的版本高度里, +重复门控会在名单演进(V2 空名单解除、V3 再拦)时和框架选版不一致。 + +mempool 入口按 `last+1` 选版是为了和下一块打包对齐;真正的网络级拦截点仍是共识在已到达高度上的判定。 diff --git a/go.mod b/go.mod index d66748b0d..5cac7f74f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.12 replace github.com/ava-labs/avalanchego => github.com/33cn/avalanchego v1.10.10-0.20240529041529-ada691598153 require ( - github.com/33cn/chain33 v1.69.1-0.20260806044231-1edb5ac8d4fd + github.com/33cn/chain33 v1.71.0 github.com/BurntSushi/toml v1.3.2 github.com/NebulousLabs/Sia v1.3.7 github.com/bitly/go-simplejson v0.5.0 diff --git a/go.sum b/go.sum index b1b17f029..83fc1639a 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/33cn/chain33 v1.69.1-0.20260806044231-1edb5ac8d4fd h1:1W3IAqtTf9ILrH326JWYcdBFsLxNhsvWSGD6lOyVbZw= -github.com/33cn/chain33 v1.69.1-0.20260806044231-1edb5ac8d4fd/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= +github.com/33cn/chain33 v1.71.0 h1:n3XbgHU0vcCL/wBG3+KlnKX3bzXiZAvn5IC6c4lHQTo= +github.com/33cn/chain33 v1.71.0/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v41.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= diff --git a/plugin/dapp/evm/cmd/ci/chain33.evm.test.toml b/plugin/dapp/evm/cmd/ci/chain33.evm.test.toml index 4f14c3293..828c8376e 100644 --- a/plugin/dapp/evm/cmd/ci/chain33.evm.test.toml +++ b/plugin/dapp/evm/cmd/ci/chain33.evm.test.toml @@ -154,6 +154,19 @@ genesisBlockTime=1514533394 genesis="0xd83b69C56834E85e023B1738E69BFA2F0dd52905" hotkeyAddr="0xd83b69C56834E85e023B1738E69BFA2F0dd52905" waitTxMs=10 + +# 账户黑名单:命中地址禁止收发任何交易,支持base58与0x两种地址格式 +# 名单按高度分版本演进,每个 [mver.blacklist.<分叉名>] 段是一份全量名单而非增量, +# 高度 h 生效的是「高度不大于 h 的最大分叉」那一份;下面的基线段在任何分叉生效前使用 +# 启用时须在 [fork.system] 把 ForkAccountBlacklist 配成真实高度,并取消下面 V1 段注释 +[mver.blacklist] +accountBlacklist=[] + +#[mver.blacklist.ForkAccountBlacklist] +#accountBlacklist=["1xxx", "0xyyy"] +#[mver.blacklist.ForkAccountBlacklistV2] +#accountBlacklist=["1xxx", "0xyyy", "0xzzz"] + [mver.consensus] fundKeyAddr = "1BQXS6TxaYYG5mADaWij4AxhZZUTpw95a5" powLimitBits="0x1f00ffff" diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index 618409ca2..3eefa11b9 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -157,6 +157,17 @@ genesis="0xDe79A84DD3A16BB91044167075dE17a1CA4b1d6b" minerExecs=["ticket", "autonomy"] enableBestBlockCmp=true +# 账户黑名单:命中地址禁止收发任何交易,支持base58与0x两种地址格式 +# 名单按高度分版本演进,每个 [mver.blacklist.<分叉名>] 段是一份全量名单而非增量, +# 高度 h 生效的是「高度不大于 h 的最大分叉」那一份;下面的基线段在任何分叉生效前使用 +# 启用时须在 [fork.system] 把 ForkAccountBlacklist 配成真实高度,并取消下面 V1 段注释 +[mver.blacklist] +accountBlacklist=[] + +#[mver.blacklist.ForkAccountBlacklist] +#accountBlacklist=["1xxx", "0xyyy"] +#[mver.blacklist.ForkAccountBlacklistV2] +#accountBlacklist=["1xxx", "0xyyy", "0xzzz"] [mver.consensus] fundKeyAddr = "1BQXS6TxaYYG5mADaWij4AxhZZUTpw95a5" diff --git a/plugin/dapp/evm/executor/attack_integration_test.go b/plugin/dapp/evm/executor/attack_integration_test.go index 9b020a9f4..f316a38f5 100644 --- a/plugin/dapp/evm/executor/attack_integration_test.go +++ b/plugin/dapp/evm/executor/attack_integration_test.go @@ -257,7 +257,7 @@ func TestWBTYOverflowAttackIntegration(t *testing.T) { // 使用 IsBlockedAccount 能识别的 ETH 地址形态(addrFromRole 返回 tx.From())。 attackerAddr := addrFromRole(cfg, roleAttacker) accompliceAddr := addrFromRole(cfg, roleAccomplice) - restore := ctypes.SetBlockedAccountsForTest([]string{attackerAddr, accompliceAddr}) + restore := cfg.SetBlockedAccountsForTest(0, []string{attackerAddr, accompliceAddr}) t.Cleanup(restore) // 真实节点上的闸门在 chain33 框架层:executor.checkTx 在调用驱动 Exec 之前 diff --git a/plugin/dapp/evm/executor/exec.go b/plugin/dapp/evm/executor/exec.go index 755594d66..8a57e3721 100644 --- a/plugin/dapp/evm/executor/exec.go +++ b/plugin/dapp/evm/executor/exec.go @@ -441,8 +441,8 @@ func getCaller(tx *types.Transaction) common.Address { } // checkEvmBlockedAccount 在 EVM 执行内部拦截命中黑名单的地址(发送方/接收方/合约地址)。 -// 与 types.CheckTxBlockedAccount 共用同一黑名单,但按地址维度检查(此时地址已解析为字符串)。 -// 这里走 fork 门控:仅在 ForkAccountBlacklist 高度后生效。 +// 与 types.CheckTxBlockedAccount 共用同一份按高度分版本的名单,但按地址维度检查(此时地址已解析为字符串)。 +// 不需要额外的 fork 门控:名单版本已按高度选定,分叉高度之前取到的是空名单。 // 返回 error 后由调用方按 ExecPack 语义处理(保持 revert + 扣费),不升级为 ExecErr。 // // 分层说明(详见 docs/security/evm-account-blacklist.md): @@ -453,14 +453,15 @@ func getCaller(tx *types.Transaction) common.Address { // chain33 的 Para 维度看不见,而 BytesToAddress 取后 20 字节仍能解析出黑名单地址。 // 这些分支已随本分支在主网执行过,撤除需新开 fork 或先审计历史,不可直接删除。 func checkEvmBlockedAccount(cfg *types.Chain33Config, height int64, addrs ...string) error { - if cfg == nil || !cfg.IsFork(height, types.ForkAccountBlacklist) { + if cfg == nil { return nil } for _, addr := range addrs { if addr == "" { continue } - if types.IsBlockedAccount(addr) { + if cfg.IsBlockedAccount(addr, height) { + log.Error("checkEvmBlockedAccount hit", "height", height, "addr", addr) return fmt.Errorf("%w: %s", types.ErrBlockedAccount, addr) } } diff --git a/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go b/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go index 4909326ba..b82498652 100644 --- a/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go +++ b/plugin/dapp/evm/executor/vm/runtime/account_blacklist_test.go @@ -21,8 +21,8 @@ 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) + // 名单自高度 0 起生效,覆盖下面 Context 的区块高度 1 + restore := cfg.SetBlockedAccountsForTest(0, blockedAddrs) t.Cleanup(restore) ctx := Context{BlockNumber: big.NewInt(1)} return NewEVM(ctx, &state.MemoryStateDB{}, Config{}, cfg) @@ -53,11 +53,21 @@ func TestCheckBlockedAccount(t *testing.T) { t.Run("empty blocklist pass", func(t *testing.T) { cfg := types.NewChain33Config(types.GetDefaultCfgstring()) - restore := types.SetBlockedAccountsForTest([]string{}) - defer restore() + defer cfg.SetBlockedAccountsForTest(0, []string{})() evm := NewEVM(Context{BlockNumber: big.NewInt(1)}, &state.MemoryStateDB{}, Config{}, cfg) assert.NoError(t, checkBlockedAccount(evm, blocked, blocked)) }) + + // 名单自高度 H 起生效时,H 之前的区块必须放行,保证历史回放结果不变 + t.Run("before fork height pass", func(t *testing.T) { + cfg := types.NewChain33Config(types.GetDefaultCfgstring()) + defer cfg.SetBlockedAccountsForTest(100, []string{blockedRuntimeAddr})() + evm := NewEVM(Context{BlockNumber: big.NewInt(99)}, &state.MemoryStateDB{}, Config{}, cfg) + assert.NoError(t, checkBlockedAccount(evm, blocked, blocked)) + + evm = NewEVM(Context{BlockNumber: big.NewInt(100)}, &state.MemoryStateDB{}, Config{}, cfg) + assert.Error(t, checkBlockedAccount(evm, blocked, blocked)) + }) } // TestCallBlockedAccount 验证 EVM.Call 在黑名单地址下返回 error(触发上层 revert) 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 435f73a9b..c61b7f08a 100644 --- a/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go +++ b/plugin/dapp/evm/executor/vm/runtime/blacklist_gap_test.go @@ -8,7 +8,7 @@ package runtime // 只能由 EVM 内部检查拦截的资产打出路径。每个用例都: // 1. 用真实 MemoryStateDB + coins 账户,断言余额而不是只断言 error; // 2. 先证明交易信封上的地址全部干净(chain33 放行),再证明 EVM 层拦下; -// 3. 以 fork 关闭作为对照,证明放行时资产确实会被打出,测试不是空转。 +// 3. 以清空名单作为对照,证明放行时资产确实会被打出,测试不是空转。 // // 场景编号对应 docs/security/evm-account-blacklist.md 的 B1-B4。 @@ -60,7 +60,7 @@ func newGapEnv(t *testing.T, blocked []string) *gapEnv { mdb := state.NewMemoryStateDB(stateDB, localDB, coins, gapTestHeight, api) mdb.Prepare(vmcommon.BytesToHash([]byte("blacklist-gap")), 0) - restore := ctypes.SetBlockedAccountsForTest(blocked) + restore := cfg.SetBlockedAccountsForTest(0, blocked) t.Cleanup(restore) env := &gapEnv{t: t, cfg: cfg, mdb: mdb, coins: coins} @@ -95,7 +95,7 @@ func (e *gapEnv) deploy(addr, creator vmcommon.Address, code []byte) { func (e *gapEnv) assertEnvelopeClean(addrs ...vmcommon.Address) { e.t.Helper() for _, a := range addrs { - require.False(e.t, ctypes.IsBlockedAccount(a.String()), "envelope address %s must be clean", a) + require.False(e.t, e.cfg.IsBlockedAccount(a.String(), gapTestHeight), "envelope address %s must be clean", a) } } @@ -174,8 +174,8 @@ func TestGapB1_BlockedContractWokenByInnerCall(t *testing.T) { require.Equal(t, stash, env.balance(blockedC), "blocked contract must keep its balance") require.Zero(t, env.balance(user), "user must not receive blocked funds") - // 对照:fork 关闭时同一条链路会把钱打出去,证明上面的断言不是空转。 - env.cfg.SetFork(ctypes.ForkAccountBlacklist, ctypes.MaxHeight) + // 对照:名单清空后同一条链路会把钱打出去,证明上面的断言不是空转。 + env.cfg.SetBlockedAccountsForTest(0, []string{}) _, _, _, err = env.evm.Call(AccountRef(user), relay, nil, 5_000_000, 0) require.NoError(t, err) require.Zero(t, env.balance(blockedC), "with fork off the funds must leave (proves the path is live)") @@ -211,14 +211,14 @@ func TestGapB2_TokenPrecompileThirdPartyFrom(t *testing.T) { copy(calldata[4+12:], victim.Bytes()) copy(calldata[36+12:], receiver.Bytes()) calldata[len(calldata)-1] = 100 - require.False(t, ctypes.IsBlockedAccountRaw(calldata), "chain33 Para check must be blind to 100-byte calldata") + require.False(t, env.cfg.IsBlockedAccountRaw(calldata, gapTestHeight), "chain33 Para check must be blind to 100-byte calldata") ret, _, err := RunStateFulPrecompiledContract(env.evm, AccountRef(caller), CustomizePrecompiledContracts[precompile.ToHash160()], calldata, 100000) require.ErrorIs(t, err, ctypes.ErrBlockedAccount, "ret=%s", ret) // 检查一旦被移除,同一调用会落到 tokenStatus 的 ErrNotFound(本测试不注册 token), // 不再是 ErrBlockedAccount —— 用它区分"被黑名单拦下"和"因别的原因失败"。 - env.cfg.SetFork(ctypes.ForkAccountBlacklist, ctypes.MaxHeight) + env.cfg.SetBlockedAccountsForTest(0, []string{}) _, _, err = RunStateFulPrecompiledContract(env.evm, AccountRef(caller), CustomizePrecompiledContracts[precompile.ToHash160()], calldata, 100000) require.Error(t, err) require.NotErrorIs(t, err, ctypes.ErrBlockedAccount) @@ -247,7 +247,7 @@ func TestGapB3_SelfdestructBeneficiary(t *testing.T) { require.Equal(t, stash, env.balance(blockedC)) require.Zero(t, env.balance(user)) - env.cfg.SetFork(ctypes.ForkAccountBlacklist, ctypes.MaxHeight) + env.cfg.SetBlockedAccountsForTest(0, []string{}) _, _, _, err = env.evm.Call(AccountRef(user), relay2, nil, 5_000_000, 0) require.NoError(t, err) require.Zero(t, env.balance(blockedC), "with fork off SELFDESTRUCT pays the beneficiary") @@ -281,8 +281,8 @@ func TestGapB3b_DelegatecallSelfdestructDrainsCodeAddr(t *testing.T) { require.Equal(t, stash, env.balance(blockedC), "delegatecall+selfdestruct must not drain the blocked code address") require.Zero(t, env.balance(user)) - // 对照:fork 关闭时 A 的余额确实经由 CodeAddr 被打出(proxy 已自毁,换 proxy2 触发)。 - env.cfg.SetFork(ctypes.ForkAccountBlacklist, ctypes.MaxHeight) + // 对照:名单清空后 A 的余额确实经由 CodeAddr 被打出(proxy 已自毁,换 proxy2 触发)。 + env.cfg.SetBlockedAccountsForTest(0, []string{}) _, _, _, err = env.evm.Call(AccountRef(user), proxy2, nil, 5_000_000, 0) require.NoError(t, err) require.Zero(t, env.balance(blockedC), "with fork off, opSuicide pays from CodeAddr (=A)") @@ -317,7 +317,7 @@ func TestGapB4_BlockedContractInnerValueCall(t *testing.T) { func TestGap_ForkGateBlocksNothingBeforeHeight(t *testing.T) { user, blockedC := addr(0x16), addr(0xa6) env := newGapEnv(t, []string{blockedC.String()}) - env.cfg.SetFork(ctypes.ForkAccountBlacklist, gapTestHeight+1) + env.cfg.SetBlockedAccountsForTest(gapTestHeight+1, []string{blockedC.String()}) const stash = int64(100_000) env.fund(blockedC, stash) @@ -334,14 +334,15 @@ func TestGap_ForkGateBlocksNothingBeforeHeight(t *testing.T) { // 由 innerExec 的 receiver 检查兜底。此处固定该行为,防止两侧被无意改成不一致。 func TestGap_ParaLengthAsymmetry(t *testing.T) { blockedC := addr(0xa7) - restore := ctypes.SetBlockedAccountsForTest([]string{blockedC.String()}) + cfg := ctypes.NewChain33Config(ctypes.GetDefaultCfgstring()) + restore := cfg.SetBlockedAccountsForTest(0, []string{blockedC.String()}) t.Cleanup(restore) padded := make([]byte, 32) copy(padded[12:], blockedC.Bytes()) - require.False(t, ctypes.IsBlockedAccountRaw(padded), "chain33 is blind to 32-byte padded Para") + require.False(t, cfg.IsBlockedAccountRaw(padded, 0), "chain33 is blind to 32-byte padded Para") require.Equal(t, blockedC, vmcommon.BytesToAddress(padded), "EVM resolves the same bytes to the blocked address") - require.True(t, ctypes.IsBlockedAccount(vmcommon.BytesToAddress(padded).String()), + require.True(t, cfg.IsBlockedAccount(vmcommon.BytesToAddress(padded).String(), 0), "so the EVM-side receiver check catches what chain33 misses") } diff --git a/plugin/dapp/evm/executor/vm/runtime/evm.go b/plugin/dapp/evm/executor/vm/runtime/evm.go index 8323e2456..beb3ac9e3 100644 --- a/plugin/dapp/evm/executor/vm/runtime/evm.go +++ b/plugin/dapp/evm/executor/vm/runtime/evm.go @@ -20,9 +20,9 @@ import ( evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types" ) -// checkBlockedAccount 合约内部调用/创建的黑名单拦截(fork 门控)。 +// checkBlockedAccount 合约内部调用/创建的黑名单拦截,按当前区块高度选取名单版本。 // 命中返回包装后的 types.ErrBlockedAccount,由调用方返回 error 触发 RevertToSnapshot。 -// 与 executor 层共用 types 黑名单,按 EVM 地址(0x 字符串)检查。 +// 与 executor 层共用同一份按高度分版本的名单,按 EVM 地址(0x 字符串)检查。 // // 这是 chain33 框架层看不见的一层:框架只解析交易信封(from/to/ContractAddr/Para), // 合约运行后内部 CALL 了谁只存在于运行时栈上。Call 的 target 检查是阻止 @@ -35,11 +35,13 @@ import ( // 唯一例外(DELEGATECALL 到黑名单代码执行 SELFDESTRUCT)由 statedb.Transfer 兜底。 func checkBlockedAccount(evm *EVM, addrs ...common.Address) error { cfg := evm.cfg - if cfg == nil || evm.BlockNumber == nil || !cfg.IsFork(evm.BlockNumber.Int64(), types.ForkAccountBlacklist) { + if cfg == nil || evm.BlockNumber == nil { return nil } + height := evm.BlockNumber.Int64() for _, addr := range addrs { - if types.IsBlockedAccount(addr.String()) { + if cfg.IsBlockedAccount(addr.String(), height) { + log.Error("checkBlockedAccount hit", "height", height, "addr", addr.String()) return fmt.Errorf("%w: %s", types.ErrBlockedAccount, addr.String()) } } diff --git a/plugin/dapp/evm/executor/vm/state/statedb.go b/plugin/dapp/evm/executor/vm/state/statedb.go index 9490e43b9..b3cc16864 100644 --- a/plugin/dapp/evm/executor/vm/state/statedb.go +++ b/plugin/dapp/evm/executor/vm/state/statedb.go @@ -97,8 +97,9 @@ func NewMemoryStateDB(StateDB db.KV, LocalDB db.KVDB, CoinsAccount *account.DB, return mdb } -// isBlockedAccount 账户黑名单兜底判定,带 ForkAccountBlacklist 门控。 -// statedb 的转账结果直接进入状态计算,未到分叉高度时不得改变执行结果,否则会与未升级节点分链。 +// isBlockedAccount 账户黑名单兜底判定,按当前区块高度选取名单版本。 +// statedb 的转账结果直接进入状态计算,名单版本必须严格跟随高度, +// 否则会用新名单判定旧区块,与未升级节点分链。 // // 这一层是资产打出的最后一道闸,覆盖 chain33 与 runtime.Call 都看不见的两条路径 // (docs/security/evm-account-blacklist.md 场景 B2 / B3,blacklist_gap_test.go 有对应用例): @@ -114,11 +115,11 @@ func (mdb *MemoryStateDB) isBlockedAccount(addrs ...string) bool { return false } cfg := mdb.api.GetConfig() - if cfg == nil || !cfg.IsFork(mdb.blockHeight, types.ForkAccountBlacklist) { + if cfg == nil { return false } for _, addr := range addrs { - if types.IsBlockedAccount(addr) { + if cfg.IsBlockedAccount(addr, mdb.blockHeight) { return true } } diff --git a/plugin/dapp/evm/executor/vm/state/statedb_test.go b/plugin/dapp/evm/executor/vm/state/statedb_test.go index 46cb66bbd..a8c429c0b 100644 --- a/plugin/dapp/evm/executor/vm/state/statedb_test.go +++ b/plugin/dapp/evm/executor/vm/state/statedb_test.go @@ -204,7 +204,7 @@ func TestBlacklistBlocksFundOps(t *testing.T) { cfg.SetFork(ctypes.ForkAccountBlacklist, 0) blockedAddr := "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt" - restore := ctypes.SetBlockedAccountsForTest([]string{blockedAddr}) + restore := cfg.SetBlockedAccountsForTest(0, []string{blockedAddr}) t.Cleanup(restore) // CanTransfer:黑名单发送方拒绝