diff --git a/chain33.fork.toml b/chain33.fork.toml index a855a35a6..335be59ca 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 +# Ethereum EVM compatibility set (opcodes/gas/CREATE); -1 keeps it off +ForkEVMCompat=-1 [fork.sub.evmxgo] diff --git a/chain33.para.toml b/chain33.para.toml index a6c8915cb..8d1756599 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 +# Ethereum EVM compatibility set (opcodes/gas/CREATE); -1 keeps it off +ForkEVMCompat=-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..0a7fc6be4 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 +# Ethereum EVM compatibility set (opcodes/gas/CREATE); -1 keeps it off +ForkEVMCompat=-1 [fork.sub.evmxgo] Enable=0 diff --git a/plugin/dapp/evm/cmd/ci2/docker-compose.sh b/plugin/dapp/evm/cmd/ci2/docker-compose.sh index 471b24242..95812eaa7 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 -er ".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,15 @@ 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 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) }'; then + echo "withdraw balance check failed: before=${coinsBefore} after=${coinsAfter}" + exit 1 fi } 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..fd068b01b --- /dev/null +++ b/plugin/dapp/evm/executor/create_compat_repro_test.go @@ -0,0 +1,437 @@ +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 +} + +// evmCompatForkHeight is above the execution height (0) used by the +// tests below, so setting it keeps the fork disabled. +const evmCompatForkHeight = 1000 + +func enableEVMCompat(cfg *ctypes.Chain33Config) { + cfg.SetDappFork("evm", evmtypes.ForkEVMCompat, 0) +} + +func disableEVMCompat(cfg *ctypes.Chain33Config) { + cfg.SetDappFork("evm", evmtypes.ForkEVMCompat, evmCompatForkHeight) +} + +// 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 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) + disableEVMCompat(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) + enableEVMCompat(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) + enableEVMCompat(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) + enableEVMCompat(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) + disableEVMCompat(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) + enableEVMCompat(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) + } +} + +// 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) + enableEVMCompat(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() + 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/opcodes_compat_integration_test.go b/plugin/dapp/evm/executor/opcodes_compat_integration_test.go new file mode 100644 index 000000000..27f0d91f8 --- /dev/null +++ b/plugin/dapp/evm/executor/opcodes_compat_integration_test.go @@ -0,0 +1,114 @@ +// 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 + +// Fork-gated coverage for the opcodes enabled by ForkEVMCompat: PUSH0 (EIP-3855) and +// BASEFEE (EIP-3198). + +import ( + "encoding/binary" + "testing" + + ctypes "github.com/33cn/chain33/types" + evmtypes "github.com/33cn/plugin/plugin/dapp/evm/types" + "github.com/stretchr/testify/require" +) + +// Runtime bytecode for the enabled-fork cases. Each program stores its result at memory +// offset 0 and returns one 32-byte word. + +// PUSH0; ISZERO; MSTORE(0, 1); RETURN(0, 32) +var push0Code = []byte{ + 0x5f, // PUSH0 + 0x15, // ISZERO -> 1 + 0x60, 0x00, // PUSH1 0 (memory offset) + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 (return size) + 0x60, 0x00, // PUSH1 0 (return offset) + 0xf3, // RETURN +} + +// BASEFEE; ISZERO; MSTORE(0, 1); RETURN(0, 32) +var baseFeeCode = []byte{ + 0x48, // BASEFEE + 0x15, // ISZERO -> 1 + 0x60, 0x00, // PUSH1 0 + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 + 0x60, 0x00, // PUSH1 0 + 0xf3, // RETURN +} + +func callReturnData(t *testing.T, receipt *ctypes.Receipt) []byte { + t.Helper() + for _, log := range receipt.GetLogs() { + if log.GetTy() != evmtypes.TyLogCallContract { + continue + } + var contract evmtypes.ReceiptEVMContract + require.NoError(t, ctypes.Decode(log.GetLog(), &contract)) + return contract.Ret + } + t.Fatal("receipt has no TyLogCallContract log") + return nil +} + +func callContract(t *testing.T, cfg *ctypes.Chain33Config, exec *EVMExecutor, contractAddr string, input []byte) *ctypes.Receipt { + t.Helper() + receipt, err := exec.Exec(makeCallTx(cfg, roleLegitUser, contractAddr, input, 0), 0) + require.NoError(t, err) + require.Equal(t, int32(ctypes.ExecOk), receipt.GetTy()) + applyReceiptKV(t, exec, receipt) + return receipt +} + +func uint64Word(v uint64) []byte { + word := make([]byte, 32) + binary.BigEndian.PutUint64(word[24:], v) + return word +} + +func TestEVMCompatOpcodesDisabled(t *testing.T) { + restoreBtcAddressDriver(t) + cfg := newTestConfig(t) + disableEVMCompat(cfg) + exec := newTestExecutor(t, cfg, 0) + + // The new bytes must stay invalid opcodes when the fork is off. Only the byte that is + // actually executed matters, so a single unknown opcode followed by STOP is enough. + for _, tc := range []struct { + name string + op byte + }{ + {"PUSH0", 0x5f}, + {"BASEFEE", 0x48}, + } { + t.Run(tc.name, func(t *testing.T) { + contractAddr := deployCode(t, cfg, exec, []byte{tc.op, 0x00}) + _, err := exec.Exec(makeCallTx(cfg, roleLegitUser, contractAddr, nil, 0), 0) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid opcode") + }) + } +} + +func TestEVMCompatOpcodesEnabled(t *testing.T) { + restoreBtcAddressDriver(t) + cfg := newTestConfig(t) + enableEVMCompat(cfg) + exec := newTestExecutor(t, cfg, 0) + + t.Run("PUSH0", func(t *testing.T) { + contractAddr := deployCode(t, cfg, exec, push0Code) + ret := callReturnData(t, callContract(t, cfg, exec, contractAddr, nil)) + require.Equal(t, uint64Word(1), ret, "PUSH0 must push zero, so ISZERO yields one") + }) + + t.Run("BASEFEE", func(t *testing.T) { + contractAddr := deployCode(t, cfg, exec, baseFeeCode) + ret := callReturnData(t, callContract(t, cfg, exec, contractAddr, nil)) + require.Equal(t, uint64Word(1), ret, "BASEFEE returns zero because chain33 has no base fee") + }) +} 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..e5088db56 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 } +// IsEVMCompat 以太坊 EVM 兼容性(指令/计价/CREATE)是否开启(fork 门控) +func (evm *EVM) IsEVMCompat() bool { + return evm.cfg != nil && evm.BlockNumber != nil && + evm.cfg.IsDappFork(evm.BlockNumber.Int64(), evmtypes.ExecutorName, evmtypes.ForkEVMCompat) +} + // 封装合约的各种调用逻辑中通用的预检查逻辑 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.IsEVMCompat() { + 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.IsEVMCompat() { + // 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.IsEVMCompat())) { 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..fb0fc9bb7 100644 --- a/plugin/dapp/evm/executor/vm/runtime/instructions.go +++ b/plugin/dapp/evm/executor/vm/runtime/instructions.go @@ -532,6 +532,18 @@ func opMstore8(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { return nil, nil } +// PUSH0 指令,将零压入栈(EIP-3855) +func opPush0(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { + callContext.stack.push(new(uint256.Int)) + return nil, nil +} + +// BASEFEE 指令,chain33 没有 base fee 机制,固定返回 0 +func opBaseFee(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { + callContext.stack.push(new(uint256.Int)) + return nil, nil +} + // 加载合约状态数据 func opSload(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { loc := callContext.stack.peek() @@ -642,10 +654,21 @@ func opCreate(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { gas = callContext.contract.Gas ) + if evm.IsEVMCompat() { + // 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.IsEVMCompat() { + // 以太坊语义:地址由调用者地址与调用者 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 +683,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.IsEVMCompat()) { log15.Error("evm contract opCreate instruction error,value", suberr, value) stackvalue.Clear() } else { @@ -688,7 +712,12 @@ func opCreate2(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { ) // Apply EIP150 - gas = gas / 2 + if evm.IsEVMCompat() { + // 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 +726,16 @@ func opCreate2(pc *uint64, evm *EVM, callContext *callCtx) ([]byte, error) { if !endowment.IsZero() { bigEndowment = endowment.ToBig() } + if evm.IsEVMCompat() && !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 +752,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/runtime/interpreter.go b/plugin/dapp/evm/executor/vm/runtime/interpreter.go index 5c6963e0a..3b284676d 100644 --- a/plugin/dapp/evm/executor/vm/runtime/interpreter.go +++ b/plugin/dapp/evm/executor/vm/runtime/interpreter.go @@ -56,6 +56,9 @@ func NewInterpreter(evm *EVM, cfg Config) *Interpreter { //这里需要替换为最新得指令集 cfg.JumpTable = berlinInstructionSet } + if evm.IsEVMCompat() { + cfg.JumpTable = evmCompatInstructionSet + } } return &Interpreter{ diff --git a/plugin/dapp/evm/executor/vm/runtime/jump_table.go b/plugin/dapp/evm/executor/vm/runtime/jump_table.go index 0fb7c0a8e..ce5fe795c 100644 --- a/plugin/dapp/evm/executor/vm/runtime/jump_table.go +++ b/plugin/dapp/evm/executor/vm/runtime/jump_table.go @@ -52,11 +52,33 @@ var ( constantinopleInstructionSet = newConstantinopleInstructionSet() istanbulInstructionSet = newIstanbulInstructionSet() berlinInstructionSet = newBerlinInstructionSet() + evmCompatInstructionSet = newEVMCompatInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. type JumpTable [256]*operation +// newEVMCompatInstructionSet returns the Berlin set extended with the opcodes enabled by +// ForkEVMCompat: PUSH0 (EIP-3855) and BASEFEE (EIP-3198). +func newEVMCompatInstructionSet() JumpTable { + instructionSet := newBerlinInstructionSet() + + instructionSet[PUSH0] = &operation{ + execute: opPush0, + constantGas: GasQuickStep, + minStack: minStack(0, 1), + maxStack: maxStack(0, 1), + } + instructionSet[BASEFEE] = &operation{ + execute: opBaseFee, + constantGas: GasQuickStep, + minStack: minStack(0, 1), + maxStack: maxStack(0, 1), + } + + return instructionSet +} + // newBerlinInstructionSet returns the frontier, homestead, byzantium, // contantinople, istanbul, petersburg and berlin instructions. func newBerlinInstructionSet() JumpTable { diff --git a/plugin/dapp/evm/executor/vm/runtime/opcodes.go b/plugin/dapp/evm/executor/vm/runtime/opcodes.go index fd6377e78..29fcf410d 100644 --- a/plugin/dapp/evm/executor/vm/runtime/opcodes.go +++ b/plugin/dapp/evm/executor/vm/runtime/opcodes.go @@ -92,6 +92,7 @@ func initMap() { GASLIMIT: "GASLIMIT", CHAINID: "CHAINID", SELFBALANCE: "SELFBALANCE", + BASEFEE: "BASEFEE", // 0x50 range - 'storage' and execution POP: "POP", @@ -114,6 +115,7 @@ func initMap() { RETURNSUB: "RETURNSUB", // 0x60 range - push + PUSH0: "PUSH0", PUSH1: "PUSH1", PUSH2: "PUSH2", PUSH3: "PUSH3", @@ -326,6 +328,8 @@ const ( CHAINID OpCode = 0x46 // SELFBALANCE op SELFBALANCE OpCode = 0x47 + // BASEFEE op + BASEFEE OpCode = 0x48 ) const ( @@ -361,6 +365,11 @@ const ( JUMPSUB ) +const ( + // PUSH0 op,将零压入栈(EIP-3855) + PUSH0 OpCode = 0x5f +) + const ( // PUSH1 0x60 栈操作 PUSH1 OpCode = 0x60 + iota diff --git a/plugin/dapp/evm/executor/vm/state/account.go b/plugin/dapp/evm/executor/vm/state/account.go index 5460d7bf1..2bd32b203 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.ForkEVMCompat) { + 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..972bca739 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 只在 ForkEVMCompat 之后参与状态,必须与调用侧同样门控: + // 该变更会写入合约状态 KV,未门控时任何新增的 SetNonce 都会改变历史区块的哈希。 + if mdb.api == nil || mdb.api.GetConfig() == nil || + !mdb.api.GetConfig().IsDappFork(mdb.blockHeight, evmtypes.ExecutorName, evmtypes.ForkEVMCompat) { + 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..783ffd20a 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, ForkEVMCompat, types.MaxHeight) } // InitExecutor ... diff --git a/plugin/dapp/evm/types/types.go b/plugin/dapp/evm/types/types.go index d831621b8..5499ba655 100644 --- a/plugin/dapp/evm/types/types.go +++ b/plugin/dapp/evm/types/types.go @@ -66,6 +66,16 @@ 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" + // ForkEVMCompat aligns the interpreter with Ethereum behind a single activation height: + // - internal CREATE/CREATE2: address from the executing account and its nonce, nonce + // increment persisted, address collision check, EIP-150 63/64 gas; + // - opcodes PUSH0 and BASEFEE. + // Contracts created before the fork have no nonce record (treated as 0), so their first + // post-fork internal CREATE does not match Ethereum exactly; contracts created after the + // fork match exactly. + // Enable it at the same height as ForkEVMTransferRevert: the latter rolls back the value + // attached to a failed CREATE, which this fork alone does not do. + ForkEVMCompat = "ForkEVMCompat" ) var (