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..a3965e26899 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,8 @@ 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, List.of()); Transaction txTo = preparedTx.to(Optional.of(Address.extract(Bytes32.random()))).createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); @@ -187,7 +188,8 @@ 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, List.of()); Transaction txPayload = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txPayload.writeTo(rlpOut); @@ -277,7 +279,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, List.of()); Transaction txBlob = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION); @@ -309,7 +311,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, List.of()); Transaction txBlob = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION); @@ -337,7 +339,8 @@ 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, List.of()); Transaction txPayload = preparedTx.createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txPayload.writeTo(rlpOut); @@ -369,7 +372,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, List.of()); Transaction txAccessList = preparedTx.accessList(ales).createTransaction(KEYS1); BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); txAccessList.writeTo(rlpOut); @@ -416,7 +419,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 +471,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); } 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..7a3754d3891 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 @@ -16,12 +16,14 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.hyperledger.besu.crypto.CodeDelegationSignature; import org.hyperledger.besu.crypto.KeyPair; import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; 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; @@ -31,14 +33,18 @@ import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionTestFixture; import org.hyperledger.besu.ethereum.core.Util; +import org.hyperledger.besu.ethereum.core.encoding.CodeDelegationTransactionEncoder; import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.evm.account.Account; 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 +62,9 @@ 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 = + new org.hyperledger.besu.ethereum.core.CodeDelegation( + BigInteger.ZERO, Address.ZERO, 0, createCodeDelegationSignature(KEYS1, 0)); 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 +101,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 +118,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 +144,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 +158,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 +174,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 +187,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 +200,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 +234,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 +252,7 @@ protected Transaction createTransactionReplacement( originalTransaction.getMaxGasPrice().multiply(2).divide(10), 0, 1, + originalTransaction.getCodeDelegationList().orElse(List.of()), keys); } @@ -269,4 +308,22 @@ protected long getRemovedCount( return metricsSystem.getCounterValue( TransactionPoolMetrics.REMOVED_COUNTER_NAME, source, priority, operation, layer); } + + protected static CodeDelegationSignature createCodeDelegationSignature( + final KeyPair keys, final long nonce) { + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature( + new org.hyperledger.besu.ethereum.core.CodeDelegation( + BigInteger.ZERO, Address.ZERO, 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 CodeDelegationSignature.create(signature.getR(), signature.getS(), signature.getRecId()); + } } 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..81584e7708c 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,13 @@ 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.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 +37,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 +55,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 +68,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 +181,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 +1322,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 +1487,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 +1579,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 +1631,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 +1710,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 +1724,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 +1755,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 +1778,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 +1971,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 +2105,37 @@ private void assertDropNotifications(final List expectedDroppedTxs) } } + record ConfirmedTransaction(Sender sender, long nonce, AuthorityAndNonce[] authorityAndNonces) { + static ConfirmedTransaction confirmed(final Sender sender, final long nonce) { + return new ConfirmedTransaction(sender, nonce, new AuthorityAndNonce[0]); + } + + static ConfirmedTransaction confirmed( + final Sender sender, final long nonce, final AuthorityAndNonce[] authorityAndNonces) { + return new ConfirmedTransaction(sender, nonce, authorityAndNonces); + } + } + + 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 new org.hyperledger.besu.ethereum.core.CodeDelegation( + BigInteger.ZERO, + Address.ZERO, + authorityAndNonce.nonce, + createCodeDelegationSignature(authorityAndNonce.sender.key, authorityAndNonce.nonce)); + } + + static List toCodeDelegations(final AuthorityAndNonce[] authorityAndNonces) { + return Arrays.stream(authorityAndNonces).map(AuthorityAndNonce::toCodeDelegation).toList(); + } + } + @Test void dryRunDetector() { assertThat(true)