mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 11:20:45 +00:00
core: use emptiness as pre-charge condition
This commit is contained in:
parent
e7ad8707c7
commit
3fa4bb4c40
4 changed files with 168 additions and 8 deletions
|
|
@ -73,6 +73,40 @@ func mkState(alloc types.GenesisAlloc) *state.StateDB {
|
||||||
return sdb
|
return sdb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mkCommittedState is mkState with the allocation committed to disk and
|
||||||
|
// reloaded. EIP-161-empty accounts carrying only storage do not survive an
|
||||||
|
// in-memory Finalise; committing without empty-account deletion reproduces
|
||||||
|
// the synthesized prestate an EIP-7610 fixture would load from disk.
|
||||||
|
func mkCommittedState(t *testing.T, alloc types.GenesisAlloc) *state.StateDB {
|
||||||
|
t.Helper()
|
||||||
|
db := state.NewDatabaseForTesting()
|
||||||
|
sdb, _ := state.New(types.EmptyRootHash, db)
|
||||||
|
for addr, acc := range alloc {
|
||||||
|
sdb.CreateAccount(addr)
|
||||||
|
if acc.Balance != nil {
|
||||||
|
sdb.AddBalance(addr, uint256.MustFromBig(acc.Balance), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
if acc.Nonce != 0 {
|
||||||
|
sdb.SetNonce(addr, acc.Nonce, tracing.NonceChangeGenesis)
|
||||||
|
}
|
||||||
|
if len(acc.Code) != 0 {
|
||||||
|
sdb.SetCode(addr, acc.Code, tracing.CodeChangeUnspecified)
|
||||||
|
}
|
||||||
|
for k, v := range acc.Storage {
|
||||||
|
sdb.SetState(addr, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := sdb.Commit(0, false, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit prestate: %v", err)
|
||||||
|
}
|
||||||
|
sdb, err = state.New(root, db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reopen prestate: %v", err)
|
||||||
|
}
|
||||||
|
return sdb
|
||||||
|
}
|
||||||
|
|
||||||
// amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
|
// amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
|
||||||
func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
|
func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
|
||||||
ctx := vm.BlockContext{
|
ctx := vm.BlockContext{
|
||||||
|
|
@ -105,7 +139,11 @@ func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*Executi
|
||||||
st := newStateTransition(evm, msg, gp)
|
st := newStateTransition(evm, msg, gp)
|
||||||
res, err := st.execute()
|
res, err := st.execute()
|
||||||
if err == nil && res != nil {
|
if err == nil && res != nil {
|
||||||
assertPoolSane(t, res, gp)
|
floor, ferr := FloorDataGas(rules8037, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList)
|
||||||
|
if ferr != nil {
|
||||||
|
t.Fatalf("floor data gas: %v", ferr)
|
||||||
|
}
|
||||||
|
assertPoolSane(t, res, gp, floor)
|
||||||
|
|
||||||
intrinsic, ierr := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037)
|
intrinsic, ierr := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037)
|
||||||
if ierr != nil {
|
if ierr != nil {
|
||||||
|
|
@ -142,9 +180,11 @@ func assertBudgetSane(t *testing.T, initial, got vm.GasBudget) {
|
||||||
// assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
|
// assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
|
||||||
//
|
//
|
||||||
// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas
|
// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas
|
||||||
// pre-refund: cumulativeRegular + cumulativeState <= res.MaxUsedGas (peak)
|
// regular: cumulativeRegular <= max(res.MaxUsedGas - cumulativeState, floor)
|
||||||
|
// (the calldata floor pads the regular dimension alone, so the
|
||||||
|
// dimension sum may exceed the pre-refund peak when it binds)
|
||||||
// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
|
// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
|
||||||
func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) {
|
func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool, floor uint64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if gp.cumulativeUsed != res.UsedGas {
|
if gp.cumulativeUsed != res.UsedGas {
|
||||||
t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas)
|
t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas)
|
||||||
|
|
@ -152,8 +192,12 @@ func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) {
|
||||||
if res.UsedGas > res.MaxUsedGas {
|
if res.UsedGas > res.MaxUsedGas {
|
||||||
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
|
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
|
||||||
}
|
}
|
||||||
if sum := gp.cumulativeRegular + gp.cumulativeState; sum > res.MaxUsedGas {
|
if gp.cumulativeState > res.MaxUsedGas {
|
||||||
t.Fatalf("regular+state %d exceeds peak %d", sum, res.MaxUsedGas)
|
t.Fatalf("state %d exceeds peak %d", gp.cumulativeState, res.MaxUsedGas)
|
||||||
|
}
|
||||||
|
if cap := max(res.MaxUsedGas-gp.cumulativeState, floor); gp.cumulativeRegular > cap {
|
||||||
|
t.Fatalf("regular %d exceeds pre-refund cap %d (peak %d, state %d, floor %d)",
|
||||||
|
gp.cumulativeRegular, cap, res.MaxUsedGas, gp.cumulativeState, floor)
|
||||||
}
|
}
|
||||||
if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
|
if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
|
||||||
t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState)
|
t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState)
|
||||||
|
|
@ -191,6 +235,7 @@ func createTx(nonce, gas uint64, initCode []byte) *types.Transaction {
|
||||||
var (
|
var (
|
||||||
deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
|
deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
|
||||||
revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
|
revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
|
||||||
|
haltI = []byte{0xfe, 0x00, 0x00, 0x00, 0x00} // init: INVALID, exceptional halt
|
||||||
)
|
)
|
||||||
|
|
||||||
// ===================== Top-level create transaction ======================
|
// ===================== Top-level create transaction ======================
|
||||||
|
|
@ -321,6 +366,118 @@ func TestCreate2TransientEmptyDestNoRefill(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Storage-only (EIP-7610-shaped) deployment destination ===========
|
||||||
|
//
|
||||||
|
// A destination carrying storage while having zero nonce, zero balance and
|
||||||
|
// empty code is EIP-161-empty, so the account-creation state gas is
|
||||||
|
// pre-charged in the parent frame.
|
||||||
|
|
||||||
|
// create2Orchestrator returns runtime code that CREATE2-deploys the given
|
||||||
|
// 5-byte init code with salt 0 and stores the result address at slot 0.
|
||||||
|
func create2Orchestrator(initCode []byte) []byte {
|
||||||
|
code := append([]byte{0x64}, initCode...) // PUSH5 init code
|
||||||
|
return append(code,
|
||||||
|
0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27)
|
||||||
|
0x60, 0x00, // salt = 0
|
||||||
|
0x60, 0x05, // size = 5
|
||||||
|
0x60, 0x1b, // offset = 27
|
||||||
|
0x60, 0x00, // endowment = 0
|
||||||
|
0xf5, // CREATE2
|
||||||
|
0x60, 0x00, 0x55, // SSTORE the result address at slot 0
|
||||||
|
0x00, // STOP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// storageOnlyAlloc allocates the orchestrator and its CREATE2 target, the
|
||||||
|
// latter carrying a single storage slot while remaining EIP-161-empty.
|
||||||
|
func storageOnlyAlloc(orchestrator common.Address, initCode []byte) (types.GenesisAlloc, common.Address) {
|
||||||
|
target := crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(initCode))
|
||||||
|
return types.GenesisAlloc{
|
||||||
|
orchestrator: {Code: create2Orchestrator(initCode)},
|
||||||
|
target: {Storage: map[common.Hash]common.Hash{{}: common.BigToHash(big.NewInt(1))}},
|
||||||
|
}, target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploying onto a storage-only destination pre-charges the account creation.
|
||||||
|
// Under the registry-based EIP-7610 check the creation proceeds, so the
|
||||||
|
// charge is consumed like any other creation.
|
||||||
|
func TestCreate2StorageOnlyDestCharged(t *testing.T) {
|
||||||
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000004")
|
||||||
|
alloc, target := storageOnlyAlloc(orchestrator, deploy3)
|
||||||
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
||||||
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 1_000_000, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if res.Failed() {
|
||||||
|
t.Fatalf("execution failed: %v", res.Err)
|
||||||
|
}
|
||||||
|
if code := sdb.GetCode(target); len(code) != 3 {
|
||||||
|
t.Fatalf("deployed code length = %d, want 3", len(code))
|
||||||
|
}
|
||||||
|
// The created account (charged, consumed), the orchestrator's result slot
|
||||||
|
// and the 3-byte code deposit.
|
||||||
|
want := newAccountState + newSlotState + uint64(3*params.CostPerStateByte)
|
||||||
|
if gp.cumulativeState != want {
|
||||||
|
t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the pre-charge succeeds and the create frame then fails, only the create
|
||||||
|
// frame halts: the forwarded regular gas is burnt, the account-creation
|
||||||
|
// charge is refilled, and the parent frame continues.
|
||||||
|
func TestCreate2StorageOnlyDestRefillOnFrameHalt(t *testing.T) {
|
||||||
|
const gas = 1_000_000
|
||||||
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000005")
|
||||||
|
alloc, target := storageOnlyAlloc(orchestrator, haltI)
|
||||||
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
||||||
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if res.Failed() {
|
||||||
|
t.Fatalf("parent frame must survive the create-frame halt: %v", res.Err)
|
||||||
|
}
|
||||||
|
// The CREATE2 pushed zero and nothing was deployed.
|
||||||
|
if flag := sdb.GetState(orchestrator, common.Hash{}); flag != (common.Hash{}) {
|
||||||
|
t.Fatalf("create result = %v, want 0", flag)
|
||||||
|
}
|
||||||
|
if code := sdb.GetCode(target); len(code) != 0 {
|
||||||
|
t.Fatalf("deployed code length = %d, want 0", len(code))
|
||||||
|
}
|
||||||
|
// The account-creation charge was refilled in full.
|
||||||
|
if gp.cumulativeState != 0 {
|
||||||
|
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
|
||||||
|
}
|
||||||
|
if res.UsedGas > gas-newAccountState {
|
||||||
|
t.Fatalf("used gas = %d, want at most %d (charge not refilled?)", res.UsedGas, gas-newAccountState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the remaining gas cannot cover the account-creation pre-charge, the
|
||||||
|
// parent frame itself halts with out-of-gas instead of the create frame.
|
||||||
|
func TestCreate2StorageOnlyDestPrechargeOOG(t *testing.T) {
|
||||||
|
// Enough for the CREATE2 constant cost, short of the 183,600 pre-charge.
|
||||||
|
const gas = 150_000
|
||||||
|
orchestrator := common.HexToAddress("0xc0de000000000000000000000000000000000006")
|
||||||
|
alloc, _ := storageOnlyAlloc(orchestrator, deploy3)
|
||||||
|
sdb := mkCommittedState(t, senderAlloc(alloc))
|
||||||
|
res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, gas, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !res.Failed() || !errors.Is(res.Err, vm.ErrOutOfGas) {
|
||||||
|
t.Fatalf("err = %v, want out of gas in the parent frame", res.Err)
|
||||||
|
}
|
||||||
|
if gp.cumulativeState != 0 {
|
||||||
|
t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState)
|
||||||
|
}
|
||||||
|
// The parent is the topmost frame, so its halt burns the whole gas limit.
|
||||||
|
if gp.cumulativeRegular != gas {
|
||||||
|
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, gas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== Transaction validation =========================
|
// ======================== Transaction validation =========================
|
||||||
|
|
||||||
// The regular dimension must have room for min(tx.gas, MaxTxGas).
|
// The regular dimension must have room for min(tx.gas, MaxTxGas).
|
||||||
|
|
|
||||||
|
|
@ -787,7 +787,7 @@ func (st *stateTransition) executeCreate(rules params.Rules, value *uint256.Int)
|
||||||
var chargedCreation bool
|
var chargedCreation bool
|
||||||
if rules.IsAmsterdam {
|
if rules.IsAmsterdam {
|
||||||
addr := crypto.CreateAddress(msg.From, st.state.GetNonce(msg.From))
|
addr := crypto.CreateAddress(msg.From, st.state.GetNonce(msg.From))
|
||||||
if !st.state.Exist(addr) {
|
if st.state.Empty(addr) {
|
||||||
if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) {
|
if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) {
|
||||||
// The nonce increment normally performed inside evm.Create
|
// The nonce increment normally performed inside evm.Create
|
||||||
// must still happen for the included transaction.
|
// must still happen for the included transaction.
|
||||||
|
|
|
||||||
|
|
@ -476,14 +476,13 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
|
||||||
// createFramePreCheck the precondition before executing the contract deployment,
|
// createFramePreCheck the precondition before executing the contract deployment,
|
||||||
// halts the create frame if fails with any check below.
|
// halts the create frame if fails with any check below.
|
||||||
func (evm *EVM) createFramePreCheck(caller common.Address, value *uint256.Int) error {
|
func (evm *EVM) createFramePreCheck(caller common.Address, value *uint256.Int) error {
|
||||||
var nonce uint64
|
|
||||||
if evm.depth > int(params.CallCreateDepth) {
|
if evm.depth > int(params.CallCreateDepth) {
|
||||||
return ErrDepth
|
return ErrDepth
|
||||||
}
|
}
|
||||||
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
|
||||||
return ErrInsufficientBalance
|
return ErrInsufficientBalance
|
||||||
}
|
}
|
||||||
nonce = evm.StateDB.GetNonce(caller)
|
nonce := evm.StateDB.GetNonce(caller)
|
||||||
if nonce+1 < nonce {
|
if nonce+1 < nonce {
|
||||||
return ErrNonceUintOverflow
|
return ErrNonceUintOverflow
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -671,6 +671,8 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
||||||
|
|
||||||
// Refill the account-creation charge if the create frame failed (reverted,
|
// Refill the account-creation charge if the create frame failed (reverted,
|
||||||
// halted exceptionally, or collided); a successful creation consumes it.
|
// halted exceptionally, or collided); a successful creation consumes it.
|
||||||
|
// This rule is only applied since the Amsterdam, therefore all non-nil vm
|
||||||
|
// error can be interpreted as deployment failure.
|
||||||
if creationCharged && suberr != nil {
|
if creationCharged && suberr != nil {
|
||||||
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
||||||
}
|
}
|
||||||
|
|
@ -716,6 +718,8 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
||||||
|
|
||||||
// Refill the account-creation charge if the create frame failed (reverted,
|
// Refill the account-creation charge if the create frame failed (reverted,
|
||||||
// halted exceptionally, or collided); a successful creation consumes it.
|
// halted exceptionally, or collided); a successful creation consumes it.
|
||||||
|
// This rule is only applied since the Amsterdam, therefore all non-nil vm
|
||||||
|
// error can be interpreted as deployment failure.
|
||||||
if creationCharged && suberr != nil {
|
if creationCharged && suberr != nil {
|
||||||
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue