diff --git a/CHANGELOG.md b/CHANGELOG.md index 795147ccb34..57195e8cb08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Additions and Improvements - Retrieve all transaction receipts for a block in one request [#6646](https://github.com/hyperledger/besu/pull/6646) +- Add support for EIP-7702 transaction in the txpool [#8018](https://github.com/hyperledger/besu/pull/8018) [#7984](https://github.com/hyperledger/besu/pull/7984) ### Bug fixes - Fix serialization of state overrides when `movePrecompileToAddress` is present [#8204](https://github.com/hyperledger/besu/pull/8024) diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java index 5cef826ed68..08c1da71012 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java @@ -14,24 +14,35 @@ */ package org.hyperledger.besu.ethereum.core; +import org.hyperledger.besu.crypto.CodeDelegationSignature; import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; import org.hyperledger.besu.datatypes.AccessListEntry; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.BlobsWithCommitments; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.VersionedHash; import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.encoding.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import java.math.BigInteger; import java.util.List; import java.util.Optional; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; public class TransactionTestFixture { - private final SECPSignature signature = - new SECPSignature(BigInteger.ONE, BigInteger.ONE, (byte) 0); + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final KeyPair KEY_PAIR = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final org.hyperledger.besu.datatypes.CodeDelegation CODE_DELEGATION = + createSignedCodeDelegation(BigInteger.ZERO, Address.ZERO, 0, KEY_PAIR); + private TransactionType transactionType = TransactionType.FRONTIER; private long nonce = 0; @@ -100,9 +111,7 @@ public Transaction createTransaction(final KeyPair keys) { builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500))); builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000))); builder.accessList(accessListEntries.orElse(List.of())); - builder.codeDelegations( - codeDelegations.orElse( - List.of(new CodeDelegation(chainId.get(), sender, 0, signature)))); + builder.codeDelegations(codeDelegations.orElse(List.of(CODE_DELEGATION))); break; } @@ -196,7 +205,28 @@ public TransactionTestFixture blobsWithCommitments(final Optional codeDelegations) { - this.codeDelegations = Optional.of(codeDelegations); + this.codeDelegations = Optional.ofNullable(codeDelegations); return this; } + + public static CodeDelegation createSignedCodeDelegation( + final BigInteger chainId, final Address address, final long nonce, final KeyPair keys) { + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature( + new org.hyperledger.besu.ethereum.core.CodeDelegation(chainId, address, nonce, null), + rlpOutput); + + final Hash hash = + Hash.hash( + Bytes.concatenate( + org.hyperledger.besu.ethereum.core.CodeDelegation.MAGIC, rlpOutput.encoded())); + + final var signature = SIGNATURE_ALGORITHM.get().sign(hash, keys); + + return new org.hyperledger.besu.ethereum.core.CodeDelegation( + chainId, + address, + nonce, + CodeDelegationSignature.create(signature.getR(), signature.getS(), signature.getRecId())); + } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java index e6a84f69fa4..43ec525e4af 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java @@ -430,7 +430,7 @@ public interface MemorySize { int ACCESS_LIST_ENTRY_SHALLOW_SIZE = 248; int OPTIONAL_ACCESS_LIST_SHALLOW_SIZE = 40; int OPTIONAL_CODE_DELEGATION_LIST_SHALLOW_SIZE = 40; - int CODE_DELEGATION_ENTRY_SIZE = 472; + int CODE_DELEGATION_ENTRY_SIZE = 520; int VERSIONED_HASH_SIZE = 96; int LIST_SHALLOW_SIZE = 48; int OPTIONAL_SHALLOW_SIZE = 16; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java index bde1568e983..4622b9a997f 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java @@ -433,7 +433,7 @@ public OptionalLong getNextNonceForSender(final Address sender) { } @Override - public synchronized void manageBlockAdded( + public void manageBlockAdded( final BlockHeader blockHeader, final List confirmedTransactions, final List reorgTransactions, @@ -447,19 +447,21 @@ public synchronized void manageBlockAdded( final var reorgNonceRangeBySender = nonceRangeBySender(reorgTransactions); - try { - prioritizedTransactions.blockAdded(feeMarket, blockHeader, maxConfirmedNonceBySender); - } catch (final Throwable throwable) { - LOG.warn( - "Unexpected error {} when managing added block {}, maxNonceBySender {}, reorgNonceRangeBySender {}", - throwable, - blockHeader.toLogString(), - maxConfirmedNonceBySender, - reorgTransactions); - LOG.warn("Stack trace", throwable); - } + synchronized (this) { + try { + prioritizedTransactions.blockAdded(feeMarket, blockHeader, maxConfirmedNonceBySender); + } catch (final Throwable throwable) { + LOG.warn( + "Unexpected error {} when managing added block {}, maxNonceBySender {}, reorgNonceRangeBySender {}", + throwable, + blockHeader.toLogString(), + maxConfirmedNonceBySender, + reorgTransactions); + LOG.warn("Stack trace", throwable); + } - logBlockHeaderForReplay(blockHeader, maxConfirmedNonceBySender, reorgNonceRangeBySender); + logBlockHeaderForReplay(blockHeader, maxConfirmedNonceBySender, reorgNonceRangeBySender); + } } private void logBlockHeaderForReplay( @@ -498,10 +500,25 @@ private void logBlockHeaderForReplay( } private Map maxNonceBySender(final List confirmedTransactions) { + record SenderNonce(Address sender, long nonce) {} + return confirmedTransactions.stream() + .mapMulti( + (transaction, consumer) -> { + // always consider the sender + consumer.accept(new SenderNonce(transaction.getSender(), transaction.getNonce())); + + // and if a code delegation tx also the authorities + if (transaction.getType().supportsDelegateCode()) { + transaction.getCodeDelegationList().get().stream() + .map(cd -> cd.authorizer().map(address -> new SenderNonce(address, cd.nonce()))) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(consumer); + } + }) .collect( - groupingBy( - Transaction::getSender, mapping(Transaction::getNonce, reducing(0L, Math::max)))); + groupingBy(SenderNonce::sender, mapping(SenderNonce::nonce, reducing(0L, Math::max)))); } private Map nonceRangeBySender( diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java index 393e97608e5..a2646b9321a 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java @@ -140,7 +140,7 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo @Test public void toSize() { TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0); + prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0, null); Transaction txTo = preparedTx.to(Optional.of(Address.extract(Bytes32.random()))).createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); @@ -187,7 +187,7 @@ public void toSize() { public void payloadSize() { TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0); + prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0, null); Transaction txPayload = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txPayload.writeTo(rlpOut); @@ -277,7 +277,7 @@ private void blobsWithCommitmentsFieldSize( final long containerSize, final long itemSize) { TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), Wei.of(50), 10, 1); + prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), Wei.of(50), 10, 1, null); Transaction txBlob = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION); @@ -309,7 +309,7 @@ private void blobsWithCommitmentsFieldSize( @Test public void blobsWithCommitmentsSize() { TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), Wei.of(50), 10, 1); + prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), Wei.of(50), 10, 1, null); Transaction txBlob = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION); @@ -337,7 +337,7 @@ public void blobsWithCommitmentsSize() { public void pendingTransactionSize() { TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0); + prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), Wei.ZERO, 10, 0, null); Transaction txPayload = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txPayload.writeTo(rlpOut); @@ -369,7 +369,7 @@ public void accessListSize() { final List ales = List.of(ale1); TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.ACCESS_LIST, 0, Wei.of(500), Wei.ZERO, 0, 0); + prepareTransaction(TransactionType.ACCESS_LIST, 0, Wei.of(500), Wei.ZERO, 0, 0, null); Transaction txAccessList = preparedTx.accessList(ales).createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txAccessList.writeTo(rlpOut); @@ -416,7 +416,14 @@ public void codeDelegationListSize() { System.setProperty("jol.magicFieldOffset", "true"); TransactionTestFixture preparedTx = - prepareTransaction(TransactionType.DELEGATE_CODE, 0, Wei.of(500), Wei.ZERO, 0, 0); + prepareTransaction( + TransactionType.DELEGATE_CODE, + 0, + Wei.of(500), + Wei.ZERO, + 0, + 0, + List.of(CODE_DELEGATION_SENDER_1)); Transaction txDelegateCode = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txDelegateCode.writeTo(rlpOut); @@ -461,7 +468,7 @@ public void baseEIP1559AndEIP4844TransactionMemorySize() { @Test public void baseFrontierAndAccessListTransactionMemorySize() { final Transaction txFrontier = - createTransaction(TransactionType.FRONTIER, 1, Wei.of(500), 0, KEYS1); + createTransaction(TransactionType.FRONTIER, 1, Wei.of(500), 0, List.of(), KEYS1); assertThat(baseTransactionMemorySize(txFrontier, FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS)) .isEqualTo(FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE); } @@ -575,15 +582,18 @@ private long sizeOfField(final Object container, final String... excludePaths) { * * @param filePath where to save the heap dump * @param live true to only include live objects - * @throws IOException if any errors happen during the saving */ @SuppressWarnings("unused") - private static void dumpHeap(final String filePath, final boolean live) throws IOException { - MBeanServer server = ManagementFactory.getPlatformMBeanServer(); - HotSpotDiagnosticMXBean mxBean = - ManagementFactory.newPlatformMXBeanProxy( - server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class); - mxBean.dumpHeap(filePath, live); + private static void dumpHeap(final String filePath, final boolean live) { + try { + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + HotSpotDiagnosticMXBean mxBean = + ManagementFactory.newPlatformMXBeanProxy( + server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class); + mxBean.dumpHeap(filePath, live); + } catch (IOException e) { + throw new RuntimeException(e); + } } record FieldSize(String path, Class clazz, long size) implements Comparable { diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java index 8d01bd73977..163afe1dc38 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java @@ -116,6 +116,7 @@ protected Transaction createTransactionReplacement( originalTransaction.getMaxGasPrice().multiply(2).divide(10), originalTransaction.getPayload().size(), originalTransaction.getBlobCount(), + originalTransaction.getCodeDelegationList().orElse(List.of()), keys); } @@ -191,7 +192,7 @@ public void txWithEffectiveGasPriceBelowCurrentMineableMinGasPriceIsNotPrioritiz final TransactionType type) { final PendingTransaction lowGasPriceTx = createRemotePendingTransaction( - createTransaction(type, 0, DEFAULT_MIN_GAS_PRICE, Wei.ONE, 0, 1, KEYS1)); + createTransaction(type, 0, DEFAULT_MIN_GAS_PRICE, Wei.ONE, 0, 1, List.of(), KEYS1)); assertThat(prioritizeTransaction(lowGasPriceTx)).isEqualTo(DROPPED); assertEvicted(lowGasPriceTx); assertTransactionNotPrioritized(lowGasPriceTx); @@ -217,6 +218,7 @@ public void shouldPrioritizePriorityFeeThenTimeAddedToPoolSameTypeTxs( 0, DEFAULT_MIN_GAS_PRICE.add(1).multiply(20), 0, + List.of(), SIGNATURE_ALGORITHM.get().generateKeyPair()))) .collect(Collectors.toUnmodifiableList()); @@ -238,6 +240,7 @@ public void maxNumberOfTxsForTypeIsEnforced() { DEFAULT_MIN_GAS_PRICE.divide(10), 0, 1, + List.of(), SIGNATURE_ALGORITHM.get().generateKeyPair()); addedTxs.add(tx); assertThat(prioritizeTransaction(tx)).isEqualTo(ADDED); @@ -251,6 +254,7 @@ public void maxNumberOfTxsForTypeIsEnforced() { DEFAULT_MIN_GAS_PRICE.divide(10), 0, 1, + List.of(), SIGNATURE_ALGORITHM.get().generateKeyPair()); assertThat(prioritizeTransaction(overflowTx)).isEqualTo(DROPPED); @@ -272,6 +276,7 @@ public void maxNumberOfTxsForTypeWithReplacement() { DEFAULT_MIN_GAS_PRICE.divide(10), 0, 1, + List.of(), KEYS1); addedTxs.add(tx); assertThat(prioritizeTransaction(tx)).isEqualTo(ADDED); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java index 690ab02a95f..6a17c04f17e 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.eth.transactions.layered; import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.core.TransactionTestFixture.createSignedCodeDelegation; import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.crypto.SignatureAlgorithm; @@ -22,6 +23,7 @@ import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Blob; import org.hyperledger.besu.datatypes.BlobsWithCommitments; +import org.hyperledger.besu.datatypes.CodeDelegation; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.KZGCommitment; import org.hyperledger.besu.datatypes.KZGProof; @@ -39,6 +41,8 @@ import org.hyperledger.besu.metrics.StubMetricsSystem; import org.hyperledger.besu.testutil.DeterministicEthScheduler; +import java.math.BigInteger; +import java.util.List; import java.util.Optional; import java.util.Random; import java.util.stream.IntStream; @@ -56,6 +60,8 @@ public class BaseTransactionPoolTest { protected static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.get().generateKeyPair(); protected static final Address SENDER1 = Util.publicKeyToAddress(KEYS1.getPublicKey()); protected static final Address SENDER2 = Util.publicKeyToAddress(KEYS2.getPublicKey()); + protected static final CodeDelegation CODE_DELEGATION_SENDER_1 = + createSignedCodeDelegation(BigInteger.ONE, Address.ZERO, 0, KEYS1); protected static final Wei DEFAULT_MIN_GAS_PRICE = Wei.of(50); protected static final Wei DEFAULT_MIN_PRIORITY_FEE = Wei.ZERO; private static final Random randomizeTxType = new Random(); @@ -92,7 +98,12 @@ protected Transaction createTransaction( protected Transaction createEIP1559Transaction( final long nonce, final KeyPair keys, final int gasFeeMultiplier) { return createTransaction( - TransactionType.EIP1559, nonce, Wei.of(5000L).multiply(gasFeeMultiplier), 0, keys); + TransactionType.EIP1559, + nonce, + Wei.of(5000L).multiply(gasFeeMultiplier), + 0, + List.of(), + keys); } protected Transaction createEIP4844Transaction( @@ -104,6 +115,21 @@ protected Transaction createEIP4844Transaction( Wei.of(5000L).multiply(gasFeeMultiplier).divide(10), 0, blobCount, + List.of(), + keys); + } + + protected Transaction createEIP7702Transaction( + final long nonce, + final KeyPair keys, + final int gasFeeMultiplier, + final List codeDelegations) { + return createTransaction( + TransactionType.DELEGATE_CODE, + nonce, + Wei.of(5000L).multiply(gasFeeMultiplier), + 0, + codeDelegations, keys); } @@ -115,11 +141,12 @@ protected Transaction createTransactionOfSize( randomizeTxType.nextInt(txSize < blobTransaction0.getSize() ? 3 : 4)]; final Transaction baseTx = - createTransaction(txType, nonce, maxGasPrice, maxGasPrice.divide(10), 0, 1, keys); + createTransaction( + txType, nonce, maxGasPrice, maxGasPrice.divide(10), 0, 1, List.of(), keys); final int payloadSize = txSize - baseTx.getSize(); return createTransaction( - txType, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 1, keys); + txType, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 1, List.of(), keys); } protected Transaction createTransaction( @@ -128,11 +155,14 @@ protected Transaction createTransaction( final TransactionType txType = TransactionType.values()[randomizeTxType.nextInt(4)]; return switch (txType) { - case FRONTIER, ACCESS_LIST, EIP1559, DELEGATE_CODE -> - createTransaction(txType, nonce, maxGasPrice, payloadSize, keys); + case FRONTIER, ACCESS_LIST, EIP1559 -> + createTransaction(txType, nonce, maxGasPrice, payloadSize, List.of(), keys); case BLOB -> createTransaction( - txType, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 1, keys); + txType, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 1, List.of(), keys); + case DELEGATE_CODE -> + createTransaction( + txType, nonce, maxGasPrice, payloadSize, List.of(CODE_DELEGATION_SENDER_1), keys); }; } @@ -141,9 +171,10 @@ protected Transaction createTransaction( final long nonce, final Wei maxGasPrice, final int payloadSize, + final List codeDelegations, final KeyPair keys) { return createTransaction( - type, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 0, keys); + type, nonce, maxGasPrice, maxGasPrice.divide(10), payloadSize, 0, codeDelegations, keys); } protected Transaction createTransaction( @@ -153,9 +184,10 @@ protected Transaction createTransaction( final Wei maxPriorityFeePerGas, final int payloadSize, final int blobCount, + final List codeDelegations, final KeyPair keys) { return prepareTransaction( - type, nonce, maxGasPrice, maxPriorityFeePerGas, payloadSize, blobCount) + type, nonce, maxGasPrice, maxPriorityFeePerGas, payloadSize, blobCount, codeDelegations) .createTransaction(keys); } @@ -165,7 +197,8 @@ protected TransactionTestFixture prepareTransaction( final Wei maxGasPrice, final Wei maxPriorityFeePerGas, final int payloadSize, - final int blobCount) { + final int blobCount, + final List codeDelegations) { var tx = new TransactionTestFixture() @@ -198,6 +231,8 @@ protected TransactionTestFixture prepareTransaction( final var blobsWithCommitments = new BlobsWithCommitments(kgzCommitments, blobs, kzgProofs, versionHashes); tx.blobsWithCommitments(Optional.of(blobsWithCommitments)); + } else if (type.supportsDelegateCode()) { + tx.codeDelegations(codeDelegations); } } else { tx.gasPrice(maxGasPrice); @@ -214,6 +249,7 @@ protected Transaction createTransactionReplacement( originalTransaction.getMaxGasPrice().multiply(2).divide(10), 0, 1, + originalTransaction.getCodeDelegationList().orElse(List.of()), keys); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java index ee900e3dc45..75f2788d71f 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java @@ -19,9 +19,14 @@ import static org.awaitility.Awaitility.await; import static org.hyperledger.besu.datatypes.TransactionType.ACCESS_LIST; import static org.hyperledger.besu.datatypes.TransactionType.BLOB; +import static org.hyperledger.besu.datatypes.TransactionType.DELEGATE_CODE; import static org.hyperledger.besu.datatypes.TransactionType.EIP1559; import static org.hyperledger.besu.datatypes.TransactionType.FRONTIER; +import static org.hyperledger.besu.ethereum.core.TransactionTestFixture.createSignedCodeDelegation; import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason.INVALIDATED; +import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.NO_DELEGATIONS; +import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.delegation; +import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.toCodeDelegations; import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S1; import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S2; import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S3; @@ -33,6 +38,7 @@ import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.CodeDelegation; import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.core.BlockHeader; @@ -50,6 +56,7 @@ import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.testutil.DeterministicEthScheduler; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -62,6 +69,7 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.TreeMap; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -174,6 +182,12 @@ void penalized(final Scenario scenario) { assertScenario(scenario); } + @ParameterizedTest + @MethodSource("providerConfirmedEIP7702") + void confirmedEIP7702(final Scenario scenario) { + assertScenario(scenario); + } + private void assertScenario(final Scenario scenario) { scenario.run(); } @@ -1309,6 +1323,57 @@ static Stream providerPenalized() { .expectedPrioritizedForSenders())); } + static Stream providerConfirmedEIP7702() { + return Stream.of( + Arguments.of( + new Scenario("code delegation tx only") + .addForSender(S1, delegation(S2, 0), 0) + .expectedPrioritizedForSender(S1, 0) + .confirmedForSenders(S1, 0) + .expectedPrioritizedForSenders()), + Arguments.of( + new Scenario("confirmed delegation over plain tx") + .addForSender(S2, 0) + .addForSender(S1, delegation(S2, 0), 0) + .expectedPrioritizedForSenders(S2, 0, S1, 0) + .confirmedForSenders(S1, 0) + // confirming the code delegation tx updates the nonce for S2, so his conflicting + // plain tx is removed + .expectedPrioritizedForSenders()), + Arguments.of( + new Scenario("confirmed plain tx over delegation") + .addForSender(S2, 0) + .addForSender(S1, delegation(S2, 0), 0) + .expectedPrioritizedForSenders(S2, 0, S1, 0) + .confirmedForSenders(S2, 0) + // verify the code delegation for S2 is still there, of course that delegation will + // fail, + // but is it not possible to remove it from the list + .expectedPrioritizedForSender(S1, 0)), + Arguments.of( + new Scenario("self code delegation") + .addForSender(S1, delegation(S1, 1), 0) + .expectedPrioritizedForSender(S1, 0) + .confirmedForSenders(S1, 0) + .expectedPrioritizedForSenders()), + Arguments.of( + new Scenario("self code delegation and plain tx") + .addForSender(S1, delegation(S1, 1), 0) + .addForSender(S1, 1) + .expectedPrioritizedForSender(S1, 0, 1) + .confirmedForSenders(S1, 0) + .expectedPrioritizedForSenders()), + Arguments.of( + new Scenario("self code delegation and plain tx in sparse") + .addForSender(S1, delegation(S1, 1), 0) + .addForSender(S1, 2) + .expectedPrioritizedForSender(S1, 0) + .expectedSparseForSender(S1, 2) + .confirmedForSenders(S1, 0) + .expectedPrioritizedForSender(S1, 2) + .expectedSparseForSenders())); + } + private static BlockHeader mockBlockHeader() { final BlockHeader blockHeader = mock(BlockHeader.class); when(blockHeader.getBaseFee()).thenReturn(Optional.of(BASE_FEE)); @@ -1423,24 +1488,42 @@ public void run() { } public Scenario addForSender(final Sender sender, final long... nonce) { - return addForSender(sender, EIP1559, nonce); + return addForSender(sender, EIP1559, NO_DELEGATIONS, nonce); } public Scenario addForSender( final Sender sender, final TransactionType type, final long... nonce) { - internalAddForSender(sender, type, nonce); + internalAddForSender(sender, type, NO_DELEGATIONS, nonce); + actions.add(notificationsChecker::assertExpectedNotifications); + return this; + } + + public Scenario addForSender( + final Sender sender, final AuthorityAndNonce[] authorityAndNonces, final long... nonce) { + return addForSender(sender, DELEGATE_CODE, authorityAndNonces, nonce); + } + + public Scenario addForSender( + final Sender sender, + final TransactionType type, + final AuthorityAndNonce[] authorityAndNonces, + final long... nonce) { + internalAddForSender(sender, type, authorityAndNonces, nonce); actions.add(notificationsChecker::assertExpectedNotifications); return this; } private void internalAddForSender( - final Sender sender, final TransactionType type, final long... nonce) { + final Sender sender, + final TransactionType type, + final AuthorityAndNonce[] authorityAndNonces, + final long... nonce) { actions.add( () -> { Arrays.stream(nonce) .forEach( n -> { - final var pendingTx = create(sender, type, n); + final var pendingTx = create(sender, type, authorityAndNonces, n); final Account mockSender = mock(Account.class); when(mockSender.getNonce()).thenReturn(nonceBySender.get(sender)); pending.addTransaction(pendingTx, Optional.of(mockSender)); @@ -1497,7 +1580,7 @@ public Scenario addForSenders(final Object... args) { for (int i = 0; i < args.length; i = i + 2) { final Sender sender = (Sender) args[i]; final long nonce = (int) args[i + 1]; - internalAddForSender(sender, EIP1559, nonce); + internalAddForSender(sender, EIP1559, NO_DELEGATIONS, nonce); } actions.add(notificationsChecker::assertExpectedNotifications); return this; @@ -1549,21 +1632,61 @@ public Scenario replaceForSenders(final Object... args) { public Scenario confirmedForSenders(final Object... args) { actions.add( () -> { - final Map maxConfirmedNonceBySender = new HashMap<>(); + final Map maxConfirmedNonceBySender = new HashMap<>(); for (int i = 0; i < args.length; i = i + 2) { final Sender sender = (Sender) args[i]; final long nonce = (int) args[i + 1]; - maxConfirmedNonceBySender.put(sender.address, nonce); + maxConfirmedNonceBySender.put(sender, nonce); nonceBySender.put(sender, nonce + 1); - for (final var pendingTx : getAll(sender)) { - if (pendingTx.getNonce() <= nonce) { - notificationsChecker.addExpectedDropNotification( - liveTxsBySender.get(sender).remove(pendingTx.getNonce())); - } - } + + // if the confirmed tx contains delegations then update the confirmed nonce + // accordingly + getMaybe(sender, nonce) + .ifPresent( + confirmedTx -> + confirmedTx + .getTransaction() + .getCodeDelegationList() + .ifPresent( + codeDelegations -> + codeDelegations.forEach( + cd -> { + final var authority = + Sender.getByAddress(cd.authorizer().get()); + maxConfirmedNonceBySender.compute( + authority, + (unused, currentMax) -> + currentMax == null + ? cd.nonce() + : Math.max(currentMax, cd.nonce())); + nonceBySender.compute( + authority, + (unused, currentNonce) -> + currentNonce == null + ? cd.nonce() + 1 + : Math.max(currentNonce, cd.nonce()) + 1); + }))); } - prio.blockAdded(FeeMarket.london(0L), mockBlockHeader(), maxConfirmedNonceBySender); + maxConfirmedNonceBySender.entrySet().stream() + .forEach( + san -> { + final var sender = san.getKey(); + final var nonce = san.getValue(); + for (final var pendingTx : getAll(sender)) { + if (pendingTx.getNonce() <= nonce) { + notificationsChecker.addExpectedDropNotification( + liveTxsBySender.get(sender).remove(pendingTx.getNonce())); + } + } + }); + + prio.blockAdded( + FeeMarket.london(0L), + mockBlockHeader(), + maxConfirmedNonceBySender.entrySet().stream() + .collect( + Collectors.toMap(entry -> entry.getKey().address, Map.Entry::getValue))); notificationsChecker.assertExpectedNotifications(); }); return this; @@ -1588,7 +1711,10 @@ public Scenario reorgForSenders(final Object... args) { } private PendingTransaction create( - final Sender sender, final TransactionType type, final long nonce) { + final Sender sender, + final TransactionType type, + final AuthorityAndNonce[] authorityAndNonces, + final long nonce) { if (liveTxsBySender.get(sender).containsKey(nonce)) { fail( "Transaction for sender " + sender.name() + " with nonce " + nonce + " already exists"); @@ -1599,7 +1725,8 @@ private PendingTransaction create( case ACCESS_LIST -> createAccessListPendingTransaction(sender, nonce); case EIP1559 -> createEIP1559PendingTransaction(sender, nonce); case BLOB -> createBlobPendingTransaction(sender, nonce); - case DELEGATE_CODE -> throw new UnsupportedOperationException(); + case DELEGATE_CODE -> + createEIP7702PendingTransaction(sender, nonce, authorityAndNonces); }; liveTxsBySender.get(sender).put(nonce, newPendingTx); return newPendingTx; @@ -1629,13 +1756,15 @@ private List getAll(final Sender sender) { private PendingTransaction createFrontierPendingTransaction( final Sender sender, final long nonce) { return createRemotePendingTransaction( - createTransaction(FRONTIER, nonce, Wei.ONE, 0, sender.key), sender.hasPriority); + createTransaction(FRONTIER, nonce, Wei.ONE, 0, List.of(), sender.key), + sender.hasPriority); } private PendingTransaction createAccessListPendingTransaction( final Sender sender, final long nonce) { return createRemotePendingTransaction( - createTransaction(ACCESS_LIST, nonce, Wei.ONE, 0, sender.key), sender.hasPriority); + createTransaction(ACCESS_LIST, nonce, Wei.ONE, 0, List.of(), sender.key), + sender.hasPriority); } private PendingTransaction createEIP1559PendingTransaction( @@ -1650,6 +1779,14 @@ private PendingTransaction createBlobPendingTransaction(final Sender sender, fin sender.hasPriority); } + private PendingTransaction createEIP7702PendingTransaction( + final Sender sender, final long nonce, final AuthorityAndNonce[] authorityAndNonces) { + return createRemotePendingTransaction( + createEIP7702Transaction( + nonce, sender.key, sender.gasFeeMultiplier, toCodeDelegations(authorityAndNonces)), + sender.hasPriority); + } + public Scenario expectedPrioritizedForSender(final Sender sender, final long... nonce) { actions.add( () -> { @@ -1835,7 +1972,8 @@ public Scenario removeForSender(final Sender sender, final long... nonce) { .forEach( n -> { final var maybeLiveTx = getMaybe(sender, n); - final var pendingTx = maybeLiveTx.orElseGet(() -> create(sender, EIP1559, n)); + final var pendingTx = + maybeLiveTx.orElseGet(() -> create(sender, EIP1559, NO_DELEGATIONS, n)); prio.remove(pendingTx, INVALIDATED); maybeLiveTx.ifPresent( liveTx -> { @@ -1968,6 +2106,23 @@ private void assertDropNotifications(final List expectedDroppedTxs) } } + record AuthorityAndNonce(Sender sender, long nonce) { + static final AuthorityAndNonce[] NO_DELEGATIONS = new AuthorityAndNonce[0]; + + static AuthorityAndNonce[] delegation(final Sender sender, final long nonce) { + return new AuthorityAndNonce[] {new AuthorityAndNonce(sender, nonce)}; + } + + static CodeDelegation toCodeDelegation(final AuthorityAndNonce authorityAndNonce) { + return createSignedCodeDelegation( + BigInteger.ZERO, Address.ZERO, authorityAndNonce.nonce, authorityAndNonce.sender.key); + } + + static List toCodeDelegations(final AuthorityAndNonce[] authorityAndNonces) { + return Arrays.stream(authorityAndNonces).map(AuthorityAndNonce::toCodeDelegation).toList(); + } + } + @Test void dryRunDetector() { assertThat(true)