-
Notifications
You must be signed in to change notification settings - Fork 773
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, Common: implement eip-3540 (EOF1) #1719
Conversation
To run the |
Codecov Report
Flags with carried forward coverage won't be shown. Click here to find out more. |
packages/vm/src/evm/opcodes/util.ts
Outdated
sectionSizes.code = (code[4] << 8) | code[5] | ||
} else if ( | ||
code.length > 10 && | ||
code[3] === secCode && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EIP does not specify an order of the sections, so code[3]
could also be secData
. I would suggest a loop over these sections, this is also forward-compatible if new sections get introduced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EIP does not specify an order of the sections, so
code[3]
could also besecData
I don't believe this is correct. The EIP validation rules (excerpted below) require an order. Also, I'm following the reference implementation here.
"
Exactly one code section MUST be present.
The code section MUST be the first section.
A single data section MAY follow the code section.
"
I can't link to the section directly but if you look under the validation rules, it details the specifics about EOFv1 rules so let me know if I've misinterpreted something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, sorry, you are right, it must be the first section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As pointed out by @acolytec3 the first section should be the code section so in version 1 of EOF then the second section can only be the data section.
packages/vm/src/evm/opcodes/util.ts
Outdated
sectionSizes.code = (code[4] << 8) | code[5] | ||
sectionSizes.data = (code[7] << 8) | code[8] | ||
} | ||
if (code.length !== codeSize) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no check for the data size, what if this underflows or overflows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, the naming here is imprecise. codeSize
is the computed length of the entire bytestring based on the size of the header + the size of the code section and the data section. I'll clean up the naming to make things a little more clear.
@axic I have raised some questions about this EIP on https://ethereum-magicians.org/t/evm-object-format-eof/5727/28 😄 |
packages/vm/src/evm/evm.ts
Outdated
result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) } | ||
} | ||
// EIP-3540 EOF1 checks | ||
if (!eof1CodeAnalysis(result.returnValue)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I like this function signature, so this code path will run if the eof1CodeAnalysis
returns undefined
, but I'm also not super sure what would make this more clean... 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could throw instead though I'd have to rework the helper. Seems like that might be slower than just checking for undefined. Other alternatives (like constructing an execResult
with the right error the helper) didn't quite feel right to me either.
I want to discuss this question more generally than in the specific code comment. I'm open to something like this (and my original implementation was more along this line) but I decided after going through it that it made more sense (at least to me) that we implement separate functions for each EOF version to check code validity. That way, we don't have to be parsing common parameters each time we do this code check. Given we don't know what the future EOFs may look like, there may be additional handling that is not currently envisaged that we will need to incorporate and it feels like unnecessary work at the present time to try and predict what form that will take and generalize our code in some other way. My thought is if/when a new EOF v2 is added to the protocol, we can just look at that version byte in the header and call the appropriate code validation helper. |
packages/vm/src/evm/opcodes/util.ts
Outdated
@@ -261,3 +261,35 @@ export function updateSstoreGas( | |||
return new BN(common.param('gasPrices', 'sstoreSet')) | |||
} | |||
} | |||
|
|||
export const eof1CodeAnalysis = (code: Buffer) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think code
as input here is misleading. I would suggest changing it to container
this is the term the EIP uses as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, will update again. Had set it to byteCode
but agree container
makes more sense.
Cool, great start, will use this as a general comment as I think certain parts of the EIP are not present.
|
Ah, thanks for the catch. Addressed in my latest commit.
This is implemented here, I fixed the names so it would be more obvious
I guess I thought this would be taken care of by the PC counter checks in the
|
60fa984
to
c23c721
Compare
This PR was merged into this one: #1743 |
return false | ||
} | ||
} | ||
} | ||
const terminatingOpcodes = new Set([0x00, 0xd3, 0xfd, 0xfe, 0xff]) | ||
const terminatingOpcodes = new Set([0x00, 0xf3, 0xfd, 0xfe, 0xff]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, missed this 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, me too. But writing those new tests you suggested helped me catch it 🎆
Hmm, it seems that I somehow mixed up |
a141217
to
7a7b28c
Compare
There are some steps left to do for this PR which are essentially listed in this #1719 (comment) comment. I can help creating that CREATE/CREATE2 bytecode. We should also update the case where the "intrinsic gas" is consumed (data fee + upfront fee) in case we try to de a create transaction where the EOF format is invalid. I have pinged an author to clarify/update the EIP regarding what "abort" means in terms of gas. (Also seems that there is a linting problem) Side note: once the EIP-specific tests get merged in Ethereum/tests we should also add those forks to CI in case we run "test all hardforks". |
Have added invalid eof initcode tests for tx/create/create2. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I am concerned this is ready to merge, let's wait for one more review.
Ah, ok, interesting (and: cool!). 👍 😄 🎉 I would then have a strong tendency to integrate this in the current round of releases, seems to be a very good occasion to directly get this out. Will do a final review on this and merge if everything is ok and add to #1774. I would then try to do the releases itself tomorrow, Tuesday (CET) morning - also depending on the outcome of #1781 and a final "go!" from @g11tech and @ryanio, and then do a first Twitter thread directly that day solely focusing on the Kiln aspects of the release round. This would be followed up by a bugfix release round either at the end of the week or early next week, fixing some things from Kiln + adding the merge blocks and stuff and on that release occasion I would do another Twitter thread (more or less) solely focusing on all other release features (EIP-3860, Extended VM Genesis, EIP-3540,...). So far on the current plan [TM] 😎. |
} | ||
return sectionSizes | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is checking on so many conditions in such a condensed form that it is extremely hard to review and generally follow. Can we have this dramatically [TM] more commented, so basically to a comment with a condition on more or less every line of code? Guess that would make things easier to grasp.
Also: one technique @ryanio often applies and I guess which would help here as well is to instead of checking for the positive condition (if (container[1] === magic && container[2] === version) {
) and then nesting further down check for the negative (if (container[1] !== magic || container[2] !== version) {
) and the directly do the return
. This prevents further nesting and could be minimally applied on this example I made but I guess also on other code parts.
packages/vm/src/evm/interpreter.ts
Outdated
code.slice(0, 1).equals(Buffer.from('ef', 'hex')) | ||
) { | ||
if (code.slice(1, 2).equals(Buffer.from('00', 'hex'))) { | ||
if (code.slice(2, 3).equals(Buffer.from('01', 'hex'))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: negation and direct return.
Oh, that's a really great update! Thanks a lot! 👍 😍 |
I can't find/identify all parts of spec integrations in this huge EIP TBH, so I'll go with some basic questions for @acolytec3 just for re-assurance for a second review: So first round would be: are you confident that you have "caught" all execution semantics changes from this list:
Or is there one where one should have another eye upon and do some verification? |
if ( | ||
!eof1ValidOpcodes( | ||
result.returnValue.slice(codeStart, codeStart + eof1CodeAnalysisResults.code) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is from the EIP-3670
spec:
At contract creation time both initcode and code are iterated instruction-by-instruction
Might it be that you are doing validation only on the code and not the initcode? Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is checking the code returned by the create contract to verify its valid . The initcode is checked when it is run through the runInterpreter
function call.
Thanks for the feedback. I'll address these as best I can but happy to dig into things further(and @jochem-brouwer keep me honest on this).
I believe these 5 are all implicitly addressed by [this line of code]((
If the code passed to the interpreter contains an EOF1 header, we define the For JUMP/JUMPI offsets, I think you might have a point that we missed a step in checking here since check against
I looked at all 4 of these function handlers again and they are all checking against
From looking at the opcode handlers, the inputs are entirely dependent on what is passed to them as here for CREATE. Whatever offset/length are popped from the stack determines what code is pulled in but once that code is passed in, it is treated as an entire container and analyzed for EOF1 validity in the same way as a basic create transaction. So, the long answer to your short question is "yes" as far as "the entire container" corresponds to whatever code is passed to I'd also like @jochem-brouwer to comment on this one. He wrote the tests that validate the Let me know if this addresses your questions. |
Totally, thanks a lot, explanations couldn't be better! Ok, I guess I am confident enough here that I would do the following, since timing of this whole release round is really a bit challenging with all different requirements: I will merge this in now and - directly - release with the Kiln updates. In case that we stumble upon some additional things post-release we can still add this to the very imminent bugfix releases (also necessary for some Kiln updates), to be released directly on Friday or early next week. I guess that should catch all release requirements best, so we have the Kiln release out in time and also do not miss this release opportunity on the work here but still have the time for a short-termed correction if necessary (which should be very much ok given that we release as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Fixes #1704
This adds support for EIP-3540. This EIP is not part of any hardfork and can only be activated in concert with 3541. At present, this implements EVM Object Format V1(EOF1) checks as a single utility function in
src/evm/opcodes/util.ts
. There was some significant discussion around keeping all of the specific parameters governing the EOF1 checks as parameters in the EIP specification inCommon
but after working through it, it feels like it makes more sense (at least based on the current status of the EIP) that we just incorporate all of those parameters as hard coded values in a utility function that validates the code passed in based on the EOF1 rules (rather than try to generalize to a generic EOF validation function). If/when future EOF versions come up for review/implementation, we can revisit this.There are a couple of other changes made to the VM internals, notably:
runState.code
is now set based on whether or not the bytecode passed with the message is EOF1 compliant or not (code section of EOF1 container if EOF1 bytecode or entiremessage.code
for legacy code)PUSH
opcode handler that checks to see if it goes out of bounds and reverts if so (this doesn't seem to break API tests and gives a guard in EOF1 compliant bytecode to going out of bounds)runState.code
which will be set as the code section of the EOF1 container if the bytecode is EOF1 compliant.The ethereum/tests PR containing tests for this EIP is still WIP as well so we should probably wait to merge that PR gets merged or else feels stable.