Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd, core, params, trie: Add verkle access witness #29338

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

gballet
Copy link
Member

@gballet gballet commented Mar 25, 2024

This is a rewrite of #29234 that uses the simplified approach suggested by @holiman

@gballet gballet force-pushed the rewritten-eip4762-into-master branch 2 times, most recently from 8373191 to d4019ed Compare March 27, 2024 08:36
@@ -78,7 +78,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
signer = types.MakeSigner(p.config, header.Number, header.Time)
)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb)
ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb, header.Number, header.Time)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vmenv actually has an unexported rules field that I would like to be able to access instead of having to pass extra headers. Would that be an option?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah could either make the chainRules accessible, or use a vmenv.ChainConfig().Rules(header.Number, header.Time) new instance

core/state/access_witness.go Outdated Show resolved Hide resolved
Comment on lines 94 to 98
for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ {
gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This little thing iterates form VersionLeafKey to CodeSizeLeafKey, it's not apparent to me why this does so.In TouchAndChageMessageCall, we only use those two, not the things in between. Please add some comments

core/state/access_witness.go Outdated Show resolved Hide resolved
@@ -360,6 +360,10 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
amount := new(uint256.Int).SetUint64(w.Amount)
amount = amount.Mul(amount, uint256.NewInt(params.GWei))
state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)

if chain.Config().IsEIP4762(header.Number, header.Time) {
state.Witness().TouchFullAccount(w.Address[:], true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHy, nonce is not needed here, it's just a balance increase?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have the same feeling.

As account fields are stored in different position in leafNode. The state access granularity is actually a field. It brings lots of complexity for gas metering.

Nothing to against the implementation, just want to mention that gas metering could be wrong very easy.

return gas
}

func (aw *AccessWitness) TouchTxOriginAndComputeGas(originAddr []byte) uint64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these implicitly ComputeGas, no? But some have it in the name and some don't, so just drop it imo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed some don't, I changed their names and the API to reflect that.

@@ -382,8 +390,23 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
uint64CodeOffset = math.MaxUint64
}
addr := common.Address(a.Bytes20())
codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
if interpreter.evm.chainRules.IsEIP4762 {
Copy link
Contributor

@holiman holiman Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if you make opExtCodeCopyVerkle and put in eips. That way we know that the verkle-things do not interfere with pre-verkle, not even adding an extra if-clause check. Same for other changed ops in this file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not great, because if any operation is changed between the time I copy the code and the time the Osaka fork is activated, there is a regression risk. I would rather try to move it to a dynamic gas function if I can.

Copy link
Member Author

@gballet gballet Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and who knows how these functions, especially the introspecting ones, are going to change when/if EOF is added to Prague and the spec finalized.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, I guess I could just call the previous handler and just add the extra gas costs, to avoid regressions.

core/vm/evm.go Outdated
} else {
err = ErrCodeStoreOutOfGas
// Contract creation completed, touch the missing fields in the contract
if !contract.UseGas(evm.Accesses.TouchFullAccount(address.Bytes()[:], true), evm.Config.Tracer, tracing.GasChangeUnspecified) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably needs a new GasChange reason here

core/state/access_witness.go Outdated Show resolved Hide resolved
@@ -360,6 +360,10 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
amount := new(uint256.Int).SetUint64(w.Amount)
amount = amount.Mul(amount, uint256.NewInt(params.GWei))
state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal)

if chain.Config().IsEIP4762(header.Number, header.Time) {
state.Witness().TouchFullAccount(w.Address[:], true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have the same feeling.

As account fields are stored in different position in leafNode. The state access granularity is actually a field. It brings lots of complexity for gas metering.

Nothing to against the implementation, just want to mention that gas metering could be wrong very easy.

@@ -102,7 +102,7 @@ func (b *BlockGen) SetParentBeaconRoot(root common.Hash) {
blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
vmenv = vm.NewEVM(blockContext, vm.TxContext{}, b.statedb, b.cm.config, vm.Config{})
)
ProcessBeaconBlockRoot(root, vmenv, b.statedb)
ProcessBeaconBlockRoot(root, vmenv, b.statedb, b.Number(), b.Timestamp())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace it with github.com/ethereum/go-verkle

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what you mean here, this is what is already happening line 35? 🤔

core/state/access_witness.go Outdated Show resolved Hide resolved
core/state/statedb.go Show resolved Hide resolved
statedb.AddAddressToAccessList(params.BeaconRootsAddress)
txctx := NewEVMTxContext(msg)
vmenv.Reset(txctx, statedb)
if vmenv.ChainConfig().Rules(num, true, time).IsEIP2929 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be IsEIP4762?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, access lists are a concept of 2929 that is deactivated with 4762

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any particular reason why you are placing all these calls to AccessList within isEIP2929 clauses?
I mean, you could just let the call happen, couldn't you?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but it's not supposed to happen (even if it has no obvious impact afaict)


// add the coinbase to the witness iff the fee is greater than 0
if rules.IsEIP4762 && fee.Sign() != 0 {
st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we only mutate the balance field?

core/vm/evm.go Show resolved Hide resolved
@gballet gballet force-pushed the rewritten-eip4762-into-master branch from 2044918 to 7de652d Compare April 24, 2024 19:58
@gballet gballet requested a review from s1na as a code owner April 24, 2024 19:58
@@ -185,7 +190,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo

// ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root
// contract. This method is exported to be used in tests.
func ProcessBeaconBlockRoot(beaconRoot common.Hash, vmenv *vm.EVM, statedb *state.StateDB) {
func ProcessBeaconBlockRoot(beaconRoot common.Hash, vmenv *vm.EVM, statedb *state.StateDB, num *big.Int, time uint64) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this could just take the params.Rules, couldn't it?

Comment on lines 427 to 428
if msg.To != nil {
st.evm.Accesses.AddTxDestination(targetAddr.Bytes(), msg.Value.Sign() != 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? We're going to Call into that address, shouldn't that add it later on?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because when it comes to witnesses, these costs are paid for the 21000 gas. All other calls are paid for. If this is moved to the Call, I have to add an if somewhere to distinguish between the top-level call, and any other, nested call.

params/config.go Outdated Show resolved Hide resolved
@gballet gballet force-pushed the rewritten-eip4762-into-master branch from 788d16c to d4f2768 Compare April 26, 2024 13:44
core/vm/evm.go Outdated Show resolved Hide resolved
core/vm/evm.go Outdated Show resolved Hide resolved
Comment on lines 275 to 293
func (aw *AccessEvents) VersionGas(addr []byte, isWrite bool) uint64 {
return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite)
}

func (aw *AccessEvents) BalanceGas(addr []byte, isWrite bool) uint64 {
return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite)
}

func (aw *AccessEvents) NonceGas(addr []byte, isWrite bool) uint64 {
return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite)
}

func (aw *AccessEvents) CodeSizeGas(addr []byte, isWrite bool) uint64 {
return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite)
}

func (aw *AccessEvents) CodeHashGas(addr []byte, isWrite bool) uint64 {
return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these needs docstrings. What assumptions are used here, regarding the caller? For example, is it "ok" if the caller first invokes BalanceGas(.., false) then BalanceGas(.., true)?
What are the side-effects?

For example. in beacon consensus engine, this method is invoked:

state.AccessEvents().BalanceGas(w.Address[:], true)

It's not clear to me why. What are the side-effects of that invocation? Because clearly we're not concerned about the gas cost.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these needs docstrings. What assumptions are used here, regarding the caller? For example, is it "ok" if the caller first invokes BalanceGas(.., false) then BalanceGas(.., true)? What are the side-effects?

The previous version of this PR was more explicit, it defined ReadWriteMode and so this is what things are: if a location is accessed in "read" mode, the price is cheaper than if they are accessed in "write" mode.

core/state/access_events.go Show resolved Hide resolved
naw := &AccessEvents{
branches: make(map[branchAccessKey]mode),
chunks: make(map[chunkAccessKey]mode),
pointCache: aw.pointCache,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa, you just copy the pointcache so the original and the copy now uses the same instance? Is that safe?

core/state/access_events.go Outdated Show resolved Hide resolved
core/state/access_events.go Outdated Show resolved Hide resolved
@@ -120,6 +120,7 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo
}
// Create a new context to be used in the EVM environment.
txContext := NewEVMTxContext(msg)
txContext.AccessEvents = statedb.NewAccessEvents()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this only be set if verkle is active?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, inside of evm.Reset, the txContext.AccessEvents will also be initialized. Is it a bit redundant? Or some mistakes here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not a mistake, one needs to start with an empty access event list when starting a conversion.

core/state_transition.go Outdated Show resolved Hide resolved
core/state_transition.go Outdated Show resolved Hide resolved
core/state/access_events.go Outdated Show resolved Hide resolved
core/vm/operations_verkle.go Outdated Show resolved Hide resolved
core/vm/operations_verkle.go Show resolved Hide resolved
core/vm/operations_verkle.go Outdated Show resolved Hide resolved
core/vm/operations_verkle.go Outdated Show resolved Hide resolved
core/vm/operations_verkle.go Outdated Show resolved Hide resolved
core/state_processor.go Outdated Show resolved Hide resolved
@@ -120,6 +120,7 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo
}
// Create a new context to be used in the EVM environment.
txContext := NewEVMTxContext(msg)
txContext.AccessEvents = statedb.NewAccessEvents()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, inside of evm.Reset, the txContext.AccessEvents will also be initialized. Is it a bit redundant? Or some mistakes here?

core/state_transition.go Outdated Show resolved Hide resolved
core/vm/evm.go Outdated Show resolved Hide resolved
gballet and others added 6 commits April 29, 2024 16:18
Co-authored-by: Martin HS <martin@swende.se>
* params: fix chainconfig rules

* core, core/state: init accessEvent for each transactin

* core/state: reuse point cache between blocks
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
core/vm/interpreter.go Outdated Show resolved Hide resolved
@@ -402,6 +402,15 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
if evm.chainRules.IsEIP4762 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's wrong here.

In EIp158, we introduce a rule that at most 63/64 gas could be used in a inner call, therefore the gas for CALL opcode should be fully computed, and then 1/64 remaining gas will be deducted for this rule.

However in this pull request, the verkle specific gas is deducted after this rule. It's a behavioral change.

suggested change:

-       if transfersValue && !evm.chainRules.IsEIP4762 {
+       if transfersValue {
                gas += params.CallValueTransferGas
        }
        memoryGas, err := memoryGasCost(mem, memorySize)
@@ -394,7 +394,14 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
        if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
                return 0, ErrGasUintOverflow
        }
-
+       if evm.chainRules.IsEIP4762 && transfersValue {
+               // Deduct the gas cost for value transfers according to the legacy rules.
+               gas -= params.CallValueTransferGas
+               gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address().Bytes()[:], address.Bytes()[:]))
+               if overflow {
+                       return 0, ErrGasUintOverflow
+               }
+       }
        evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
        if err != nil {
                return 0, err
@@ -402,15 +409,6 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
        if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
                return 0, ErrGasUintOverflow
        }
-       if evm.chainRules.IsEIP4762 {
-               if transfersValue {
-                       gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address().Bytes()[:], address.Bytes()[:]))
-                       if overflow {
-                               return 0, ErrGasUintOverflow
-                       }
-               }
-       }

Copy link
Member Author

@gballet gballet May 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so for the part where the sent value is concerned, this is the correct behavior:

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to think about the other part a little, but I am now convinced that you are correct.

core/vm/eips.go Outdated Show resolved Hide resolved
core/vm/eips.go Outdated Show resolved Hide resolved
gballet and others added 3 commits May 6, 2024 14:22
Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that bothers me a bit, with how the access events are designed: you do e.g. BalanceGas, which does two things:

  1. Adds it to the access events
  2. Returns how much it cost

This means that the evm cannot first check if the cost is covered, and then proceed (or not). As it is, the effective gas cost is the "available gas", not the cost.

This is problematic in the following case.
1.Y: Call contract X with near-zero gas
2. X: load slot 1
3. X: The load operation fails because insufficient gas for adding to witness
4.Y: Call contract X with near-zero gas
5. X: load slot 2
6. X: The load operation fails because insufficient gas for adding to witness
7. repeat

The end result is that we've added N slots to the witness, bloating it, at the cost of ~100 (the cost of a call to a warm address, plus a little more). Whereas the 'true' cost would have been something like N x ( (WitnessBranchReadCost @ 1900) + (WitnessChunkReadCost @ 200))

Now, the same is true for a few other ops, notably the access lists. However, the access lists are journalled, which makes this attack moot. This can be fixed, if the design is changed so that either

  1. The methods to "add and return cost" takes a availableGas parameter, and aborts if the cost is higher than that
  2. The methods to "add and return cost" is split into two: return cost and add, where the first is pure, the second just executes the change without any checks.

core/state/access_events.go Outdated Show resolved Hide resolved
core/state/access_events.go Outdated Show resolved Hide resolved
core/state/access_events.go Outdated Show resolved Hide resolved
Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All in all, I'm fairly confident that this does not break existing code. Did you run it on any of the benchmarkers in prod at all? Or somewhere else.. ?

createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
evm.StateDB.SetCode(address, ret)
if !evm.chainRules.IsEIP4762 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should rename IsEIP4762 to IsVerkle, to make things easier for a casual reader

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but here's the thing: EIP4762 and verkle are two separate things, eip4762 pertains to statelessness while verkle is a specific implementation of statelessness (i.e. if for whatever reason we switch to another state tree later, eip4762 will still apply).

gballet and others added 2 commits May 7, 2024 10:52
Co-authored-by: Martin HS <martin@swende.se>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants