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

witness: only include elements if there's enough gas for it #495

Open
wants to merge 36 commits into
base: kaustinen-with-shapella
Choose a base branch
from

Conversation

jsign
Copy link
Collaborator

@jsign jsign commented Sep 12, 2024

This PR does changes to satisfy a new requirement when adding elements to the execution witness.

The new requirement involves only adding elements to the witness if there's sufficient gas for it.

Examples:

  • Imagine a JUMP that jumps to a new code chunk, charging 200 gas to be included in the witness. Before this PR, TouchCodeChunksRangeAndChargeGas(...) would add the code chunk to the witness and return the cost to be charged. If we had, say, 100 gas available, this would mean that the user didn't have enough funds to pay for that (i.e: 100<200). But it's too late, and it was already added to the witness.
  • Imagine an instruction writes a new balance in a non-existen address, thus this address should be created. This involves writing BASIC_DATA and CODEHASH of this address. Before this PR, we always added BASIC_DATA and CODEHASH and returned the cost. If the user only had gas to pay for BASIC_DATA but not CODEHASH, it would be too late since we have already added both.

More generally, if you have X of available gas and include something in the witness cost Y, with Y>X, then we must not include it in the witness. This is made more apparent when we try to include a wide range of code chunks and only have gas for one but are asked to include 15. Before this PR, we were including 15, not only one.

From a stateless client perspective, being this strict is fine. Knowing if you have enough gas to include something in the witness doesn't depend on what you received from the witness (i.e: this PR including less things in the witness doesn't affect it). This is because everything you need to know to be sure that you can add something new to the witness is the access witness state of which branches and code chunks were added.

@jsign jsign changed the title Jsign witness fix witness: only include elements if there's enough gas for it Sep 12, 2024
@gballet gballet added this to the verkle-gen-devnet-7 milestone Sep 13, 2024
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
.
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
statedb.Witness().TouchFullAccount(w.Address[:], true)
statedb.Witness().TouchFullAccount(w.Address[:], true, nil)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For any Touch*(...) call that doesn't care about charging gas, we send the nil UseGas(...) function.

This is the equivalent action of the previous API where we ignored the returned value (since we didn't charge that gas to anyone).

@@ -30,6 +29,9 @@ import (
// * the second bit is set if the branch has been read
type mode byte

// UseGasFn is a function that can be used to charge gas for a given amount.
type UseGasFn func(uint64) bool
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Now Touch*(....) functions receive a function that we'll use as a callback to charge the required gas.

Many Touch*(...) do more than one charging. This means that this callback could be called multiple times.

Before I tried another idea which was not passing this callback, but asking for availableGas. The problem with that is that it adds a lot of strain in callers to solve partial charging. The Touch*(...) calls can charge for more than one thing. In the case the availableGas was sufficient for some of them but not all, that means the API should return:

  • gasThatMustBeCharged
  • and ok bool to signal if the availableGas was sufficient for everything or not.

The caller should:

  • Always charge gasThatMustBeCharged, respectively of what ok is. Since the passed availableGas was at least sufficient to put some stuff in the witness.
  • if ok is false, fail due to out of gas.

The above logic should be done in all the callers.

This proposed API where we pass a callback function, avoids all this logic in all callers. The API call will already charge for whatever was allowed, and simply return false signaling that the available gas was insufficient and let the caller fail (i.e: the caller shouldn't deal with charging stuff since that was already done in the API call).

That's the "TLDR" of this approach. I know was mentioned this still sounds like can get some pushback from the geth team, so we can try again with the other approach... but I found it a bit annoying.

}

// touchAddress adds any missing access event to the witness.
func (aw *AccessWitness) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) (bool, bool, bool, bool, bool) {
func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool, useGasFn UseGasFn) (uint64, bool) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the new "base" fundamental method that charges for stuff.
Basically:

  1. we fill the boolanes if this charging required branchRead, chunkRead, etc. But we don't yet affect aw.[branches|chunks] since we want to do that ONLY if we have available gas.

(Continue to read below comments for further bullets)

Comment on lines +180 to +195
var gas uint64
if branchRead {
gas += params.WitnessBranchReadCost
}
if chunkRead {
gas += params.WitnessChunkReadCost
}
if branchWrite {
gas += params.WitnessBranchWriteCost
}
if chunkWrite {
gas += params.WitnessChunkWriteCost
}
if chunkFill {
gas += params.WitnessChunkFillCost
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. With the booleans, we calcualte the gas as usual.

Comment on lines +197 to +201
if useGasFn != nil {
if ok := useGasFn(gas); !ok {
return 0, false
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. Ask for the callback to charge this gas, and signal us if it was possible or not.

If wasn't possible, return early. Notice that we haven't technically added anything to the witness. This is important.

Comment on lines 265 to 266
chargedGas, ok := interpreter.evm.Accesses.TouchBasicData(address[:], false, scope.Contract.UseGas)
if !ok || (chargedGas == 0 && !scope.Contract.UseGas(params.WarmStorageReadCostEIP2929)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You could be wondering why some of the Touch*(...) APIs returned the charged gas if we were sending the callback?

This is the reason. We have this rule that if on many charges the actual charged gas was 0, then we charge warm. We ned a way to know if that UseGas was asked to charge 0 or not.

If was 0, we do a usual scope.Contract.UseGas(Warm).

Copy link
Owner

Choose a reason for hiding this comment

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

hmmm yeah but we could simplify this by adding a chargeWarm flag to the function, so that we don't need to clutter the code too much here. If you still have to call UseGas after calling the function you pass it to, this makes the code that much harder to read.

Comment on lines 770 to 786
func chargeCallVariantEIP4762(evm *EVM, scope *ScopeContext) bool {
target := common.Address(scope.Stack.Back(1).Bytes20())
if _, isPrecompile := evm.precompile(target); isPrecompile {
return scope.Contract.UseGas(params.WarmStorageReadCostEIP2929)
}
// The charging for the value transfer is done BEFORE subtracting
// the 1/64th gas, as this is considered part of the CALL instruction.
// (so before we get to this point)
// But the message call is part of the subcall, for which only 63/64th
// of the gas should be available.
chargedGas, ok := evm.Accesses.TouchAndChargeMessageCall(target.Bytes(), scope.Contract.UseGas)
if !ok || (chargedGas == 0 && !scope.Contract.UseGas(params.WarmStorageReadCostEIP2929)) {
return false
}
return true

}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the makeCallVariantGas4762 layer that we removed and moved to op*Call(...) codes.

Comment on lines 789 to 812
if interpreter.evm.chainRules.IsEIP4762 {
address := common.Address(scope.Stack.Back(1).Bytes20())
transfersValue := !scope.Stack.Back(2).IsZero()

// If value is transferred, it is charged before 1/64th
// is subtracted from the available gas pool.
if transfersValue {
if !interpreter.evm.Accesses.TouchAndChargeValueTransfer(scope.Contract.Address().Bytes()[:], address.Bytes()[:], scope.Contract.UseGas) {
return nil, ErrExecutionReverted
}
}
}

var err error
interpreter.evm.callGasTemp, err = callGas(interpreter.evm.chainRules.IsEIP150, scope.Contract.Gas, 0, scope.Stack.Back(0))
if err != nil {
return nil, err
}
if !scope.Contract.UseGas(interpreter.evm.callGasTemp) {
return nil, ErrOutOfGas
}
if !chargeCallVariantEIP4762(interpreter.evm, scope) {
return nil, ErrExecutionReverted
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For CALL we moved the 1/64 stuff here.

@@ -762,6 +852,9 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt
}

func opCallCode(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
if !chargeCallVariantEIP4762(interpreter.evm, scope) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the rest of *CALL, we simply call the helper so keeps being relatively simple.

@@ -1,160 +0,0 @@
// Copyright 2024 The go-ethereum Authors
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nothing about this file survived, which makes sense.
The gas*(...) style of apis can't work with what we need to do, so it makes sense that everything here went away (since was mostly moved to op*(..) above).

@jsign jsign requested a review from gballet September 19, 2024 19:44
@jsign jsign marked this pull request as ready for review September 19, 2024 19:44
Signed-off-by: Ignacio Hagopian <[email protected]>
Copy link
Owner

@gballet gballet left a comment

Choose a reason for hiding this comment

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

just leaving a comment on how to improve the handling of warm costs, going to experiment quickly but if I can't get anything to work today, I'll merge and try to fix it later.

Comment on lines 265 to 266
chargedGas, ok := interpreter.evm.Accesses.TouchBasicData(address[:], false, scope.Contract.UseGas)
if !ok || (chargedGas == 0 && !scope.Contract.UseGas(params.WarmStorageReadCostEIP2929)) {
Copy link
Owner

Choose a reason for hiding this comment

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

hmmm yeah but we could simplify this by adding a chargeWarm flag to the function, so that we don't need to clutter the code too much here. If you still have to call UseGas after calling the function you pass it to, this makes the code that much harder to read.

Signed-off-by: Ignacio Hagopian <[email protected]>
Copy link
Owner

@gballet gballet left a comment

Choose a reason for hiding this comment

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

This is just a brain-dumping wall of text, so that I can come back to it when there is pushback by the geth team.

Doesn't LGTM, as I expect there will be substantial pushback when mainlining this, but I tried using the upper bound for myself and can confirm the difficulty of the approach. I still think there is a way to make this PR less intrusive that it already is.

It took me a bit to get to the depth of the reason for this function parameter, but I agree it will be hard to work around it. The approach that consumes all the gas and returns the consumed gas works most of the time, but it doesn't make the difference between something that took all the remaining gas (for which we should still execute the action), and something that went out of gas (for which we should not).

It is possible to share this information and let the calling code consume the gas, which is much cleaner. I am currently trying this approach, and will throw in the towel if I can't make it work in the next couple of hours.

Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
* fix

Signed-off-by: Ignacio Hagopian <[email protected]>

* eip4762: push witness fix

Signed-off-by: Ignacio Hagopian <[email protected]>

---------

Signed-off-by: Ignacio Hagopian <[email protected]>
* fix

Signed-off-by: Ignacio Hagopian <[email protected]>

* nit

Signed-off-by: Ignacio Hagopian <[email protected]>

---------

Signed-off-by: Ignacio Hagopian <[email protected]>
* ci: new workflows for testing

Signed-off-by: Ignacio Hagopian <[email protected]>

* nit

Signed-off-by: Ignacio Hagopian <[email protected]>

* ci: fix stable consumption

Signed-off-by: Ignacio Hagopian <[email protected]>

* fix

Signed-off-by: Ignacio Hagopian <[email protected]>

* more fixes

Signed-off-by: Ignacio Hagopian <[email protected]>

* more fixes

Signed-off-by: Ignacio Hagopian <[email protected]>

* update tag

Signed-off-by: Ignacio Hagopian <[email protected]>

---------

Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
gballet and others added 2 commits October 15, 2024 17:25
Signed-off-by: Guillaume Ballet <[email protected]>
…CODEHASH (#508)

* avoid extra 100 gas in value-bearing CALL

Signed-off-by: Ignacio Hagopian <[email protected]>

* missing codeHash write event

Signed-off-by: Ignacio Hagopian <[email protected]>

* feedback

Signed-off-by: Ignacio Hagopian <[email protected]>

* Update core/vm/evm.go

Co-authored-by: Guillaume Ballet <[email protected]>

---------

Signed-off-by: Ignacio Hagopian <[email protected]>
Co-authored-by: Guillaume Ballet <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
* fix EXTCODESIZE rule for system contract

Signed-off-by: Ignacio Hagopian <[email protected]>

* 21000 witness target write touch CODEHASH

Signed-off-by: Ignacio Hagopian <[email protected]>

* fix EXTCODECOPY witness for system contract

Signed-off-by: Ignacio Hagopian <[email protected]>

* rename and comment

Signed-off-by: Ignacio Hagopian <[email protected]>

---------

Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants