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

VM: Generate Access Lists in runTx() #1170

Merged
merged 9 commits into from
Mar 30, 2021
Merged

Conversation

holgerd77
Copy link
Member

Ok, here is the access list generation PR fixing #1152. This needs significantly more tests, but this should generally do what is expected. I just tried to generate the following super-simple access list with this:

{
  gasUsed: <BN: 52f8>,
  execResult: {
    gasUsed: <BN: 0>,
    exceptionError: undefined,
    returnValue: <Buffer >,
    gasRefund: <BN: 0>
  },
  bloom: Bloom {
    bitvector: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 206 more bytes>
  },
  amountSpent: <BN: 2068e0>,
  accessList: [
    {
      address: 'be862ad9abfe6f22bcb087716c7d89a26051f74c',
      storageKeys: []
    }
  ]
}

Which obviously is not enough yet. 😄 Functionality for more complex AL is there but should be manually tested or optimally by adding additional test cases.

For now I've added the functionality to StateManger.clearWarmedAccounts() in a non-breaking way. On a second thought along pushing I rather think it might be better and more flexible to just add a distinct method for that which can be called separately (which then still can be called somewhat along the clearing-call in our context).

I also added a new helper function Address.isPrecompileOrSystemAddress() to Util which checks an address to be a precompile or system address following EIP-1352.

@codecov
Copy link

codecov bot commented Mar 27, 2021

Codecov Report

Merging #1170 (95c9522) into master (71c54f6) will decrease coverage by 0.07%.
The diff coverage is 60.00%.

Impacted file tree graph

Flag Coverage Δ
block 81.98% <ø> (+0.17%) ⬆️
blockchain 84.16% <ø> (ø)
client 84.05% <ø> (+0.01%) ⬆️
common 86.87% <ø> (ø)
devp2p 83.88% <ø> (-0.25%) ⬇️
ethash 82.08% <ø> (ø)
tx 84.25% <100.00%> (+0.19%) ⬆️
vm 81.69% <55.55%> (-0.66%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@@ -75,6 +75,14 @@ tape('Address', (t) => {
st.end()
})

t.test('should provide correct precompile check', (st) => {
const precompile = Address.fromString('0x0000000000000000000000000000000000000009')
st.true(precompile.isPrecompileOrSystemAddress(), 'should detect precompile address')
Copy link
Member

Choose a reason for hiding this comment

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

Hmm this EIP is actually a little bit confusing. It does specify which address range could be assumed to be system precompiles. However, for access lists, we are only allowed to push precompiles which actually have code to the access lists. There was a bug in Besu where they also included BLS addresses to the access lists, which does not match the EIP. What is weird is that EIP2930 in combination with EIP1352 is not very consistent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I'm just using this to remove precompile addresses, so there should be no problem? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

"which actually have code to the access lists"

What do you mean by that actually? 😀

Copy link
Member

Choose a reason for hiding this comment

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

It means that once we setup the initial access list, we add the caller and the target to the access list, but we also add every precompile which has attached code to it. So, Besu made a mistake and also added the BLS precompile addresses. However, in Berlin, we do not have BLS, so there is no precompiled code at these BLS addresses and they should thus not be added to the access lists.

@@ -123,6 +142,10 @@ export default async function runTx(this: VM, opts: RunTxOpts): Promise<RunTxRes
const result = await _runTx.bind(this)(opts)
await state.commit()
debug(`tx checkpoint committed`)
if (this._common.isActivatedEIP(2929) && opts.reportAccessList) {
result.accessList = state.clearWarmedAccounts(true)
Copy link
Member

Choose a reason for hiding this comment

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

I know this is WIP, but this currently only supports the accessed items for the root transaction right? It will not report access lists if you *CALL* into another address.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm it might actually report it, but it will not report if we REVERT one of these inner calls, since that removes the accessed items from warmed storage.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, does this happen very often in practice and do you think it's an important miss?

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 can add extra logic for it though)

Copy link
Member

Choose a reason for hiding this comment

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

Well, internal reverts sometimes happen, and if you don't put these storage slots on the access lists then you are going to get charged to touch a cold account multiple times. So if we want to generate "correct" access lists (we want to be friendly and provide the complete list of the slots and accounts we are going to add in the access list), then we should also include the reverted slots.

This is actually a bit mind-boggling... There is an edge case here. What if an internal call reverts, but if we add it to the access list then it does not revert? This might also change the behavior of the call; we might now touch other slots. I think we could try to rebuild the access lists based upon whatever we just touched, but I think we might get in a flip/flop situation where each time you edit the access list, you get another access list which we already considered before 🤔 (Hope this reasoning makes sense)

Copy link
Member

Choose a reason for hiding this comment

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

I will ask on R&D

Copy link
Member

Choose a reason for hiding this comment

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

Checkout geth's implementation; ethereum/go-ethereum#22550

They cap the count of attempts to generate an access list at 10.

@holgerd77
Copy link
Member Author

Hi @jochem-brouwer, thanks for digging in so deeply here, really cool! 😄 I won't do this edge case implementation here since I simply don't have the development resources for this given all our other tasks atm, instead I've added a note here along the implementation. Since this is not breaking anything if an access list might not be complete on some edge cases (+ also this 10 -run-limit from geth seems also conceptually be rather a practical solution than a 100%-correct implementation) and it is not yet clear how much this whole access list feature will be used, I think this should be acceptable.

I have added the additional structure though for revert(), also did some further functional cleanup on the folding logic in the StateManager and added a set of new tests dedicatedly for StateManager.createAccessList().

Ah, yes, and this is now a separate function, haven't added to the official interface yet though for compatibility reasons. Instead runTx() (with reportAccessList option activated) will throw if used with a custom state manager not implementing this method.

Ok, ready for review. 😄

@holgerd77
Copy link
Member Author

Hi @ryanio, could you have a look at the latest VM API test run, which is failing for the karma browser run? I could just add the examples folder to the exclude directive and also add this downlevelIteration compiler option, but I guess you are better at judging the overall situation and see why this might be happening in the first place and if I e.g. introduced some structures which are problematic from a browser perspective or something.

this._accessedStorage.pop()
const lastItem = this._accessedStorage.pop()
if (lastItem) {
this._accessedStorageReverted.push(lastItem)
Copy link
Member

Choose a reason for hiding this comment

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

This will degrade performance, right? If we don't want to get the access lists, then we also should not copy these items since it is unnecessary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yes, you are right. With this openly usable generateAccessList() function we might nevertheless want to leave as-is, but haven't thought about this aspect yes.

if (!address.isPrecompileOrSystemAddress()) {
const storageSlots = Array.from(slots)
.map((s) => `0x${s}`)
.sort()
Copy link
Member

Choose a reason for hiding this comment

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

Sorting this is fine, but it is not really necessary, right?

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 would be a fan of sorting (even if it comes to some performance costs), I think this gives a better overview if one want to manually check the outcome. And I guess in this use case performance is a bit secondary (just an unproven assumption reflecting on how I imagine people using this stuff).

Copy link
Member

@jochem-brouwer jochem-brouwer left a comment

Choose a reason for hiding this comment

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

Some general remarks, looks great in general, nice tests 👍

Copy link
Contributor

@ryanio ryanio left a comment

Choose a reason for hiding this comment

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

lgtm!

@ryanio ryanio merged commit 71e0f67 into master Mar 30, 2021
@ryanio ryanio deleted the vm-access-list-generation branch March 30, 2021 02:43
@jochem-brouwer
Copy link
Member

Question here, do the reported access lists sometimes also report the sender address or the to address (or a precompile)? Because these should not be included on the access lists, they are already added to the lists internally by default.

@holgerd77
Copy link
Member Author

@jochem-brouwer ah, good catch, yes, I assume these are still in. Will put the release PR on "blocked" and open a subsequent PR soon.

@holgerd77
Copy link
Member Author

@jochem-brouwer ah, actually to is likely not a good candidate to remove since there might be storage slots added as well? 🤔 Would it be a problem if we then just keep both of the addresses in?

@jochem-brouwer
Copy link
Member

Ah you are right. We only don't want to put to in if the storage slots there are empty. But I do not think there is a situation where this can occur. It is not really a problem if we would put the to address in with no slots, but that means the user has to pay more gas than necessary.

@holgerd77 holgerd77 mentioned this pull request Feb 16, 2022
51 tasks
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

3 participants