Fix code delegation issue found in pectra devnet (#8203)

* add acceptance tests to verify that code delegations are persisted even if the tx itself fails
* set node to use bonsai, set correct nonce for code delegation, make sure revert in contract has the 2 necessary items on the stack
* Hive test fix: only add account if authority nonce is valid (0)
---------

Signed-off-by: Karim Taam <karim.t2am@gmail.com>
Signed-off-by: Daniel Lehrner <daniel.lehrner@consensys.net>
Signed-off-by: Simon Dudley <simon.dudley@consensys.net>
Co-authored-by: Daniel Lehrner <daniel.lehrner@consensys.net>
Co-authored-by: Simon Dudley <simon.dudley@consensys.net>
This commit is contained in:
Karim Taam
2025-01-31 06:42:13 +01:00
committed by GitHub
parent 97a69b03c8
commit 68665db454
12 changed files with 287 additions and 5 deletions

View File

@@ -532,6 +532,7 @@ public class BesuNodeFactory {
.jsonRpcTxPool()
.engineRpcEnabled(true)
.jsonRpcDebug()
.dataStorageConfiguration(DataStorageConfiguration.DEFAULT_BONSAI_CONFIG)
.build());
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.transaction.eth;
import static org.assertj.core.api.Assertions.assertThat;
import static org.web3j.protocol.core.DefaultBlockParameterName.LATEST;
import org.hyperledger.besu.tests.acceptance.dsl.account.Account;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
import java.io.IOException;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.protocol.core.methods.response.EthGetCode;
public class EthGetCodeTransaction implements Transaction<Bytes> {
private final Account account;
public EthGetCodeTransaction(final Account account) {
this.account = account;
}
@Override
public Bytes execute(final NodeRequests node) {
try {
final EthGetCode result = node.eth().ethGetCode(account.getAddress(), LATEST).send();
assertThat(result).isNotNull();
assertThat(result.hasError()).isFalse();
return Bytes.fromHexString(result.getCode());
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -43,6 +43,10 @@ public class EthTransactions {
return new EthGetBalanceTransaction(account);
}
public EthGetCodeTransaction getCode(final Account account) {
return new EthGetCodeTransaction(account);
}
public EthGetBalanceAtBlockTransaction getBalanceAtBlock(
final Account account, final BigInteger block) {
return new EthGetBalanceAtBlockTransaction(account, block);

View File

@@ -46,6 +46,9 @@ public class CodeDelegationTransactionAcceptanceTest extends AcceptanceTestBase
public static final Address SEND_ALL_ETH_CONTRACT_ADDRESS =
Address.fromHexStringStrict("0000000000000000000000000000000000009999");
public static final Address ALWAYS_REVERT_CONTRACT_ADDRESS =
Address.fromHexStringStrict("0000000000000000000000000000000000000666");
private final Account authorizer =
accounts.createAccount(
Address.fromHexStringStrict("8da48afC965480220a3dB9244771bd3afcB5d895"));
@@ -232,4 +235,124 @@ public class CodeDelegationTransactionAcceptanceTest extends AcceptanceTestBase
assertThat(otherAccountBalanceAfterFirstTx.add(BigInteger.ONE))
.isEqualTo(otherAccountBalanceAfterSecondTx);
}
/**
* EIP-7702 code delegation should be persisted even if the transaction that contains the
* authorization is reverted.
*/
@Test
public void shouldPersistCodeDelegationAfterRevert() throws IOException {
final long GAS_LIMIT = 1_000_000L;
// check the authorizer has no code before the transaction
final Bytes authorizerCodeBeforeCodeDelegation =
besuNode.execute(ethTransactions.getCode(authorizer));
assertThat(authorizerCodeBeforeCodeDelegation).isEqualTo(Bytes.EMPTY);
// valid 7702 code delegation to SEND_ALL_ETH_CONTRACT_ADDRESS
final CodeDelegation codeDelegation =
org.hyperledger.besu.ethereum.core.CodeDelegation.builder()
.chainId(BigInteger.valueOf(20211))
.nonce(0L)
.address(SEND_ALL_ETH_CONTRACT_ADDRESS)
.signAndBuild(
secp256k1.createKeyPair(
secp256k1.createPrivateKey(AUTHORIZER_PRIVATE_KEY.toUnsignedBigInteger())));
// the transaction will revert, because the to address is a contract that always reverts
final Transaction tx =
Transaction.builder()
.type(TransactionType.DELEGATE_CODE)
.chainId(BigInteger.valueOf(20211))
.nonce(0)
.maxPriorityFeePerGas(Wei.of(1_000_000_000))
.maxFeePerGas(Wei.fromHexString("0x02540BE400"))
.gasLimit(GAS_LIMIT)
.to(ALWAYS_REVERT_CONTRACT_ADDRESS)
.value(Wei.ZERO)
.payload(Bytes.EMPTY)
.codeDelegations(List.of(codeDelegation))
.signAndBuild(
secp256k1.createKeyPair(
secp256k1.createPrivateKey(
TRANSACTION_SPONSOR_PRIVATE_KEY.toUnsignedBigInteger())));
// include the tx in the next block
final String txHash =
besuNode.execute(ethTransactions.sendRawTransaction(tx.encoded().toHexString()));
testHelper.buildNewBlock();
// check that the transaction was included and has indeed reverted
Optional<TransactionReceipt> maybeTransactionReceipt =
besuNode.execute(ethTransactions.getTransactionReceipt(txHash));
assertThat(maybeTransactionReceipt).isPresent();
assertThat(maybeTransactionReceipt.get().getStatus()).isEqualTo("0x0");
// check the authorizer has the code delegation after the transaction even though it has
// reverted
final Bytes expectedCode =
Bytes.concatenate(Bytes.fromHexString("ef0100"), SEND_ALL_ETH_CONTRACT_ADDRESS);
final Bytes authorizerCode = besuNode.execute(ethTransactions.getCode(authorizer));
assertThat(authorizerCode).isEqualTo(expectedCode);
}
/**
* EIP-7702 code delegation should be persisted even if the transaction that contains the
* authorization is reverted and the transaction sender is the same as the code delegation
* authorizer.
*/
@Test
public void shouldPersistCodeDelegationAfterRevertWhenSelfSponsored() throws IOException {
final long GAS_LIMIT = 1_000_000L;
// check the authorizer has no code before the transaction
final Bytes authorizerCodeBeforeCodeDelegation =
besuNode.execute(ethTransactions.getCode(authorizer));
assertThat(authorizerCodeBeforeCodeDelegation).isEqualTo(Bytes.EMPTY);
// valid 7702 code delegation to SEND_ALL_ETH_CONTRACT_ADDRESS
final CodeDelegation codeDelegation =
org.hyperledger.besu.ethereum.core.CodeDelegation.builder()
.chainId(BigInteger.valueOf(20211))
.nonce(1L)
.address(SEND_ALL_ETH_CONTRACT_ADDRESS)
.signAndBuild(
secp256k1.createKeyPair(
secp256k1.createPrivateKey(AUTHORIZER_PRIVATE_KEY.toUnsignedBigInteger())));
// the transaction will revert, because the to address is a contract that always reverts
final Transaction tx =
Transaction.builder()
.type(TransactionType.DELEGATE_CODE)
.chainId(BigInteger.valueOf(20211))
.nonce(0)
.maxPriorityFeePerGas(Wei.of(1_000_000_000))
.maxFeePerGas(Wei.fromHexString("0x02540BE400"))
.gasLimit(GAS_LIMIT)
.to(ALWAYS_REVERT_CONTRACT_ADDRESS)
.value(Wei.ZERO)
.payload(Bytes.EMPTY)
.codeDelegations(List.of(codeDelegation))
.signAndBuild(
secp256k1.createKeyPair(
secp256k1.createPrivateKey(AUTHORIZER_PRIVATE_KEY.toUnsignedBigInteger())));
// include the tx in the next block
final String txHash =
besuNode.execute(ethTransactions.sendRawTransaction(tx.encoded().toHexString()));
testHelper.buildNewBlock();
// check that the transaction was included and has indeed reverted
Optional<TransactionReceipt> maybeTransactionReceipt =
besuNode.execute(ethTransactions.getTransactionReceipt(txHash));
assertThat(maybeTransactionReceipt).isPresent();
assertThat(maybeTransactionReceipt.get().getStatus()).isEqualTo("0x0");
// check the authorizer has the code delegation after the transaction even though it has
// reverted
final Bytes expectedCode =
Bytes.concatenate(Bytes.fromHexString("ef0100"), SEND_ALL_ETH_CONTRACT_ADDRESS);
final Bytes authorizerCode = besuNode.execute(ethTransactions.getCode(authorizer));
assertThat(authorizerCode).isEqualTo(expectedCode);
}
}

View File

@@ -57,6 +57,13 @@
"privateKey": "11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1",
"balance": "90000000000000000000000"
},
"0x0000000000000000000000000000000000000666": {
"comment": "Contract reverts immediately when called",
"balance": "0",
"code": "5F5FFD",
"codeDecompiled": "PUSH0 PUSH0 REVERT",
"storage": {}
},
"0x0000000000000000000000000000000000009999": {
"comment": "Contract sends all its Ether to the address provided as a call data.",
"balance": "0",

View File

@@ -272,4 +272,20 @@ public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelega
return new CodeDelegation(chainId, address, nonce, signature);
}
}
@Override
public String toString() {
return "CodeDelegation{"
+ "chainId="
+ chainId
+ ", address="
+ address
+ ", nonce="
+ nonce
+ ", signature="
+ signature
+ ", authorizerSupplier="
+ authorizerSupplier
+ '}';
}
}

View File

@@ -192,6 +192,7 @@ public abstract class AbstractBlockProcessor implements BlockProcessor {
}
blockUpdater.commit();
blockUpdater.markTransactionBoundary();
currentGasUsed += transaction.getGasLimit() - transactionProcessingResult.getGasRemaining();
if (transaction.getVersionedHashes().isPresent()) {

View File

@@ -126,6 +126,10 @@ public class CodeDelegationProcessor {
MutableAccount authority;
boolean authorityDoesAlreadyExist = false;
if (maybeAuthorityAccount.isEmpty()) {
// only create an account if nonce is valid
if (codeDelegation.nonce() != 0) {
return;
}
authority = evmWorldUpdater.createAccount(authorizer.get());
} else {
authority = maybeAuthorityAccount.get();
@@ -146,7 +150,7 @@ public class CodeDelegationProcessor {
}
if (authorityDoesAlreadyExist) {
result.incremenentAlreadyExistingDelegators();
result.incrementAlreadyExistingDelegators();
}
evmWorldUpdater

View File

@@ -27,7 +27,7 @@ public class CodeDelegationResult {
accessedDelegatorAddresses.add(address);
}
public void incremenentAlreadyExistingDelegators() {
public void incrementAlreadyExistingDelegators() {
alreadyExistingDelegators += 1;
}

View File

@@ -499,8 +499,6 @@ public abstract class DiffBasedWorldStateUpdateAccumulator<ACCOUNT extends DiffB
tracked.setStorageWasCleared(false); // storage already cleared for this transaction
}
});
getUpdatedAccounts().clear();
getDeletedAccounts().clear();
}
@Override
@@ -598,6 +596,21 @@ public abstract class DiffBasedWorldStateUpdateAccumulator<ACCOUNT extends DiffB
return results;
}
/**
* Marks the boundary of a transaction by clearing tracking collections.
*
* <p>These tracking collections store changes made during the transaction. After committing the
* transaction, they become unnecessary and can be safely cleared.
*
* <p>Note: If the transaction is not committed before this method is called, any uncommitted
* changes will be lost.
*/
@Override
public void markTransactionBoundary() {
getUpdatedAccounts().clear();
getDeletedAccounts().clear();
}
@Override
public boolean isModifyingHeadWorldState() {
return true;

View File

@@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -112,6 +113,23 @@ class CodeDelegationProcessorTest {
verify(delegationCodeService).processCodeDelegation(authority, DELEGATE_ADDRESS);
}
@Test
void shouldNotCreateAccountIfNonceIsInvalid() {
// Arrange
CodeDelegation codeDelegation = createCodeDelegation(CHAIN_ID, 1L);
when(transaction.getCodeDelegationList()).thenReturn(Optional.of(List.of(codeDelegation)));
when(worldUpdater.getAccount(any())).thenReturn(null);
// Act
CodeDelegationResult result = processor.process(worldUpdater, transaction);
// Assert
assertThat(result.alreadyExistingDelegators()).isZero();
verify(worldUpdater, never()).createAccount(any());
verify(authority, never()).incrementNonce();
verify(delegationCodeService, never()).processCodeDelegation(authority, DELEGATE_ADDRESS);
}
@Test
void shouldProcessValidDelegationForExistingAccount() {
// Arrange
@@ -150,6 +168,51 @@ class CodeDelegationProcessorTest {
verify(delegationCodeService, never()).processCodeDelegation(any(), any());
}
@Test
void shouldSkipOverInvalidMultipleInvalidNonceDelegationsForSameAuthorityForNewAccount() {
// Arrange
when(worldUpdater.codeDelegationService()).thenReturn(delegationCodeService);
var signature1 = new SECPSignature(BigInteger.ONE, BigInteger.ONE, (byte) 0);
long cd1_invalidNonce = 2L;
var cd1_invalid =
new org.hyperledger.besu.ethereum.core.CodeDelegation(
CHAIN_ID,
Address.fromHexString("0x0000000000000000000000000000000000001000"),
cd1_invalidNonce,
signature1);
var signature2 = new SECPSignature(BigInteger.TWO, BigInteger.TWO, (byte) 0);
final long cd2_validNonce = 0L;
var cd2_valid =
new org.hyperledger.besu.ethereum.core.CodeDelegation(
CHAIN_ID,
Address.fromHexString("0x0000000000000000000000000000000000001100"),
cd2_validNonce,
signature2);
var signature3 = new SECPSignature(BigInteger.TWO, BigInteger.TWO, (byte) 0);
final long cd3_invalidNonce = 0L;
var cd3_invalid =
new org.hyperledger.besu.ethereum.core.CodeDelegation(
CHAIN_ID,
Address.fromHexString("0x0000000000000000000000000000000000001200"),
cd3_invalidNonce,
signature3);
when(transaction.getCodeDelegationList())
.thenReturn(Optional.of(List.of(cd1_invalid, cd2_valid, cd3_invalid)));
when(worldUpdater.getAccount(any())).thenReturn(null).thenReturn(null).thenReturn(authority);
when(worldUpdater.createAccount(any())).thenReturn(authority);
when(authority.getNonce()).thenReturn(0L).thenReturn(1L);
when(delegationCodeService.canSetCodeDelegation(any())).thenReturn(true);
// Act
CodeDelegationResult result = processor.process(worldUpdater, transaction);
// Assert
assertThat(result.alreadyExistingDelegators()).isZero();
verify(authority, times(1)).incrementNonce();
verify(delegationCodeService, times(1)).processCodeDelegation(any(), any());
}
@Test
void shouldRejectDelegationWithSGreaterThanHalfCurveOrder() {
// Arrange

View File

@@ -59,7 +59,7 @@ public class CodeDelegationService {
}
/**
* Returns if the provided account has either no code set or has already delegated code.
* Returns true if the provided account has either no code set or has already delegated code.
*
* @param account the account to check.
* @return {@code true} if the account can set delegated code, {@code false} otherwise.