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

implement FILL_COSTS on top of devnet7 #524

Draft
wants to merge 3 commits into
base: kaustinen-with-shapella
Choose a base branch
from

Conversation

gballet
Copy link
Owner

@gballet gballet commented Nov 4, 2024

TODO:

  • rewrite code so that the access event takes the isFill from the statedb directly, as much as possible.

core/vm/operations_verkle.go Outdated Show resolved Hide resolved
Comment on lines +125 to +129
// The isFill here is a bit strange: one would have to pay for the fill costs
// and potentially run out of gas before the account is created. This is due
// to the charging for a potential account creation being split in two locations:
// the basic data is written in the gas function, and the code hash in opCall.
_, wanted2 := aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, isFill, availableGas-wanted1)
Copy link
Owner Author

Choose a reason for hiding this comment

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

@jsign this is the comment describing the situation we just discussed. I think it's terrible. Let me know if you have a better way to describe this, otherwise I'll take another shot at it tomorrow.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I see. I don't think is that bad. If I look at this method in a vaccum, what is doing seems logical compared to how the API is proposed. i.e: the caller should already exist so looks reasonable L121 is ignoring isFill and setting to false, and the targetAddr we trust the callr to tell us if fill cost should be charged.

core/state_processor_test.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@jsign jsign left a comment

Choose a reason for hiding this comment

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

I left some comments for your consideration. I think most of what I say won't be surprising since I think I shared those thoughts a while ago when chatting about FILL_COSTs.

Also, not proposing doing a refactor on this PR. If the geth team is OK with this solution, I think it's fine (of course) -- just that form I prefer less assumptions in calls.

statedb.Witness().TouchFullAccount(w.Address[:], true, math.MaxUint64)
statedb.Witness().TouchFullAccount(w.Address[:], true, true, math.MaxUint64)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this code line already shows something evident in the proposed isFill bool API.

Every caller should decide if the witness addition should charge a fill cost, which seems odd unless every caller knows if the underlying tree has a "hole" in the tree at that place.

For example, for this line, it doesn't seem obvious why a withdrawal would need to charge fill cost unconditionally since that seems to depend on runtime conditions. In this case isn't that relevant since we aren't charging that to anyone -- but just feels odd.

The much nicer alternative is not including this isFill bool in the APIs, and let AccessWitness figure out if the write should charge fill cost, probably assisted by statedb.

I think I mentioned this a while ago when we discussed about FILL_COST, so shouldn't be a big surprise this thought came again now -- resharing just in case.

Copy link
Owner Author

Choose a reason for hiding this comment

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

The much nicer alternative is not including this isFill bool in the APIs, and let AccessWitness figure out if the write should charge fill cost, probably assisted by statedb.

yeah that is right, we would save some typing for sure. This is meant for measurements so I won't do it before devcon, but you are correct that we should do it this way.

Comment on lines +52 to +56
func NewAccessWitness(pointCache *utils.PointCache, fills map[chunkAccessKey]struct{}) *AccessWitness {
return &AccessWitness{
branches: make(map[branchAccessKey]mode),
chunks: make(map[chunkAccessKey]mode),
fills: fills,
Copy link
Collaborator

Choose a reason for hiding this comment

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

First reaction looking at this, is why you'd like to initialize a witness with a pre-filled map.

My intuition this is related with AccessWitness not having access to statedb, so now you have to deal with previous tx in the same block having done writes that this tx shouldn't charge. So.. kind of a second order "dirty" map.

This might add up to the previous comment I did why I think AccessWitness having access to statedb might be a good idea. You can avoid having to do this trick, and just rely on statedb in a state already committed by previous tx, so no "special consideration" is needed.

(Note: not saying we should do this in this PR or similar -- mostly talking about long term strategies)

Copy link
Owner Author

Choose a reason for hiding this comment

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

the difference is that writes are per-tx while the fills are per-block. Not sure the stateedb can always provide us with this info, I'll have to think about it when rewriting.

return naw
}

func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool, availableGas uint64) uint64 {
func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite, isFill bool, availableGas uint64) uint64 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As mentioned before: assuming the caller always is write about knowing the tree doesn't have the value, then I guess this is fine.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note this also shows as fundamental flaw in the API design: someone calling isWrite=false nad isFill=true.

I think I had said this before too and suggested maybe a flag-ish style API? Like isRead | isWrite | isWriteWithFill. Despite this solves the flaw, it can be somewhat annoying from the caller side -- but looks safer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we could add:

if !isWrite && isFill {
   panic("can't can't charge fill cost in a state read")
}

or similar.

Copy link
Owner Author

Choose a reason for hiding this comment

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

you can't charge fill costs in a state read, if write is false.

Comment on lines +125 to +129
// The isFill here is a bit strange: one would have to pay for the fill costs
// and potentially run out of gas before the account is created. This is due
// to the charging for a potential account creation being split in two locations:
// the basic data is written in the gas function, and the code hash in opCall.
_, wanted2 := aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, isFill, availableGas-wanted1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I see. I don't think is that bad. If I look at this method in a vaccum, what is doing seems logical compared to how the API is proposed. i.e: the caller should already exist so looks reasonable L121 is ignoring isFill and setting to false, and the targetAddr we trust the callr to tell us if fill cost should be charged.

@@ -294,8 +313,8 @@ func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, s
return statelessGasCharged, statelessGasCharged
}

func (aw *AccessWitness) TouchBasicData(addr []byte, isWrite bool, availableGas uint64, warmCostCharging bool) uint64 {
_, wanted := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite, availableGas)
func (aw *AccessWitness) TouchBasicData(addr []byte, isWrite, isFill bool, availableGas uint64, warmCostCharging bool) uint64 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Feel some tension here between isFill and warmCostCharging. Looks like when isFill=true, whatever warmCostCharging say is not relevant.

Not saying this is a huge problem... just feels a bit weird.

Copy link
Owner Author

Choose a reason for hiding this comment

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

agreed, not a problem but we're starting to accumulate booleans and that creates for non-sensical situations that should never happen ™️ but might be triggered at some point

@@ -192,5 +192,5 @@ func ProcessParentBlockHash(statedb *state.StateDB, prevNumber uint64, prevHash
var key common.Hash
binary.BigEndian.PutUint64(key[24:], ringIndex)
statedb.SetState(params.HistoryStorageAddress, key, prevHash)
statedb.Witness().TouchSlotAndChargeGas(params.HistoryStorageAddress[:], key, true, math.MaxUint64, false)
statedb.Witness().TouchSlotAndChargeGas(params.HistoryStorageAddress[:], key, true, true, math.MaxUint64, false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ditto. It's correct for some time while the ring buffer is empty, but after some point is actually always false. (Again can argue doesn't matter since we ignore the output, but still).

st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true, math.MaxUint64)
st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true, true, math.MaxUint64)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ditto.

core/state_processor_test.go Outdated Show resolved Hide resolved
core/state_processor_test.go Outdated Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, so the three diffs in this file are "redundant cases" of isFill since isWrite=false. Just a consequence of the API.

Signed-off-by: Guillaume Ballet <[email protected]>
Signed-off-by: Guillaume Ballet <[email protected]>
Comment on lines +170 to +175
func (s *stateObject) GetOriginState(key common.Hash) common.Hash {
if value, cached := s.originStorage[key]; cached {
return value
}
return common.Hash{}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could be somehow better signal that originStorage is a cache and not a fully reliable source of truth? Maybe renaming to originStorageCache and also rename the method to GetOriginStateCache or something?

I don't like the rename to GetOriginStateCache but L309 of core/state/statedb.go was quite surprising to me, and then I realized it's because it's only asking for a cache.

And even a more important reason to be super-clear about this GetOriginState being a cache, is that this method is public. If I see that name in an API and returns common.Hash{} I'd assume it doesn't exist, not that "potentially I don't know".

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