From 86b4b37dd028750ec120070dd13621bfdeb5b910 Mon Sep 17 00:00:00 2001 From: bysomeone <11565373+bysomeone@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:09:26 +0800 Subject: [PATCH 1/5] fix(evm): derive internal CREATE addresses from caller nonce behind a fork EVM-internal CREATE used hash(txHash||origin), so every CREATE inside one transaction produced the same address. EVM.Create also accepted an existing account silently, so a factory deploying two children in one call had the second overwrite the first while the transaction still reported success. Gate the Ethereum behaviour behind ForkEVMContractCreateV2. Registered at MaxHeight (off by default, opt-in per chain) like ForkEVMTransferRevert: enabling it from genesis on an existing chain would rewrite historical execution. - CREATE address = CreateAddress(executing account, its nonce); the caller nonce is incremented before any create snapshot and persisted in contract state, so it survives across transactions and failed creates. - Newly created contracts start at nonce 1 (EIP-161); an account with a nonce is no longer treated as non-existent. - CREATE/CREATE2 fail with ErrContractAddressCollision (consuming the forwarded gas) when the target already has a nonce or code. - ErrCodeStoreOutOfGas rolls the creation back instead of keeping the account and pushing its address. - CREATE/CREATE2 forward 63/64 of the remaining gas (EIP-150); CREATE2 no longer halves it, and converts value precision for ETH-signed transactions the same way CREATE does. Address construction for the new path bypasses the chain33 address driver: a btc-format driver cannot parse a 0x hex string and produced an all-zero raw address. Fork-off execution is unchanged; the nonceChange KV write is gated on the same fork. Co-authored-by: Claude --- chain33.fork.toml | 2 + chain33.para.toml | 2 + .../dapp/evm/cmd/ci2/chain33.proxyminer.toml | 2 + .../evm/executor/create_compat_repro_test.go | 398 ++++++++++++++++++ plugin/dapp/evm/executor/query_test.go | 4 + plugin/dapp/evm/executor/vm/common/address.go | 8 + plugin/dapp/evm/executor/vm/runtime/evm.go | 25 +- .../evm/executor/vm/runtime/instructions.go | 38 +- plugin/dapp/evm/executor/vm/state/account.go | 12 +- plugin/dapp/evm/executor/vm/state/snapshot.go | 15 +- plugin/dapp/evm/types/evm.go | 4 + plugin/dapp/evm/types/types.go | 4 + 12 files changed, 499 insertions(+), 15 deletions(-) create mode 100644 plugin/dapp/evm/executor/create_compat_repro_test.go diff --git a/chain33.fork.toml b/chain33.fork.toml index a855a35a6..bada5286b 100644 --- a/chain33.fork.toml +++ b/chain33.fork.toml @@ -103,6 +103,8 @@ ForkEVMFixOverflow=0 ForkEVMSuicidePayer=0 # Revertible value transfers; -1 keeps the fix off (resolved to MaxHeight) ForkEVMTransferRevert=-1 +# Internal CREATE/CREATE2 follow Ethereum address derivation; -1 keeps it off +ForkEVMContractCreateV2=-1 [fork.sub.evmxgo] diff --git a/chain33.para.toml b/chain33.para.toml index a6c8915cb..bf9aaa08a 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -439,6 +439,8 @@ ForkEVMFixOverflow=0 ForkEVMSuicidePayer=0 # Revertible value transfers; -1 keeps the fix off (resolved to MaxHeight) ForkEVMTransferRevert=-1 +# Internal CREATE/CREATE2 follow Ethereum address derivation; -1 keeps it off +ForkEVMContractCreateV2=-1 [fork.sub.blackwhite] Enable=0 diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index 316bbd346..31124fc31 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -607,6 +607,8 @@ ForkEVMFixOverflow=0 ForkEVMSuicidePayer=0 # Revertible value transfers; -1 keeps the fix off (resolved to MaxHeight) ForkEVMTransferRevert=-1 +# Internal CREATE/CREATE2 follow Ethereum address derivation; -1 keeps it off +ForkEVMContractCreateV2=-1 [fork.sub.evmxgo] Enable=0 diff --git a/plugin/dapp/evm/executor/create_compat_repro_test.go b/plugin/dapp/evm/executor/create_compat_repro_test.go new file mode 100644 index 000000000..3f756246f --- /dev/null +++ b/plugin/dapp/evm/executor/create_compat_repro_test.go @@ -0,0 +1,398 @@ +package executor + +import ( + "bytes" + "testing" + + apimock "github.com/33cn/chain33/client/mocks" + "github.com/33cn/chain33/common/address" + ctypes "github.com/33cn/chain33/types" + vcomm "github.com/33cn/plugin/plugin/dapp/evm/executor/vm/common" + evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types" + gethcommon "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" +) + +// Factory whose runtime code performs two CREATEs of the same 5-byte init code +// (0x60006000f3: return empty runtime) and returns the two resulting addresses +// as 64 bytes of return data. +// +// runtime: +// +// PUSH5 0x60006000f3; PUSH1 0x00; MSTORE ; mem[0..32] = initcode +// PUSH1 5; PUSH1 27; PUSH1 0; CREATE ; addr1 +// PUSH1 5; PUSH1 27; PUSH1 0; CREATE ; addr2 +// PUSH1 0x20; MSTORE ; mem[32..64] = addr2 +// PUSH1 0x00; MSTORE ; mem[0..32] = addr1 +// PUSH1 0x40; PUSH1 0x00; RETURN +const factoryCreateTwiceRuntime = "6460006000f3" + "600052" + "6005601b6000f0" + "6005601b6000f0" + "602052" + "600052" + "60406000f3" + +// deploy wrapper: CODECOPY runtime (34 bytes at offset 12) then RETURN it +const factoryCreateTwiceBytecode = "6022600c60003960226000f3" + factoryCreateTwiceRuntime + +// Factory whose runtime code performs a single CREATE and returns the resulting +// address as 32 bytes of return data. +const factoryCreateOnceRuntime = "6460006000f3" + "600052" + "6005601b6000f0" + "600052" + "60206000f3" + +// deploy wrapper: CODECOPY runtime (24 bytes at offset 12) then RETURN it +const factoryCreateOnceBytecode = "6018600c60003960186000f3" + factoryCreateOnceRuntime + +// CREATE2 with a constant salt and constant init code: the address is by design +// the same on both calls, so the second one must fail as a collision (EIP-1014). +const create2TwiceBytecode = "6026600c60003960266000f3" + + "6460006000f3" + "600052" + + "60016005601b6000f5" + + "60016005601b6000f5" + + "602052" + "600052" + "60406000f3" + +// deployContract deploys bytecode and applies the resulting KV set the way the +// block executor does between transactions. +func deployContract(t *testing.T, cfg *ctypes.Chain33Config, exec *EVMExecutor, bytecode string) string { + t.Helper() + execAddr := address.ExecAddress(cfg.ExecName(evmtypes.ExecutorName)) + action := &evmtypes.EVMContractAction{Code: vcomm.FromHex(bytecode), ContractAddr: execAddr} + tx := &ctypes.Transaction{ + ChainID: cfg.GetChainID(), + Execer: []byte(cfg.ExecName(evmtypes.ExecutorName)), + Payload: ctypes.Encode(action), + Fee: 1e6, To: execAddr, Nonce: 0, + } + signTx(tx, roleDeployer) + resetEvmExecAddress(cfg) + r, err := exec.Exec(tx, 0) + if err != nil || r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("deploy: err=%v ty=%d", err, r.GetTy()) + } + addr := vcomm.NewContractAddress(*vcomm.StringToAddress(tx.From()), tx.Hash()).String() + for _, kv := range r.KV { + if err := exec.GetStateDB().Set(kv.Key, kv.Value); err != nil { + t.Fatalf("apply deploy kv: %v", err) + } + } + return addr +} + +// callRet runs a call and returns the receipt plus the EVM return data. +func callRet(t *testing.T, cfg *ctypes.Chain33Config, exec *EVMExecutor, addr string) (*ctypes.Receipt, []byte, error) { + t.Helper() + r, err := exec.Exec(makeCallTx(cfg, roleDeployer, addr, nil, 0), 0) + if err != nil || r == nil { + return r, nil, err + } + var ret []byte + for _, lg := range r.Logs { + if lg.Ty != evmtypes.TyLogCallContract { + continue + } + var cr evmtypes.ReceiptEVMContract + if err := ctypes.Decode(lg.Log, &cr); err != nil { + t.Fatalf("decode receipt: %v", err) + } + ret = cr.Ret + } + return r, ret, nil +} + +// newTestExecutorFrom builds another executor over the same state/local DB, used +// to prove that state written by one transaction is visible to the next one. +func newTestExecutorFrom(t *testing.T, cfg *ctypes.Chain33Config, height int64, src *EVMExecutor) *EVMExecutor { + t.Helper() + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig").Return(cfg) + + exec := NewEVMExecutor() + exec.SetAPI(api) + exec.SetLocalDB(src.GetLocalDB()) + exec.SetStateDB(src.GetStateDB()) + exec.SetEnv(height, 0, 0) + exec.CheckInit() + return exec +} + +// contractCreateV2ForkHeight is above the execution height (0) used by the +// tests below, so setting it keeps the fork disabled. +const contractCreateV2ForkHeight = 1000 + +func enableContractCreateV2(cfg *ctypes.Chain33Config) { + cfg.SetDappFork("evm", evmtypes.ForkEVMContractCreateV2, 0) +} + +func disableContractCreateV2(cfg *ctypes.Chain33Config) { + cfg.SetDappFork("evm", evmtypes.ForkEVMContractCreateV2, contractCreateV2ForkHeight) +} + +// resetEvmExecAddress refreshes the package-level exec address caches. They are +// computed once by Init, and other tests in this package switch the global EVM +// address driver (query_test.go), which leaves the caches stale and makes the +// executor treat a deploy as a plain transfer. +func resetEvmExecAddress(cfg *ctypes.Chain33Config) { + evmExecAddress = vcomm.StringToAddress(address.ExecAddress(cfg.ExecName(evmtypes.ExecutorName))).String() + evmExecFormatAddress = address.FormatEthAddress(evmExecAddress) +} + +// readContractNonce reads the persisted EVM nonce of a contract account. +func readContractNonce(t *testing.T, exec *EVMExecutor, addr string) uint64 { + t.Helper() + key := []byte("mavl-" + evmtypes.ExecutorName + "-state: " + addr) + raw, err := exec.GetStateDB().Get(key) + if err != nil { + return 0 + } + var st evmtypes.EVMContractState + if err := ctypes.Decode(raw, &st); err != nil { + t.Fatalf("decode contract state: %v", err) + } + return uint64(st.GetNonce()) +} + +// ethCreateAddress computes the ethereum CREATE address for a chain33-format +// contract address at the given nonce. +func ethCreateAddress(t *testing.T, chain33Addr string, nonce uint64) gethcommon.Address { + t.Helper() + raw := vcomm.StringToAddress(chain33Addr) + if raw == nil { + t.Fatalf("bad contract address %q", chain33Addr) + } + return ethcrypto.CreateAddress(gethcommon.BytesToAddress(raw.Bytes()), nonce) +} + +func TestInternalCreateAddressForkOff(t *testing.T) { + cfg := newTestConfig(t) + disableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateTwiceBytecode) + t.Logf("factory deployed at %s", factoryAddr) + + r, ret, err := callRet(t, cfg, exec, factoryAddr) + if err != nil { + t.Fatalf("call factory err: %v", err) + } + if r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory bad receipt: %+v", r) + } + if len(ret) != 64 { + t.Fatalf("unexpected return length %d: %s", len(ret), vcomm.Bytes2Hex(ret)) + } + addr1 := vcomm.BytesToAddress(ret[:32]) + addr2 := vcomm.BytesToAddress(ret[32:]) + t.Logf("create #1 -> %s", addr1.String()) + t.Logf("create #2 -> %s", addr2.String()) + + // fork 关闭时保持旧行为:同一笔交易内两次 CREATE 得到同一个地址。 + if !bytes.Equal(addr1.Bytes(), addr2.Bytes()) { + t.Fatalf("fork off should keep the legacy behavior, got %s and %s", addr1.String(), addr2.String()) + } +} + +func TestInternalCreateAddressForkOn(t *testing.T) { + cfg := newTestConfig(t) + enableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateTwiceBytecode) + t.Logf("factory deployed at %s", factoryAddr) + + r, ret, err := callRet(t, cfg, exec, factoryAddr) + if err != nil { + t.Fatalf("call factory err: %v", err) + } + if r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory bad receipt: %+v", r) + } + if len(ret) != 64 { + t.Fatalf("unexpected return length %d: %s", len(ret), vcomm.Bytes2Hex(ret)) + } + addr1 := vcomm.BytesToAddress(ret[:32]) + addr2 := vcomm.BytesToAddress(ret[32:]) + t.Logf("create #1 -> %s", addr1.String()) + t.Logf("create #2 -> %s", addr2.String()) + + if addr1 == addr2 { + t.Fatalf("BUG: both CREATEs in one tx returned the same address %s", addr1.String()) + } + want1 := ethCreateAddress(t, factoryAddr, 1) + want2 := ethCreateAddress(t, factoryAddr, 2) + if !bytes.Equal(addr1.Bytes(), want1.Bytes()) { + t.Fatalf("create #1 = %s, want %s", addr1.String(), gethcommon.BytesToAddress(want1.Bytes()).Hex()) + } + if !bytes.Equal(addr2.Bytes(), want2.Bytes()) { + t.Fatalf("create #2 = %s, want %s", addr2.String(), gethcommon.BytesToAddress(want2.Bytes()).Hex()) + } + t.Logf("create #1 matches CreateAddress(factory, 1) = %s", want1.Hex()) + t.Logf("create #2 matches CreateAddress(factory, 2) = %s", want2.Hex()) +} + +func TestCreate2CollisionForkOn(t *testing.T) { + cfg := newTestConfig(t) + enableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + addr := deployContract(t, cfg, exec, create2TwiceBytecode) + + r, ret, err := callRet(t, cfg, exec, addr) + if err != nil { + t.Fatalf("call: %v", err) + } + if r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("bad receipt: %+v", r) + } + if len(ret) != 64 { + t.Fatalf("unexpected return length %d", len(ret)) + } + a1 := vcomm.BytesToAddress(ret[:32]) + a2 := vcomm.BytesToAddress(ret[32:]) + t.Logf("create2 #1 -> %s", a1.String()) + t.Logf("create2 #2 -> %s", a2.String()) + if a1 == (vcomm.Address{}) { + t.Fatalf("first CREATE2 should succeed") + } + if a2 != (vcomm.Address{}) { + t.Fatalf("second CREATE2 to an occupied address returned %s instead of failing", a2.String()) + } +} + +func TestCreateNoncePersistsAcrossTransactions(t *testing.T) { + cfg := newTestConfig(t) + enableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateOnceBytecode) + t.Logf("factory deployed at %s", factoryAddr) + + callAndApply := func(e *EVMExecutor) vcomm.Address { + r, ret, err := callRet(t, cfg, e, factoryAddr) + if err != nil { + t.Fatalf("call factory err: %v", err) + } + if r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory bad receipt: %+v", r) + } + if len(ret) != 32 { + t.Fatalf("unexpected return length %d", len(ret)) + } + applyReceiptKV(t, e, r) + return vcomm.BytesToAddress(ret) + } + + // Transaction #1: factory nonce goes 1 -> 2, the new contract is + // CreateAddress(factory, 1). + addr1 := callAndApply(exec) + want1 := ethCreateAddress(t, factoryAddr, 1) + if !bytes.Equal(addr1.Bytes(), want1.Bytes()) { + t.Fatalf("tx #1 address = %s, want %s", addr1.String(), want1.Hex()) + } + t.Logf("tx #1 -> %s (nonce 1)", addr1.String()) + + // The incremented nonce must be part of the receipt KV written to state. + var state evmtypes.EVMContractState + stateKey := []byte("mavl-" + evmtypes.ExecutorName + "-state: " + factoryAddr) + stateKV, err := exec.GetStateDB().Get(stateKey) + if err != nil { + t.Fatalf("read factory state: %v", err) + } + if err := ctypes.Decode(stateKV, &state); err != nil { + t.Fatalf("decode factory state: %v", err) + } + if state.GetNonce() != 2 { + t.Fatalf("factory nonce persisted = %d, want 2", state.GetNonce()) + } + t.Logf("factory nonce persisted to state: %d", state.GetNonce()) + + // Transaction #2 uses a fresh executor over the same DB: the address must + // come from the persisted nonce (2), not from the in-memory cache. + exec2 := newTestExecutorFrom(t, cfg, 0, exec) + addr2 := callAndApply(exec2) + want2 := ethCreateAddress(t, factoryAddr, 2) + if !bytes.Equal(addr2.Bytes(), want2.Bytes()) { + t.Fatalf("tx #2 address = %s, want %s", addr2.String(), want2.Hex()) + } + t.Logf("tx #2 -> %s (nonce 2)", addr2.String()) + + // Transaction #3 continues the sequence. + exec3 := newTestExecutorFrom(t, cfg, 0, exec2) + addr3 := callAndApply(exec3) + want3 := ethCreateAddress(t, factoryAddr, 3) + if !bytes.Equal(addr3.Bytes(), want3.Bytes()) { + t.Fatalf("tx #3 address = %s, want %s", addr3.String(), want3.Hex()) + } + t.Logf("tx #3 -> %s (nonce 3)", addr3.String()) +} + +// factory whose single CREATE always fails: the init code reverts. +// +// PUSH5 0x60006000fd; PUSH1 0; MSTORE +// PUSH1 5; PUSH1 27; PUSH1 0; CREATE; STOP +const factoryCreateRevertingChildRuntime = "6460006000fd" + "600052" + "6005601b6000f0" + "00" + +// deploy wrapper: CODECOPY runtime (17 bytes at offset 12) then RETURN it +const factoryCreateRevertingChildBytecode = "6011600c60003960116000f3" + factoryCreateRevertingChildRuntime + +// fork off: the address must stay the legacy hash(txHash||origin), and no nonce +// may be written to the contract state. +func TestInternalCreateAddressForkOffLegacy(t *testing.T) { + cfg := newTestConfig(t) + disableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateTwiceBytecode) + + tx := makeCallTx(cfg, roleDeployer, factoryAddr, nil, 0) + resetEvmExecAddress(cfg) + r, err := exec.Exec(tx, 0) + if err != nil || r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory: err=%v ty=%d", err, r.GetTy()) + } + _, ret := receiptRet(t, r) + if len(ret) != 64 { + t.Fatalf("unexpected return length %d", len(ret)) + } + + legacy := vcomm.NewContractAddress(*vcomm.StringToAddress(tx.From()), tx.Hash()) + want := vcomm.LeftPadBytes(legacy.Bytes(), 32) + for i, got := range [][]byte{ret[:32], ret[32:]} { + if !bytes.Equal(got, want) { + t.Fatalf("create #%d = %x, want legacy %x", i+1, got, want) + } + } + if nonce := readContractNonce(t, exec, factoryAddr); nonce != 0 { + t.Fatalf("fork off wrote nonce %d to contract state", nonce) + } +} + +// fork on: a failed CREATE still consumes one nonce, like go-ethereum. +func TestCreateFailureStillIncrementsNonce(t *testing.T) { + cfg := newTestConfig(t) + enableContractCreateV2(cfg) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateRevertingChildBytecode) + if nonce := readContractNonce(t, exec, factoryAddr); nonce != 1 { + t.Fatalf("new contract nonce = %d, want 1 (EIP-161)", nonce) + } + + r, _, err := callRet(t, cfg, exec, factoryAddr) + if err != nil || r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory: err=%v ty=%d", err, r.GetTy()) + } + applyReceiptKV(t, exec, r) + + if nonce := readContractNonce(t, exec, factoryAddr); nonce != 2 { + t.Fatalf("factory nonce after failed CREATE = %d, want 2", nonce) + } +} + +// receiptRet extracts the EVM return data from a receipt. +func receiptRet(t *testing.T, r *ctypes.Receipt) (*evmtypes.ReceiptEVMContract, []byte) { + t.Helper() + for _, lg := range r.Logs { + if lg.Ty != evmtypes.TyLogCallContract { + continue + } + var cr evmtypes.ReceiptEVMContract + if err := ctypes.Decode(lg.Log, &cr); err != nil { + t.Fatalf("decode receipt: %v", err) + } + return &cr, cr.Ret + } + return nil, nil +} diff --git a/plugin/dapp/evm/executor/query_test.go b/plugin/dapp/evm/executor/query_test.go index dd5ebb4e9..9926d414f 100644 --- a/plugin/dapp/evm/executor/query_test.go +++ b/plugin/dapp/evm/executor/query_test.go @@ -188,7 +188,11 @@ func initEvmExeccutor(t *testing.T, api *apimock.QueueProtocolAPI) *EVMExecutor driver, err := address.LoadDriver(2, -1) assert.Equal(t, nil, err) + // The EVM address driver is package-global: restore it so the switch does + // not leak into other tests (test order is not guaranteed). + prevDriver := vcomm.GetEvmAddressDriver() vcomm.InitEvmAddressDriver(driver) + t.Cleanup(func() { vcomm.InitEvmAddressDriver(prevDriver) }) var exec = NewEVMExecutor() exec.SetAPI(api) statDB, err := dbm.NewGoMemDB("state", "state", 1024) diff --git a/plugin/dapp/evm/executor/vm/common/address.go b/plugin/dapp/evm/executor/vm/common/address.go index 14d8d602f..f8d1cc55a 100644 --- a/plugin/dapp/evm/executor/vm/common/address.go +++ b/plugin/dapp/evm/executor/vm/common/address.go @@ -161,6 +161,14 @@ func NewEvmContractAddress(b Address, nonce uint64) Address { return a } +// NewEvmContractAddressRaw 通过nonce创建合约地址,raw 直接取以太坊地址的 20 字节, +// 不经过 chain33 地址驱动的 FormatAddr/FromString 往返转换。 +// btc 类地址驱动无法解析 0x 十六进制串,往返会得到全零 raw,内部 CREATE 必须用本方法。 +func NewEvmContractAddressRaw(b Address, nonce uint64) Address { + execAddr := ethcrypto.CreateAddress(common.BytesToAddress(b.Bytes()), nonce) + return BytesToAddress(execAddr.Bytes()) +} + // PubKey2Address pub key to address func PubKey2Address(pub []byte) Address { diff --git a/plugin/dapp/evm/executor/vm/runtime/evm.go b/plugin/dapp/evm/executor/vm/runtime/evm.go index cb916a6d7..cee3678cf 100644 --- a/plugin/dapp/evm/executor/vm/runtime/evm.go +++ b/plugin/dapp/evm/executor/vm/runtime/evm.go @@ -198,6 +198,12 @@ func (evm *EVM) CheckIsEthTx() bool { return evm.isEthTx } +// IsContractCreateV2 内部 CREATE/CREATE2 是否按以太坊语义执行(fork 门控) +func (evm *EVM) IsContractCreateV2() bool { + return evm.cfg != nil && evm.BlockNumber != nil && + evm.cfg.IsDappFork(evm.BlockNumber.Int64(), evmtypes.ExecutorName, evmtypes.ForkEVMContractCreateV2) +} + // 封装合约的各种调用逻辑中通用的预检查逻辑 func (evm *EVM) preCheck(caller ContractRef, value uint64) (pass bool, err error) { // 检查调用深度是否合法 @@ -537,6 +543,19 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b return nil, -1, gas, berr } + // 以太坊语义(fork 门控):先递增调用者 nonce,再检查目标地址是否已被占用。 + // nonce 递增必须在快照之前,这样创建失败时 nonce 仍然生效。 + if evm.IsContractCreateV2() { + callerAddr := caller.Address() + nonce := evm.StateDB.GetNonce(callerAddr.String()) + evm.StateDB.SetNonce(callerAddr.String(), nonce+1) + + if evm.StateDB.GetNonce(contractAddr.String()) != 0 || evm.StateDB.GetCodeSize(contractAddr.String()) > 0 { + log.Error("evm contract address collision", "caller", callerAddr.String(), "contractAddr", contractAddr.String()) + return nil, -1, 0, model.ErrContractAddressCollision + } + } + // 向合约地址转账 cfg := evm.StateDB.GetConfig() // From ForkEVMTransferRevert on, the snapshot is taken before the transfer so that a @@ -576,6 +595,10 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b snapshot = evm.StateDB.Snapshot() } evm.StateDB.CreateAccount(contractAddr.String(), contract.CallerAddress.String(), execName, alias) + if evm.IsContractCreateV2() { + // EIP-161:新建合约账户的 nonce 初始化为 1 + evm.StateDB.SetNonce(contractAddr.String(), 1) + } if EVMDebugOn == evm.VMConfig.Debug && evm.depth == 0 { evm.VMConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, 0) @@ -598,7 +621,7 @@ func (evm *EVM) Create(caller ContractRef, contractAddr common.Address, code []b // 如果合约代码超大,或者出现除Gas不足外的其它错误情况 // 则回滚本次合约创建操作 - if maxCodeSizeExceeded || (err != nil && err != model.ErrCodeStoreOutOfGas) { + if maxCodeSizeExceeded || (err != nil && (err != model.ErrCodeStoreOutOfGas || evm.IsContractCreateV2())) { evm.StateDB.RevertToSnapshot(snapshot) // 如果之前步骤出错,且没有回滚过,则扣除Gas diff --git a/plugin/dapp/evm/executor/vm/runtime/instructions.go b/plugin/dapp/evm/executor/vm/runtime/instructions.go index e140cbff1..0cdda30cc 100644 --- a/plugin/dapp/evm/executor/vm/runtime/instructions.go +++ b/plugin/dapp/evm/executor/vm/runtime/instructions.go @@ -642,10 +642,21 @@ func opCreate(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { gas = callContext.contract.Gas ) + if evm.IsContractCreateV2() { + // EIP-150:CREATE 最多使用剩余 Gas 的 63/64 + gas = gas - gas/64 + } + callContext.contract.UseGas(gas) stackvalue := size addr := common.NewContractAddress(evm.Origin, evm.TxHash) + if evm.IsContractCreateV2() { + // 以太坊语义:地址由调用者地址与调用者 nonce 派生 + creator := callContext.contract.Address() + nonce := evm.StateDB.GetNonce(creator.String()) + addr = common.NewEvmContractAddressRaw(creator, nonce) + } if !value.IsZero() && evm.CheckIsEthTx() { // value 是coins的值,opcall 中默认是 1e18,框架默认coins 1e8 ,需要对value 处理成精度1e8的值 @@ -660,7 +671,8 @@ func opCreate(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { res, _, returnGas, suberr := evm.Create(callContext.contract, addr, input, gas, "innerContract", "", value.Uint64()) // 出错时压栈0,否则压栈创建出来的合约对象的地址 - if suberr != nil && suberr != model.ErrCodeStoreOutOfGas { + // fork 开启时 ErrCodeStoreOutOfGas 与其它错误一致,整体失败并压栈0 + if suberr != nil && (suberr != model.ErrCodeStoreOutOfGas || evm.IsContractCreateV2()) { log15.Error("evm contract opCreate instruction error,value", suberr, value) stackvalue.Clear() } else { @@ -688,7 +700,12 @@ func opCreate2(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { ) // Apply EIP150 - gas = gas / 2 + if evm.IsContractCreateV2() { + // EIP-150:CREATE2 最多使用剩余 Gas 的 63/64 + gas = gas - gas/64 + } else { + gas = gas / 2 + } callContext.contract.UseGas(gas) // reuse size int for stackvalue stackvalue := size @@ -697,6 +714,16 @@ func opCreate2(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { if !endowment.IsZero() { bigEndowment = endowment.ToBig() } + if evm.IsContractCreateV2() && !endowment.IsZero() && evm.CheckIsEthTx() { + // value 是coins的值,opcall 中默认是 1e18,框架默认coins 1e8 ,需要对value 处理成精度1e8的值 + newValue := evm.ethPrecision2Chain33Standard(endowment.ToBig()) + bigValue, overflow := uint256.FromBig(newValue) + if overflow { + panic("opCreate2: value wrong format") + } + endowment = *bigValue + bigEndowment = endowment.ToBig() + } //res, addr, returnGas, suberr := evm.Create2(callContext.contract, input, gas, // bigEndowment, &salt) @@ -713,13 +740,8 @@ func opCreate2(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { log15.Info("opCreate2", "callContext.contract.Address()", callContext.contract.Address(), "salt", saltStr, "suberr", suberr) // 出错时压栈0,否则压栈创建出来的合约对象的地址 - if suberr != nil && suberr != model.ErrCodeStoreOutOfGas { - log15.Error("evm contract opCreate instruction error,endowment,salt", suberr, bigEndowment, salt) - stackvalue.Clear() - } else { - stackvalue.SetBytes(newContractAddr.Bytes()) - } if suberr != nil { + log15.Error("evm contract opCreate instruction error,endowment,salt", suberr, bigEndowment, salt) stackvalue.Clear() } else { stackvalue.SetBytes(newContractAddr.Bytes()) diff --git a/plugin/dapp/evm/executor/vm/state/account.go b/plugin/dapp/evm/executor/vm/state/account.go index 5460d7bf1..da2bb34f3 100644 --- a/plugin/dapp/evm/executor/vm/state/account.go +++ b/plugin/dapp/evm/executor/vm/state/account.go @@ -292,7 +292,17 @@ func (ca *ContractAccount) HasSuicided() bool { // Empty 是否为空对象 func (ca *ContractAccount) Empty() bool { - return ca.Data.GetCodeHash() == nil || len(ca.Data.GetCodeHash()) == 0 + if ca.Data.GetCodeHash() != nil && len(ca.Data.GetCodeHash()) != 0 { + return false + } + // fork 开启后采用以太坊语义:有 nonce 的账户非空 + if ca.State.GetNonce() != 0 && ca.mdb != nil && ca.mdb.api != nil { + cfg := ca.mdb.api.GetConfig() + if cfg != nil && cfg.IsDappFork(ca.mdb.blockHeight, evmtypes.ExecutorName, evmtypes.ForkEVMContractCreateV2) { + return false + } + } + return true } // SetNonce 设置nonce值 diff --git a/plugin/dapp/evm/executor/vm/state/snapshot.go b/plugin/dapp/evm/executor/vm/state/snapshot.go index 8151837a4..c944c8e79 100644 --- a/plugin/dapp/evm/executor/vm/state/snapshot.go +++ b/plugin/dapp/evm/executor/vm/state/snapshot.go @@ -250,11 +250,16 @@ func (ch nonceChange) revert(mdb *MemoryStateDB) error { } func (ch nonceChange) getData(mdb *MemoryStateDB) []*types.KeyValue { - // nonce目前没有应用场景,而且每次调用都会变更,暂时先不写到状态数据库中 - //acc := mdb.accounts[ch.account] - //if acc != nil { - // return acc.GetStateKV() - //} + // nonce 只在 ForkEVMContractCreateV2 之后参与状态,必须与调用侧同样门控: + // 该变更会写入合约状态 KV,未门控时任何新增的 SetNonce 都会改变历史区块的哈希。 + if mdb.api == nil || mdb.api.GetConfig() == nil || + !mdb.api.GetConfig().IsDappFork(mdb.blockHeight, evmtypes.ExecutorName, evmtypes.ForkEVMContractCreateV2) { + return nil + } + acc := mdb.accounts[ch.account] + if acc != nil { + return acc.GetStateKV() + } return nil } diff --git a/plugin/dapp/evm/types/evm.go b/plugin/dapp/evm/types/evm.go index 8b43b653c..b6a5477de 100644 --- a/plugin/dapp/evm/types/evm.go +++ b/plugin/dapp/evm/types/evm.go @@ -55,6 +55,10 @@ func InitFork(cfg *types.Chain33Config) { // not configure the key, rewriting historical execution. A chain that wants the fix sets // a concrete height under [fork.sub.evm]; initForkConfig overrides this default. cfg.RegisterDappFork(ExecutorName, ForkEVMTransferRevert, types.MaxHeight) + // 内部CREATE/CREATE2采用以太坊地址派生(caller nonce)、nonce递增与地址碰撞检查。 + // 与 ForkEVMTransferRevert 同理:默认关闭(MaxHeight),需要该修复的链在 + // [fork.sub.evm] 里显式配置高度,避免对存量链追溯生效。 + cfg.RegisterDappFork(ExecutorName, ForkEVMContractCreateV2, types.MaxHeight) } // InitExecutor ... diff --git a/plugin/dapp/evm/types/types.go b/plugin/dapp/evm/types/types.go index d831621b8..53c864c37 100644 --- a/plugin/dapp/evm/types/types.go +++ b/plugin/dapp/evm/types/types.go @@ -66,6 +66,10 @@ const ( // 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" + // ForkEVMContractCreateV2 内部CREATE/CREATE2采用以太坊地址派生(caller nonce)、nonce递增与地址碰撞检查。 + // fork 前创建的合约没有 nonce 记录(视为0),其 fork 后的首次 CREATE 从 nonce 0 派生, + // 与以太坊(合约 nonce 从1开始)不完全一致;fork 后创建的合约派生的子合约地址与以太坊完全相同。 + ForkEVMContractCreateV2 = "ForkEVMContractCreateV2" ) var ( From 4a89f1cab9b666ebbbea39b592c3c521e093f562 Mon Sep 17 00:00:00 2001 From: bysomeone <11565373+bysomeone@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:36:29 +0800 Subject: [PATCH 2/5] test(evm): drop redundant uint64 conversion flagged by unconvert Co-authored-by: Claude --- plugin/dapp/evm/executor/create_compat_repro_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/dapp/evm/executor/create_compat_repro_test.go b/plugin/dapp/evm/executor/create_compat_repro_test.go index 3f756246f..ebaa76768 100644 --- a/plugin/dapp/evm/executor/create_compat_repro_test.go +++ b/plugin/dapp/evm/executor/create_compat_repro_test.go @@ -142,7 +142,7 @@ func readContractNonce(t *testing.T, exec *EVMExecutor, addr string) uint64 { if err := ctypes.Decode(raw, &st); err != nil { t.Fatalf("decode contract state: %v", err) } - return uint64(st.GetNonce()) + return st.GetNonce() } // ethCreateAddress computes the ethereum CREATE address for a chain33-format From 11a05813bcc6baa8e6f39b76492f66c98758eb01 Mon Sep 17 00:00:00 2001 From: bysomeone <11565373+bysomeone@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:48:08 +0800 Subject: [PATCH 3/5] fix(ci): assert the coins delta after the evm2 ticket withdraw withdrawTicketBalance compared the post-withdraw coins balance with the pre-withdraw ticket balance. The ticket balance also grows with mining rewards, so the check only held when it happened to be exactly 100000 and failed intermittently otherwise (observed 100005 vs 100001 on the same head). Record the coins balance before the withdraw and assert the increase is the withdrawn amount, allowing a small tolerance for fees and rounding. Co-authored-by: Claude --- plugin/dapp/evm/cmd/ci2/docker-compose.sh | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugin/dapp/evm/cmd/ci2/docker-compose.sh b/plugin/dapp/evm/cmd/ci2/docker-compose.sh index 471b24242..3b57096ed 100755 --- a/plugin/dapp/evm/cmd/ci2/docker-compose.sh +++ b/plugin/dapp/evm/cmd/ci2/docker-compose.sh @@ -394,6 +394,10 @@ function withdrawTicketBalance(){ local balance=$(echo "${result}" | jq -r ".balance") tbalance=$(printf "%.0f\n" $balance) echo "ticket balance:${tbalance},addr:${m4normalAddr},timestamp:$(date +"%s")" + # The ticket balance also grows with mining rewards, so it cannot be used as + # the expected coin balance after the withdraw. Compare the coins delta. + local coinsBefore=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -r ".balance") + echo "coins balance before withdraw:${coinsBefore}" local withdrawTx=$( ${CLI} coins withdraw -e ticket -a 100000 -n "take test money") echo "withdraw rawTx:${withdrawTx}" @@ -409,14 +413,14 @@ function withdrawTicketBalance(){ fi echo "withdraw hash:${hash}" chain33_QueryTx ${hash} ${MCli} - #check coins balance - local result=$(${CLI} account balance -a "${m4normalAddr}" -e coins) - local balance=$(echo "${result}" | jq -r ".balance") - echo "coldminer addr coins balance:${balance}" - local checkBalance=$(printf "%.0f\n" $balance) - echo "checkBalance: $(("${checkBalance}")) " - if [ $((checkBalance)) -lt $((tbalance)) ]; then - exit 1 + #check coins balance: the withdraw moves exactly 100000 into the coins account, + #minus the transaction fee; allow a small tolerance for fees and rounding. + local coinsAfter=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -r ".balance") + echo "coldminer addr coins balance after withdraw:${coinsAfter}" + if ! awk -v before="${coinsBefore}" -v after="${coinsAfter}" \ + 'BEGIN { exit !((after - before) >= 99999.9) }'; then + echo "withdraw balance check failed: before=${coinsBefore} after=${coinsAfter}" + exit 1 fi } From 6caa95d0749c7a3563747fa70c4553ffe3a1a119 Mon Sep 17 00:00:00 2001 From: bysomeone <11565373+bysomeone@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:16:02 +0800 Subject: [PATCH 4/5] fix(ci): widen the evm2 withdraw balance check boundary The withdraw moves 100000 coins minus a ~0.1 coin fee, so the expected increase is exactly 99999.9 and the previous 99999.9 lower bound sat on the floating-point boundary: the same balances can compare either side of it depending on their magnitude and on the awk implementation. Use a 99999 lower bound instead, and make jq fail on a missing balance rather than silently comparing a null as zero. Co-authored-by: Claude --- plugin/dapp/evm/cmd/ci2/docker-compose.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugin/dapp/evm/cmd/ci2/docker-compose.sh b/plugin/dapp/evm/cmd/ci2/docker-compose.sh index 3b57096ed..95812eaa7 100755 --- a/plugin/dapp/evm/cmd/ci2/docker-compose.sh +++ b/plugin/dapp/evm/cmd/ci2/docker-compose.sh @@ -396,7 +396,7 @@ function withdrawTicketBalance(){ echo "ticket balance:${tbalance},addr:${m4normalAddr},timestamp:$(date +"%s")" # The ticket balance also grows with mining rewards, so it cannot be used as # the expected coin balance after the withdraw. Compare the coins delta. - local coinsBefore=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -r ".balance") + local coinsBefore=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -er ".balance") echo "coins balance before withdraw:${coinsBefore}" local withdrawTx=$( ${CLI} coins withdraw -e ticket -a 100000 -n "take test money") @@ -413,12 +413,13 @@ function withdrawTicketBalance(){ fi echo "withdraw hash:${hash}" chain33_QueryTx ${hash} ${MCli} - #check coins balance: the withdraw moves exactly 100000 into the coins account, - #minus the transaction fee; allow a small tolerance for fees and rounding. - local coinsAfter=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -r ".balance") + #check coins balance: the withdraw moves 100000 into the coins account minus the + #transaction fee (~0.1), so require a lower bound with margin instead of an exact + #value: the boundary is sensitive to floating-point rounding of the balances. + local coinsAfter=$(${CLI} account balance -a "${m4normalAddr}" -e coins | jq -er ".balance") echo "coldminer addr coins balance after withdraw:${coinsAfter}" if ! awk -v before="${coinsBefore}" -v after="${coinsAfter}" \ - 'BEGIN { exit !((after - before) >= 99999.9) }'; then + 'BEGIN { exit !((after - before) >= 99999) }'; then echo "withdraw balance check failed: before=${coinsBefore} after=${coinsAfter}" exit 1 fi From 79a8405ff6b84006e4a14f1845738c8cfd017ade Mon Sep 17 00:00:00 2001 From: bysomeone <11565373+bysomeone@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:16:02 +0800 Subject: [PATCH 5/5] test(evm): cover the failed-create value rollback with transfer revert ForkEVMContractCreateV2 and ForkEVMTransferRevert are independent forks, but the full Ethereum semantics need both: this fork keeps the nonce of a failed CREATE, and the transfer-revert fork rolls back the value attached to it. Add a test that enables both, and document the pairing next to the fork. Co-authored-by: Claude --- .../evm/executor/create_compat_repro_test.go | 39 +++++++++++++++++++ plugin/dapp/evm/types/types.go | 2 + 2 files changed, 41 insertions(+) diff --git a/plugin/dapp/evm/executor/create_compat_repro_test.go b/plugin/dapp/evm/executor/create_compat_repro_test.go index ebaa76768..703f35a17 100644 --- a/plugin/dapp/evm/executor/create_compat_repro_test.go +++ b/plugin/dapp/evm/executor/create_compat_repro_test.go @@ -381,6 +381,45 @@ func TestCreateFailureStillIncrementsNonce(t *testing.T) { } } +// factory that CREATEs one child with value 1 whose init code reverts. +// +// PUSH5 0x60006000fd; PUSH1 0; MSTORE +// PUSH1 5; PUSH1 27; PUSH1 1; CREATE; STOP +const factoryCreateWithValueRuntime = "6460006000fd" + "600052" + "6005601b6001f0" + "00" + +// deploy wrapper: CODECOPY runtime (17 bytes at offset 12) then RETURN it +const factoryCreateWithValueBytecode = "6011600c60003960116000f3" + factoryCreateWithValueRuntime + +// A failed CREATE consumes a nonce; with ForkEVMTransferRevert enabled the value +// attached to the CREATE is rolled back as well. +func TestFailedCreateRollsBackValueWithTransferRevert(t *testing.T) { + cfg := newTestConfig(t) + enableContractCreateV2(cfg) + cfg.SetDappFork("evm", evmtypes.ForkEVMTransferRevert, 0) + exec := newTestExecutor(t, cfg, 0) + + factoryAddr := deployContract(t, cfg, exec, factoryCreateWithValueBytecode) + fundAddr(t, exec, factoryAddr, 1000) + before := exec.mStateDB.GetBalance(factoryAddr) + + r, _, err := callRet(t, cfg, exec, factoryAddr) + if err != nil || r == nil || r.Ty != ctypes.ExecOk { + t.Fatalf("call factory: err=%v ty=%d", err, r.GetTy()) + } + applyReceiptKV(t, exec, r) + + if after := exec.mStateDB.GetBalance(factoryAddr); after != before { + t.Fatalf("factory balance %d -> %d, value was not rolled back", before, after) + } + if nonce := readContractNonce(t, exec, factoryAddr); nonce != 2 { + t.Fatalf("factory nonce after failed CREATE = %d, want 2", nonce) + } + child := ethCreateAddress(t, factoryAddr, 1) + if bal := exec.mStateDB.GetBalance(vcomm.BytesToAddress(child.Bytes()).String()); bal != 0 { + t.Fatalf("failed create address holds %d coins", bal) + } +} + // receiptRet extracts the EVM return data from a receipt. func receiptRet(t *testing.T, r *ctypes.Receipt) (*evmtypes.ReceiptEVMContract, []byte) { t.Helper() diff --git a/plugin/dapp/evm/types/types.go b/plugin/dapp/evm/types/types.go index 53c864c37..c2f12a609 100644 --- a/plugin/dapp/evm/types/types.go +++ b/plugin/dapp/evm/types/types.go @@ -69,6 +69,8 @@ const ( // ForkEVMContractCreateV2 内部CREATE/CREATE2采用以太坊地址派生(caller nonce)、nonce递增与地址碰撞检查。 // fork 前创建的合约没有 nonce 记录(视为0),其 fork 后的首次 CREATE 从 nonce 0 派生, // 与以太坊(合约 nonce 从1开始)不完全一致;fork 后创建的合约派生的子合约地址与以太坊完全相同。 + // 与 ForkEVMTransferRevert 同高开启才是完整的以太坊语义:只开本 fork 时, + // 创建失败会保留已递增的 nonce,但不会回滚随 CREATE 转账的 value。 ForkEVMContractCreateV2 = "ForkEVMContractCreateV2" )