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

AA-161 validate Nonce in EntryPoint #247

Merged
merged 22 commits into from
Apr 9, 2023
Merged

AA-161 validate Nonce in EntryPoint #247

merged 22 commits into from
Apr 9, 2023

Conversation

drortirosh
Copy link
Contributor

@drortirosh drortirosh commented Mar 20, 2023

account doesn't check nonce. instead, entryPoint validate and increment nonce.
nonces are two-dimensional, with 192 bit of "key" and 64 bit of sequence.

For description about this change, please see this doc

drortirosh and others added 8 commits March 2, 2023 02:35
- NonceManager to handle 2d nonces
  - _validateAndUpdateNonce() called after validateUserOp
  - getNonce(sender, key)
- remove nonce checking from BaseAccount
- BaseAccount implements nonce() using ep.getNonce
contracts/core/NonceManager.sol Outdated Show resolved Hide resolved
@@ -511,6 +514,11 @@ contract EntryPoint is IEntryPoint, StakeManager {
uint256 gasUsedByValidateAccountPrepayment;
(uint256 requiredPreFund) = _getRequiredPrefund(mUserOp);
(gasUsedByValidateAccountPrepayment, validationData) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund);

if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed some pattern on old sample accounts that it skip nonce increment when initCode.length > 0.
I assume it's for gas savings, but I don't see it here. Is it intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was an internal optimization of SimpleWallet.

//increment nonce during construction.
// This way, the cost of moving from "0" to "1" is absorbed
// by first (construction) transaction, instead of during the 2nd transaction
entryPoint().incrementNonce(0);
Copy link
Contributor

@leekt leekt Mar 21, 2023

Choose a reason for hiding this comment

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

I know that this part is just a sample but just out of curiosity, since this will be called on construction and userOp.nonce will start with 1 as far as i know since validateUserOp will check for nonce after initialize. Is this a best practice?

And on my understanding, this nonce will be increased to 1 after validateUserOp even without this. Why bother going 0 -> 1 -> 2 for first userOp? instead of going 0 -> 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was done so that even wallets that are not created via a dedicated UserOp, will have their next operation gas "clean".
A wallet can be created by execution its initCode either by the UserOp, or by other means.
Calling "incrementNonce" the first time will cost 20000 gas, and any further call 5000 (I ignore for now explicitly setting to a new key, which is will cost 20000 again, but that an explicit operation a wallet owner does)
Not incrementing it during construction will mean first TX after construction will cost 20000 gas more than any further TX. We prefer to make gas-per-transaction as consistent as possible, and thus make only the construction transaction different.

Calling "incrementNonce" from the factory, indeed causes double-increment of the nonce, but the overhead is minimal (~400 gas)
The side-effect is indeed an extra increment of the nonce.

create account doesn't increment nonce automatically.
The nonce is incremented if it is created through a UserOp

Side-effect:
if account is created NOT through AA, then first UserOp will cost 20000
gas more.
(if created as a userop, then this extra cost is part of the "creation"
and not on a separate transaction.
function getNonce(address sender, uint192 key)
external view returns (uint256 nonce);

function incrementNonce(uint192 key) external;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add documentation, explaining that this function doesn't need to be called by the account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually, it is not needed at all. I just wonder if we should keep it for completeness, or remove it altogether..

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought it was needed for the "bypass" case where an account wants to support non-EntryPoint interactions and still use the same nonce. Is that no longer the case?

Copy link

Choose a reason for hiding this comment

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

I think one side-effect of this function is breaking continuity of nonce, that lose the 'one nonce corresponding to one OP' property.

For example, wallet can call _validateSignature in OP calldata to increase nonce by 2 in one single OP.

Also, wallet can call incrementNonce in _validateSignature, just before checking in _validateAndUpdateNonce, which makes it feasible to execute OP with nonce 3 when the next nonce is 2. So I suggest to check nonce _validateAndUpdateNonce before _validateSignature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The match of "one nonce to one OP" exists, but in any case, it can't be trusted as a counter: e.g. "current nonce is # of past transactions" is not true, because:

  • you can incrementNonce(), but also:
  • you can as many different key values, each with another sent of transactions.
  • accounts can support "batch", which means a single UserOp can map to multiple transactions.

In short, at the system level, nonces are nothing but replay protection..

contracts/samples/gnosis/EIP4337Manager.sol Outdated Show resolved Hide resolved
contracts/samples/gnosis/EIP4337Manager.sol Show resolved Hide resolved
@@ -345,11 +346,12 @@ contract EntryPoint is IEntryPoint, StakeManager {
/**
* Get counterfactual sender address.
* Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
* this method always revert, and returns the address in SenderAddressResult error
* this method always revert, and returns the address (and the nonce) in SenderAddressResult error
Copy link
Contributor

Choose a reason for hiding this comment

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

Not returning nonce anymore

@@ -174,7 +175,7 @@ interface IEntryPoint is IStakeManager {
/**
* Get counterfactual sender address.
* Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
* this method always revert, and returns the address in SenderAddressResult error
* this method always revert, and returns the address (and nonce) in SenderAddressResult error
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't return nonce

@@ -907,7 +920,7 @@ See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts`

## Security Considerations

The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-4337](./eip-4337.md). In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [EIP-4337](./eip-4337.md). In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we changed EIP to ERC

function getNonce(address sender, uint192 key)
external view returns (uint256 nonce);

function incrementNonce(uint192 key) external;
Copy link

Choose a reason for hiding this comment

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

I think one side-effect of this function is breaking continuity of nonce, that lose the 'one nonce corresponding to one OP' property.

For example, wallet can call _validateSignature in OP calldata to increase nonce by 2 in one single OP.

Also, wallet can call incrementNonce in _validateSignature, just before checking in _validateAndUpdateNonce, which makes it feasible to execute OP with nonce 3 when the next nonce is 2. So I suggest to check nonce _validateAndUpdateNonce before _validateSignature.

// (mainly so that during construction nonce can be made non-zero,
// to "absorb" the gas cost of first nonce increment to 1st transaction (construction),
// not to 2nd transaction)
function incrementNonce(uint192 key) public override {
Copy link

Choose a reason for hiding this comment

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

Wallet can call incrementNonce in _validateSignature, just before checking in _validateAndUpdateNonce, which makes it feasible to execute OP with nonce 3 when the next nonce is 2. So I suggest to check nonce _validateAndUpdateNonce before _validateSignature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that in make sense to modify the nonce before calling a user-provided method.
Still, our opcode rules prevent the account from calling the entryPoint (including incrementNonce) from within validation.

@drortirosh drortirosh changed the title validate Nonce in EntryPoint AA-161 validate Nonce in EntryPoint Apr 2, 2023
@drortirosh drortirosh merged commit 19918cd into develop Apr 9, 2023
@drortirosh drortirosh deleted the ep-nonce branch May 4, 2023 09:37
ptescher pushed a commit to BitskiCo/account-abstraction that referenced this pull request May 16, 2023
* EntryPoint to manage Nonce

- NonceManager to handle 2d nonces
   - _validateAndUpdateNonce() called after validateUserOp
   - getNonce(sender, key)

- remove nonce checking from BaseAccount
- BaseAccount implements getNonce() using ep.getNonce
ptescher pushed a commit to BitskiCo/account-abstraction that referenced this pull request Jun 14, 2023
* EntryPoint to manage Nonce

- NonceManager to handle 2d nonces
   - _validateAndUpdateNonce() called after validateUserOp
   - getNonce(sender, key)

- remove nonce checking from BaseAccount
- BaseAccount implements getNonce() using ep.getNonce
kaiix pushed a commit to kaiix/account-abstraction that referenced this pull request Nov 7, 2023
* EntryPoint to manage Nonce

- NonceManager to handle 2d nonces
   - _validateAndUpdateNonce() called after validateUserOp
   - getNonce(sender, key)

- remove nonce checking from BaseAccount
- BaseAccount implements getNonce() using ep.getNonce
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.

6 participants