Fix: CodeDelegation account clearing side-effect issue#9854
Fix: CodeDelegation account clearing side-effect issue#9854macfarla merged 3 commits intohyperledger:mainfrom
Conversation
…lidation Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com>
There was a problem hiding this comment.
Pull request overview
Fixes a side-effect where reading an authority account during code delegation validation could “touch” it and cause empty pre-existing accounts to be cleared later, changing the post-state.
Changes:
- Switches validation-only account reads from
worldUpdater.getAccount()to side-effect-freeworldUpdater.get(). - Reorders validation to only call
getAccount()after all checks pass and mutation is required. - Updates tests to mock/verify
get()vsgetAccount()usage on accept/reject paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/CodeDelegationProcessor.java | Avoids marking accounts as touched during validation by using get() and deferring getAccount() until mutation. |
| ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/CodeDelegationProcessorTest.java | Adjusts mocks and adds assertions to ensure getAccount() isn’t invoked on rejection paths. |
Comments suppressed due to low confidence (2)
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/CodeDelegationProcessor.java:151
maybeExistingAccount.get()is dereferenced multiple times andgetNonce()is called twice, which makes the logic noisier than necessary. Consider assigningmaybeExistingAccount.get()to a local (e.g.,existingAccount) and reusing a singleexistingNoncefor the comparison/logging.
if (!codeDelegationService.canSetCodeDelegation(maybeExistingAccount.get())) {
return;
}
if (codeDelegation.nonce() != maybeExistingAccount.get().getNonce()) {
LOG.trace(
"Invalid nonce for code delegation. Expected: {}, Actual: {}",
maybeExistingAccount.get().getNonce(),
codeDelegation.nonce());
return;
}
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/CodeDelegationProcessor.java:125
- This comment references a specific downstream cleanup method (
clearAccountsThatAreEmpty()), which is an implementation detail outside this class. Consider rephrasing to document the local contract/intent (e.g., “useget()for validation-only reads becausegetAccount()tracks updates/touches accounts”) without tying the behavior to a particular external method name.
// Use read-only get() to avoid marking the account as touched during validation.
// getAccount() would mark it as touched, causing empty accounts to be incorrectly
// deleted by clearAccountsThatAreEmpty() even when authorization is invalid/skipped.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
running the reproduction instructions from the issue #9840 |
|
running some test mainnet nodes with this branch |
daniellehrner
left a comment
There was a problem hiding this comment.
Looks good, this should solve the issue
PR description
The problem is in CodeDelegationProcessor.java:122-123:
final Optional maybeAuthorityAccount =
Optional.ofNullable(worldUpdater.getAccount(authorizer.get()));
getAccount() returns a mutable account and calls track() in AbstractWorldUpdater, which adds the account to the updatedAccounts map — effectively marking it as touched.
Later, in MainnetTransactionProcessor.java:494-496, clearAccountsThatAreEmpty() iterates all touched accounts and deletes any that are empty. So a pre-existing empty authority account that was only read for validation gets deleted, changing the
post-state trie root.
The Fix
The WorldUpdater already has two account access methods:
The fix is to use get() for the initial validation read, and only call getAccount() when we've passed validation and actually need to mutate the account.
Changes Made
CodeDelegationProcessor.java (the fix):
CodeDelegationProcessorTest.java (test updates):
Fixed Issue(s)
fixes #9840
Thanks for sending a pull request! Have you done the following?
doc-change-requiredlabel to this PR if updates are required.Locally, you can run these tests to catch failures early:
./gradlew spotlessApply./gradlew build./gradlew acceptanceTest./gradlew integrationTest./gradlew ethereum:referenceTests:referenceTests