-
Notifications
You must be signed in to change notification settings - Fork 756
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 EIP 7702 #3470
Implement EIP 7702 #3470
Conversation
So this is touching the whole topic of a fitting structural setup for the tx library which we litterally have for years, so since tx types have been introduced. I don't think "stacking" or inheritance is a good design choice here, also cc-ing @gabrocheleau here since you had some related exchange in the chat, since this is just tighly bundling functionality together which now "accidentally" matches but will likely bite us in the future. Tx types are designed to be independent of each other (for the good and the bad), so there is no such thing as "this tx type inherits its properties from tx type x" or similar stated in EIPs, and in the future arbitrary things can (and likely will) change, be it the way access lists are structured or the fee market is organzied (to pick up the 2930/1559 example). If you think this along the lines of a potentially introduced inheritance, things get messy pretty quickly (or: first one is starting with some still-working work-arounds (inherited This actually also goes for the I think what describes a desired design goal best is the concept of "composability", so that one can plug the different "features" togehter, as independently as possible. That's why we came up with this When looking at the current code I find this actually not "quite as bad" already. 🙂 (things would be a lot worse without our So: I wouldn't count these one liner integrations like: getMessageToSign(): Uint8Array {
return EIP2718.serialize(this, this.raw().slice(0, 9))
} actually as redundancy. This is more "by design" and rather has its beauty by stating explicitly what this respective tx type composes off (takes the base seralization functionality from generic tx types). What is admittedly a bit suboptimal here is this code doc doubling, maybe we can look into this a bit specifically at some point and find a targeted solution here (not sure, is it possible to "programmatically reference existing code docs" to be included? 🤔). I generally think to improve here needs specific looks into what is doubled/getting redundant. I think we just have different "more subtle" problem sets still in which needs to be addressed separately and are a bit a left-over (respectively just: weren't addressed yet) along this capability refactor (since this was the big first step) and are just "step 2". I will try to tear this a bit apart, will see how far I'll come 🙂: raw() methodSo the What we could do here is to put the 1559 inner method code over to the 1559 capabilites and call the capability code from the 1559 tx and then also call the 1559 capability code from the 7702 tx and then use Might be worth it for code redundancy reduction, and might also not have relevant performance implications. Note: I guess these kind of things I write down here are structurally present throughout the wider code base and will likely be able to applied on other tx types as well (if we decide we want that). Static constructors if (
equalsBytes(serialized.subarray(0, 1), txTypeBytes(TransactionType.EOACodeEIP7702)) === false
) {
throw new Error(
`Invalid serialized tx input: not an EIP-7702 transaction (wrong tx type, expected: ${
TransactionType.EOACodeEIP7702
}, received: ${bytesToHex(serialized.subarray(0, 1))}`
)
}
const values = RLP.decode(serialized.subarray(1))
if (!Array.isArray(values)) {
throw new Error('Invalid serialized tx input: must be array')
} I think this can go into a Guess main construtorNot sure if it's possible to put the following into a one-liner in the 2930 capabilites, might be worth it: // Populate the access list fields
const accessListData = AccessLists.getAccessListData(accessList ?? [])
this.accessList = accessListData.accessList
this.AccessListJSON = accessListData.AccessListJSON
// Verify the access list format.
AccessLists.verifyAccessList(this.accessList) I think most of the following can be put into a 1559 capability check method? 🤔 this.maxFeePerGas = bytesToBigInt(toBytes(maxFeePerGas))
this.maxPriorityFeePerGas = bytesToBigInt(toBytes(maxPriorityFeePerGas))
this._validateCannotExceedMaxInteger({
maxFeePerGas: this.maxFeePerGas,
maxPriorityFeePerGas: this.maxPriorityFeePerGas,
})
BaseTransaction._validateNotArray(txData)
if (this.gasLimit * this.maxFeePerGas > MAX_INTEGER) {
const msg = this._errorMsg('gasLimit * maxFeePerGas cannot exceed MAX_INTEGER (2^256-1)')
throw new Error(msg)
}
if (this.maxFeePerGas < this.maxPriorityFeePerGas) {
const msg = this._errorMsg(
'maxFeePerGas cannot be less than maxPriorityFeePerGas (The total must be the larger of the two)'
)
throw new Error(msg)
} addSignatureI have the impression this is just a bit ugly because there is generally something a bit suboptimally designed around here. Not sure, maybe this can be generally made more elegant throughout the library (might or might not need some breaking changes). Yeah, so I would say that's already mostly it. I would agree that if we "fix" the doc redundancy problem (generally), that would be a somewhat big win for the library. But otherwise things should already look substantially (and, from my POV: sufficiently) better 🙂 without the need for very deep reaching refactoring. |
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 - we should remove the TODO and merge.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more. |
maxPriorityFeePerGas: bigIntToHex(this.maxPriorityFeePerGas), | ||
maxFeePerGas: bigIntToHex(this.maxFeePerGas), | ||
accessList: accessListJSON, | ||
authorizationList: this.AuthorizationListJSON, |
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.
why are we saving json upfront in the tx field
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 also do this in 1559 / 2929, I suggest we factor this out in another PR which also covers those txs
return false | ||
} | ||
|
||
export function isAuthorizationList( |
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 typescript should be able to infer the other type by itself if you use !isAuthorizationList
but am not 100% sure of typescript shenanigans
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 have copied this pattern from the access lists :)
if (isAuthorizationList(authorizationList)) { | ||
AuthorizationListJSON = authorizationList | ||
const newAuthorizationList: AuthorizationListBytes = [] | ||
const jsonItems = ['chainId', 'address', 'nonce', 'yParity', 'r', 's'] |
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.
would just reading the properities and comparing them to undefined with a ||
be simpler ?
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'm not sure what you mean here?
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
🎉 |
] | ||
export type AuthorizationListBytes = AuthorizationListBytesItem[] | ||
export type AuthorizationList = AuthorizationListItem[] | ||
|
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 not a good reason that we have these AuthorizationList
types in the Common interface file (these should really be reserved for the VIIs, to the "Very Important Interfaces" 😂 used to type on other libraries.
Same goes I guess for the access list interfaces (for both: unless I am unaware of some below-tx-library usages).
We should move them over to tx in a small clean-up PR.
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 just checked, for both (so also AccessList
) the only packages which import those are tx and vm, and since vm has a tx dependency we can indeed move these into common.
I don't recall why we put AccessList in common (but to be consistent I also put AuthorizationList in here). I do slightly recall we maybe did this for package consumers to import these types (?) but then they would only need the common
package and not import tx
? However this does not sound like a very good reason for me, I don't know if there is any use case for those types which would not depend or use tx
.
We should indeed clean-up and remove both AuthorizationList
and AccessList
from common (in same PR)
r: ones32, | ||
s: ones32, | ||
}, | ||
], |
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.
These repeated test data lists make this whole test setup pretty expansive. Can we clean this up by taking a base authorization list item data set (or the full authorization list, whatever fits better), and then only overwrite the data item to test (can maybe be done in one PR togehter with the interface movings to tx, see above).
Guess that's something to //cc @jochem-brouwer in.
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.
Yes, that would be a better approach in general, will address.
{ | ||
authorizationList: [ | ||
{ | ||
chainId: '0x', |
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 valid? To have the chainId
set to 0x
??
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.
Respectively going further (without having read the EIP): shouldn't there be a check on our side if the chainId here matches the one from Common and otherwise (we) throw?
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.
Yes, 0x means it is valid on all chains. From the EIP:
2. Verify the chain id is either 0 or the chain’s current ID.
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, interesting.
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.
Ok, but then I guess at least Common (and therefore signed tx through chain ID in signature) and chain ID here differing should throw on the tx side?
Because atm it does not, tested with mainnet Common and chain ID 2.
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.
Had another look into the EIP, this (chain ID check) is only mentioned in execution, but would this make sense to allow such a tx in the network (so: in a block?).
If you haven't heard anything specific about that we should likely ask for confirmation/explanation/re-evaluation.
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.
Yes, it is thus a valid tx and it is thus allowed in the block, we should not throw. If the chain id does not match, we thus "skip" the tuple and do not invalidate the tx.
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.
What was the reasoning to allow this during the EIP discussion/specification?
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.
(or: was this a decision?)
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 know the exact reason, but I think it is among the lines just like with access lists, you are allowed to put the same address twice. This does not invalidate the tx, you just have to pay more gas (so no one wil do it). The same reasoning works here, you can input any authority tuple, but if it does not match the chain id it will do absolutely nothing, but you have to pay gas.
const chainIdBN = bytesToBigInt(chainId) | ||
if (chainIdBN !== BIGINT_0 && chainIdBN !== this.common.chainId()) { | ||
// Chain id does not match, continue | ||
continue |
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.
Ok, just seeing this here.
So I would interpret this (still without reading the EIP): txs with non-matching chain IDs are valid (could be included in mainnet), but execution just "does nothing" if not matching.
Is this correct/intended?
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.
Yes, from the EIP:
If any of the above steps fail, immediately stop processing that tuple and continue to the next tuple in the list.
The list contains point (2) the chain id check, it should either be 0 or the current chain id. If it thus does not match (and it is nonzero) then we continue processing, but it thus does not change any code. (Point 7 is changing the code of the authority).
Implements 7702 according to https://github.com/ethereum/EIPs/blob/14400434e1199c57d912082127b1d22643788d11/EIPS/eip-7702.md
Note that there are many open PRs in the EIP repo:
https://github.com/ethereum/EIPs/pulls?q=is%3Apr+is%3Aopen+7702
So, the spec is likely to change.
Some other items I found:
EOFCodeEIP7702Transaction extends FeeMarketEIP1559Transaction
is that the return types of some methods change, i.e. the new txs now have these mandatoryauthorityLists
fields, so we should likely think of something the same as how we have createdBaseTransaction
now (these new txs now also accept a return type, so we can "stack" the tx types)Still WIP, main things to do: