-
Notifications
You must be signed in to change notification settings - Fork 20.5k
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
core/txpool/legacypool: add support for SetCode transactions #31073
Conversation
77301e7
to
b2ece7b
Compare
We have to add the SetCodeType support in the txpool, https://github.com/ethereum/go-ethereum/blob/master/core/txpool/legacypool/legacypool.go#L280 |
Hey @lightclient happy to see you working on this. While writing end-2-end tests for |
9e78130
to
84593a1
Compare
Co-authored-by: lightclient <[email protected]>
I’m fine with adding the restriction to the txpool, ensuring that delegation and normal transactions cannot coexist simultaneously. However, I’m curious about the typical transaction flow in real-world usage and unsure if it’s valid to first have a delegation and then follow it up with a normal transaction. In the legacyPool, we maintain a pendingNoncer to ensure the multiple inflight transactions from the same sender could be accepted and bundled in a single block. We can extend this noncer a bit by considering the auths as well in the future, to have more flexible/loose rules on txpool |
core/txpool/validation.go
Outdated
// Verify no authorizations will invalidate existing transactions known to | ||
// the pool. | ||
if opts.KnownConflicts != nil { | ||
if conflicts := opts.KnownConflicts(from, tx.Authorities()); len(conflicts) > 0 { |
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.
IIUC we only check authority conflicts between "normal" transactions and ignore blob transactions. I think thats fine, I did a benchmark back then which showed that our blobpool can handle removals and reorgs quite well. I will try to find that benchmark again to add it
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.
It probably makes sense to reserve all seen the tx authorities to the legacy pool to totally avoid this conflict?
The real world use case is almost certain:
It's possible there is a small cohort of users who self-sponsor to their delegated account, but in those cases we don't need to handle multiple pending txs from the account since they can batch their calls in a single tx.
We can see how usage plays out and if there is demand. Since it's possible to set a delegation for an account and in the same tx make batched calls on behalf, I don't see a reason that user would want to stack a tx on a delegation. |
// KnownConflicts is an optional callback which iterates over the list of | ||
// addresses and returns all addresses known to the pool with in-flight | ||
// transactions. | ||
KnownConflicts func(sender common.Address, authorizers []common.Address) []common.Address |
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.
should this add to blobTxPool too?
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.
An authority account still can send blobTxs even if he has a conflict nonce in the legacyPool.
I think you can add a checkConflict
method to subpools, and move the check to txpool.ValidateTransaction
core/txpool/legacypool/legacypool.go
Outdated
// The transaction sender cannot have an in-flight authorization. | ||
if _, ok := pool.all.auths[from]; ok { | ||
conflicts = append(conflicts, from) | ||
} |
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.
If I may ask simply out of curiosity, what is the reason for this rule?
It seemed to me that an "in-flight" (meaning "currently in the mempool", right?) authorization is not very different from an authorization already on-chain. At least, not in how it can suddently invalidate some "legacy" transaction from the mempool.
And if I understand this code correctly, at this point the authorizations in pool.all.auths
are not even checked to have a valid nonce, so even an invalid authorization in a mempool prevents a "legacy" transaction from entering the mempool.
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 attack I was trying to avoid here is many EOA accounts send the max number of pending txs possible and all of those accounts get invalidated with a single 7702 tx. There also doesn't seem to be a good reason for an honest user to have both a pending delegation and an unrelated pending tx originating from their account.
This policy is probably too draconian and can be used to deny service from regular users. Maybe a better thing to do is verify the auths as they enter the pool, then when txs are included on chain, purge all pending txs with that auth.
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 ended up relaxing the requirement slightly so accounts with a pending delegation can originate 1 tx. This matches what is offered for accounts with deployed delegations while avoiding the costly state lookup for every account in a 7702 tx.
1394d6d
to
2dc72d5
Compare
I think here should check the continuity of nonces index d11e9c0b5..30fa3a2b1 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -227,6 +231,40 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
if next > tx.Nonce() {
return fmt.Errorf("%w: next nonce %v, tx nonce %v", core.ErrNonceTooLow, next, tx.Nonce())
}
+ // If the transaction is of type SetCodeTxType, validate the authorizations
+ if tx.Type() == types.SetCodeTxType {
+ // Initialize a map to track nonces for each authority
+ nonces := map[common.Address]uint64{from: tx.Nonce()}
+ // Iterate through each authorization in the transaction
+ for _, auth := range tx.SetCodeAuthorizations() {
+ // Check if the chain ID in the authorization matches the signer's chain ID
+ if auth.ChainID.CmpBig(signer.ChainID()) != 0 && !auth.ChainID.IsZero() {
+ return core.ErrAuthorizationWrongChainID
+ }
+ // Retrieve the authority address from the authorization
+ authority, err := auth.Authority()
+ if err != nil {
+ return errors.Join(core.ErrAuthorizationInvalidSignature, err)
+ }
+ // Validate the nonce in the authorization
+ if nonce, ok := nonces[authority]; ok {
+ if auth.Nonce != nonce+1 {
+ return fmt.Errorf("invalid nonce in authorization for %s: expect %d but got %d", authority, nonce+1, auth.Nonce)
+ }
+ } else {
+ // Retrieve the next expected nonce for the authority from the state
+ next := opts.State.GetNonce(authority)
+ if next != auth.Nonce {
+ return fmt.Errorf("invalid nonce in authorization for %s: expect %d but got %d", authority, next, auth.Nonce)
+ }
+ // Verify the code of authority is either empty or already delegated
+ code := opts.State.GetCode(authority)
+ if _, ok := types.ParseDelegation(code); len(code) != 0 && !ok {
+ return core.ErrAuthorizationDestinationHasCode
+ }
+ }
+ // Update the nonce for the authority in the map
+ nonces[authority] = auth.Nonce
+ }
+ }
// Ensure the transaction doesn't produce a nonce gap in pools that do not
// support arbitrary orderings
if opts.FirstNonceGap != nil { |
It's not necessary to validate authorization nonces in the txpool. Authorizations are pre-paid by the fee of the transaction, and their validity will be checked when applying the transaction in a block (invalid authorizations will be ignored). In the txpool, we do not care whether transactions 'work' or not, we just care about whether the fees are high enough and the sender account can cover them. |
But this invalid transaction will remain in txpool and be propagated by p2p until timeout |
That's not true. The transaction can be applied anyway. Since invalid authorizations are ignored when processing a SetCode transaction, the transaction itself is valid even if it contains authorizations with wrong nonce. go-ethereum/core/state_transition.go Lines 493 to 497 in d11e9c0
|
The problem with validating authorizations in txpool is that it's too expensive. We'd have to access the state for each authorization, and there can be many of them in a single transaction. These accesses are not paid for by the transaction (the gas fee only covers state access in block), so if the transaction turns out invalid or gets removed from the pool, we did all these checks for nothing. It'd be a griefing vector. |
thanks for the head up, that's a good design. |
got |
@@ -565,6 +567,11 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error { | |||
if list := pool.queue[addr]; list != nil { | |||
have += list.Len() | |||
} | |||
if pool.currentState.GetCodeHash(addr) != types.EmptyCodeHash || len(pool.all.auths[addr]) != 0 { |
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.
Do you think len(pool.all.auths[addr]) != 0
could become an attack vector? Consider the following scenario:
- An authorization w is created and included in transaction x.
- An attacker copies the same authorization w and includes it in a new transaction y, submitting y to the network with a relatively lower gas price.
- If a node receives transaction y first, it will reject transaction x.
- Furthermore, if x contains multiple authorizations, constructing and submitting a conflicting transaction like y could effectively prevent the entire authorization list in x from being included.
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.
In this case, the len(pool.all.auths[addr]) != 0
is used to limit the number of txs an account can queue when they have the ability to sweep their balance from underneath the txpool w/o originating a tx to do so. Without this check, what is possible:
A submits a tx which sets code in B. During the tx, B sweeps its balance. Immediately after A submits tx1, B submits 16 txs to the pool. When A is mined first, it invalidates all txs from B. You can expand the A's tx to include many accounts, now A can invalidate many txs from many accounts.
By only allowing a single tx from accounts who have code / will potentially have code soon, we can reduce the potency of this attack.
Originally I blocked this attack, by not allowing a tx from an account with a pending delegation, but this leads to the attack Gary described. Now that we allow 1 tx from the account with a pending delegation, we can avoid that account getting deadlocked.
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.
In this example, X and Y are from different senders, say A an B respectively. Even if X is from A and includes the delegation for A, when B copies the auth and puts it in Y, the node will still accept X because although it has a pending delegation, it will still have 1 free slot for the account A. This does mean B can limit the number of txs A can queue.
The only scenario the tx will be rejected is as follows: suppose A submits an auth to bundler B. Say B submits the auth in tx Y. If A is able to queue a tx X before nodes start recieving Y then Y will be rejected.
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.
A submits a tx which sets code in B. During the tx, B sweeps its balance. Immediately after A submits tx1, B submits 16 txs to the pool. When A is mined first, it invalidates all txs from B. You can expand the A's tx to include many accounts, now A can invalidate many txs from many accounts.
Will this have a serious impact? I think we have the similar issues nowadays in the legacy pool. Let's say account a has 16 transactions with continuous nonces. If the first transaction drains the balance of A and the following 15 transactions will be invalidated later.
In the scenario you mentioned, tx can set code for a list of accounts [B, C, D, ..., Z] and meantime these accounts can submit a tons of pending transactions. However, these accounts must sign an auth in which the balance will be drained by the code. Although the scale of this invalidation seems to be magnified, but it should be feasible to submit legacy pending transactions from multiple accounts and invalidate the remaining by using balance in the first tx?
Maybe I miss 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.
I'm not sure if I understand this. The quote you have here was me explaining an attack that we avoid by limiting the account to 1 pending tx? I don't think you can easily replace all the txs pending for an account because we check the total value of the stack?
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 you can easily replace all the txs pending for an account because we check the total value of the stack?
Yeah right. There is no way to drain the balance of an EOA without the deployed code!
…m#31073) The new SetCode transaction type introduces some additional complexity when handling the transaction pool. This complexity stems from two new account behaviors: 1. The balance and nonce of an account can change during regular transaction execution *when they have a deployed delegation*. 2. The nonce and code of an account can change without any EVM execution at all. This is the "set code" mechanism introduced by EIP-7702. The first issue has already been considered extensively during the design of ERC-4337, and we're relatively confident in the solution of simply limiting the number of in-flight pending transactions an account can have to one. This puts a reasonable bound on transaction cancellation. Normally to cancel, you would need to spend 21,000 gas. Now it's possible to cancel for around the cost of warming the account and sending value (`2,600+9,000=11,600`). So 50% cheaper. The second issue is more novel and needs further consideration. Since authorizations are not bound to a specific transaction, we cannot drop transactions with conflicting authorizations. Otherwise, it might be possible to cherry-pick authorizations from txs and front run them with different txs at much lower fee amounts, effectively DoSing the authority. Fortunately, conflicting authorizations do not affect the underlying validity of the transaction so we can just accept both. --------- Co-authored-by: Marius van der Wijden <[email protected]> Co-authored-by: Felix Lange <[email protected]>
The new SetCode transaction type introduces some additional complexity when handling the transaction pool.
This complexity stems from two new account behaviors:
The first issue has already been considered extensively during the design of ERC-4337, and we're relatively confident in the solution of simply limiting the number of in-flight pending transactions an account can have to one. This puts a reasonable bound on transaction cancellation. Normally to cancel, you would need to spend 21,000 gas. Now it's possible to cancel for around the cost of warming the account and sending value (
2,600+9,000=11,600
). So 50% cheaper.The second issue is more novel and needs further consideration.
There are a few issues we must carefully design around:
transactions with conflicting authorizations. Otherwise, it might be possible to cherry-pick authorizations from txs and front run them with different txs at much lower fee amounts, effectively DoSing the authority. Fortunately, conflicting
authorizations do not affect the underlying validity of the transaction so we can just accept both.
A few situations to test:
the tx are removed.
5.1. This should also work when a self-sponsored setcode tx attempts
to replace itself.
the pool, otherwise someone might submit old validations to deny
service to unsuspecting users.
maybe try to write out each possible scenario for all types of txs entering the
pool +different 7702 situations and consider outcomes?