diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java index 124abd89..07050b79 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java @@ -214,7 +214,10 @@ private void performSharedStartTasksOnce(final ServiceManager serviceManager) { bundlePoolService = new LineaLimitedBundlePool( - transactionSelectorConfiguration().maxBundlePoolSizeBytes(), besuEvents); + besuConfiguration.getDataPath(), + transactionSelectorConfiguration().maxBundlePoolSizeBytes(), + besuEvents); + bundlePoolService.loadFromDisk(); } @Override diff --git a/sequencer/src/main/java/net/consensys/linea/rpc/services/BundlePoolService.java b/sequencer/src/main/java/net/consensys/linea/rpc/services/BundlePoolService.java index 318a626f..f49472ec 100644 --- a/sequencer/src/main/java/net/consensys/linea/rpc/services/BundlePoolService.java +++ b/sequencer/src/main/java/net/consensys/linea/rpc/services/BundlePoolService.java @@ -14,14 +14,20 @@ */ package net.consensys.linea.rpc.services; +import static java.util.stream.Collectors.joining; + +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.regex.Pattern; import lombok.Getter; import lombok.experimental.Accessors; +import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.plugin.services.BesuService; public interface BundlePoolService extends BesuService { @@ -30,6 +36,9 @@ public interface BundlePoolService extends BesuService { @Accessors(fluent = true) @Getter class TransactionBundle { + private static final String FIELD_SEPARATOR = "|"; + private static final String ITEM_SEPARATOR = ","; + private static final String LINE_TERMINATOR = "$"; private final Hash bundleIdentifier; private final List pendingTransactions; private final Long blockNumber; @@ -52,6 +61,74 @@ public TransactionBundle( this.revertingTxHashes = revertingTxHashes; } + public String serializeForDisk() { + // version=1 | blockNumber | bundleIdentifier | minTimestamp | maxTimestamp | + // revertingTxHashes, | txs, $ + return new StringBuilder("1") + .append(FIELD_SEPARATOR) + .append(blockNumber) + .append(FIELD_SEPARATOR) + .append(bundleIdentifier.toHexString()) + .append(FIELD_SEPARATOR) + .append(minTimestamp.map(l -> l + FIELD_SEPARATOR).orElse(FIELD_SEPARATOR)) + .append(maxTimestamp.map(l -> l + FIELD_SEPARATOR).orElse(FIELD_SEPARATOR)) + .append( + revertingTxHashes + .map(l -> l.stream().map(Hash::toHexString).collect(joining(ITEM_SEPARATOR))) + .orElse(FIELD_SEPARATOR)) + .append( + pendingTransactions.stream() + .map(PendingBundleTx::serializeForDisk) + .collect(joining(ITEM_SEPARATOR))) + .append(LINE_TERMINATOR) + .toString(); + } + + public static TransactionBundle restoreFromSerialized(final String str) { + if (!str.endsWith(LINE_TERMINATOR)) { + throw new IllegalArgumentException( + "Unterminated bundle serialization, missing terminal " + LINE_TERMINATOR); + } + + final var parts = + str.substring(0, str.length() - LINE_TERMINATOR.length()) + .split(Pattern.quote(FIELD_SEPARATOR)); + if (!parts[0].equals("1")) { + throw new IllegalArgumentException("Unsupported bundle serialization version " + parts[0]); + } + if (parts.length != 7) { + throw new IllegalArgumentException( + "Invalid bundle serialization, expected 7 fields but got " + parts.length); + } + + final var blockNumber = Long.parseLong(parts[1]); + final var bundleIdentifier = Hash.fromHexString(parts[2]); + final Optional minTimestamp = + parts[3].isEmpty() ? Optional.empty() : Optional.of(Long.parseLong(parts[3])); + final Optional maxTimestamp = + parts[4].isEmpty() ? Optional.empty() : Optional.of(Long.parseLong(parts[4])); + final Optional> revertingTxHashes = + parts[5].isEmpty() + ? Optional.empty() + : Optional.of( + Arrays.stream(parts[5].split(Pattern.quote(ITEM_SEPARATOR))) + .map(Hash::fromHexString) + .toList()); + final var transactions = + Arrays.stream(parts[6].split(Pattern.quote(ITEM_SEPARATOR))) + .map(Bytes::fromBase64String) + .map(Transaction::readFrom) + .toList(); + + return new TransactionBundle( + bundleIdentifier, + transactions, + blockNumber, + minTimestamp, + maxTimestamp, + revertingTxHashes); + } + /** A pending transaction contained in a bundle. */ public class PendingBundleTx extends org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.Local { @@ -72,6 +149,12 @@ public boolean isBundleStart() { public String toTraceLog() { return "Bundle tx: " + super.toTraceLog(); } + + String serializeForDisk() { + final var rlpOutput = new BytesValueRLPOutput(); + getTransaction().writeTo(rlpOutput); + return rlpOutput.encoded().toBase64String(); + } } } @@ -146,4 +229,8 @@ public String toTraceLog() { * @return the number of bundles in the pool */ long size(); + + void saveToDisk(); + + void loadFromDisk(); } diff --git a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaLimitedBundlePool.java b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaLimitedBundlePool.java index 52f7c13e..aa3883ac 100644 --- a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaLimitedBundlePool.java +++ b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaLimitedBundlePool.java @@ -15,8 +15,15 @@ package net.consensys.linea.rpc.services; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -44,8 +51,10 @@ @AutoService(BesuService.class) @Slf4j public class LineaLimitedBundlePool implements BundlePoolService, BesuEvents.BlockAddedListener { + public static final String BUNDLE_SAVE_FILENAME = "bundles.dump"; private final Cache cache; private final Map> blockIndex; + private final Path saveFilePath; private final AtomicLong maxBlockHeight = new AtomicLong(0L); /** @@ -55,7 +64,9 @@ public class LineaLimitedBundlePool implements BundlePoolService, BesuEvents.Blo * @param maxSizeInBytes The maximum size in bytes of the pool objects. */ @VisibleForTesting - public LineaLimitedBundlePool(long maxSizeInBytes, BesuEvents eventService) { + public LineaLimitedBundlePool( + final Path dataDir, final long maxSizeInBytes, final BesuEvents eventService) { + this.saveFilePath = dataDir.resolve(BUNDLE_SAVE_FILENAME); this.cache = Caffeine.newBuilder() .maximumWeight(maxSizeInBytes) // Maximum size in bytes @@ -185,6 +196,63 @@ public long size() { return cache.estimatedSize(); } + @Override + public void saveToDisk() { + log.info("Saving bundles to {}", saveFilePath); + try (final BufferedWriter bw = + Files.newBufferedWriter(saveFilePath, StandardCharsets.US_ASCII)) { + final var savedCount = + blockIndex.values().stream() + .flatMap(List::stream) + .sorted(Comparator.comparing(TransactionBundle::blockNumber)) + .map(TransactionBundle::serializeForDisk) + .peek( + str -> { + try { + bw.write(str); + bw.newLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .count(); + log.info("Saved {} bundles to {}", savedCount, saveFilePath); + } catch (final Throwable ioe) { + log.error("Error while saving bundles to {}", saveFilePath, ioe); + } + } + + @Override + public void loadFromDisk() { + if (saveFilePath.toFile().exists()) { + log.info("Loading bundles from {}", saveFilePath); + final var loadedCount = new AtomicLong(0L); + try (final BufferedReader br = + Files.newBufferedReader(saveFilePath, StandardCharsets.US_ASCII)) { + br.lines() + .parallel() + .forEach( + line -> { + try { + final var bundle = TransactionBundle.restoreFromSerialized(line); + this.putOrReplace(bundle.bundleIdentifier(), bundle); + loadedCount.incrementAndGet(); + } catch (final Exception e) { + log.warn("Error while loading bundle from serialized format {}", line, e); + } + }); + log.info("Loaded {} bundles from {}", loadedCount.get(), saveFilePath); + } catch (final Throwable t) { + log.error( + "Error while reading bundles from {}, partially loaded {} bundles", + saveFilePath, + loadedCount.get(), + t); + } + saveFilePath.toFile().delete(); + } + } + /** * Adds a TransactionBundle to the block index. * diff --git a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSendBundleEndpointPlugin.java b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSendBundleEndpointPlugin.java index f95cd79c..59e81998 100644 --- a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSendBundleEndpointPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSendBundleEndpointPlugin.java @@ -60,4 +60,10 @@ public void doStart() { lineaSendBundleMethod.init(bundlePoolService); lineaCancelBundleMethod.init(bundlePoolService); } + + @Override + public void stop() { + + super.stop(); + } } diff --git a/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java b/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java index 370962bf..fa8c1e43 100644 --- a/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java +++ b/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import java.nio.file.Path; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -37,9 +38,10 @@ import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class LineaSendBundleTest { - + @TempDir Path dataDir; private LineaSendBundle lineaSendBundle; private BesuEvents mockEvents; private LineaLimitedBundlePool bundlePool; @@ -59,7 +61,7 @@ class LineaSendBundleTest { @BeforeEach void setup() { mockEvents = mock(BesuEvents.class); - bundlePool = spy(new LineaLimitedBundlePool(4096L, mockEvents)); + bundlePool = spy(new LineaLimitedBundlePool(dataDir, 4096L, mockEvents)); lineaSendBundle = new LineaSendBundle().init(bundlePool); } diff --git a/sequencer/src/test/java/net/consensys/linea/rpc/services/LineaLimitedBundlePoolTest.java b/sequencer/src/test/java/net/consensys/linea/rpc/services/LineaLimitedBundlePoolTest.java index 6354d392..069ecf6a 100644 --- a/sequencer/src/test/java/net/consensys/linea/rpc/services/LineaLimitedBundlePoolTest.java +++ b/sequencer/src/test/java/net/consensys/linea/rpc/services/LineaLimitedBundlePoolTest.java @@ -15,6 +15,9 @@ package net.consensys.linea.rpc.services; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static net.consensys.linea.rpc.services.LineaLimitedBundlePool.BUNDLE_SAVE_FILENAME; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -23,6 +26,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -30,16 +37,35 @@ import net.consensys.linea.rpc.services.BundlePoolService.TransactionBundle; import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.plugin.data.AddedBlockContext; import org.hyperledger.besu.plugin.data.BlockHeader; import org.hyperledger.besu.plugin.services.BesuEvents; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class LineaLimitedBundlePoolTest { - + private static final KeyPair KEY_PAIR_1 = + new KeyPair( + SECPPrivateKey.create(BigInteger.valueOf(Long.MAX_VALUE), SignatureAlgorithm.ALGORITHM), + SECPPublicKey.create(BigInteger.valueOf(Long.MIN_VALUE), SignatureAlgorithm.ALGORITHM)); + + private static final Transaction TX1 = + new TransactionTestFixture().nonce(0).gasLimit(21000).createTransaction(KEY_PAIR_1); + private static final Transaction TX2 = + new TransactionTestFixture().nonce(1).gasLimit(21000).createTransaction(KEY_PAIR_1); + private static final Transaction TX3 = + new TransactionTestFixture().nonce(2).gasLimit(21000).createTransaction(KEY_PAIR_1); + + @TempDir Path dataDir; private LineaLimitedBundlePool pool; private BesuEvents eventService; private AddedBlockContext addedBlockContext; @@ -49,7 +75,8 @@ class LineaLimitedBundlePoolTest { void setUp() { eventService = mock(BesuEvents.class); addedBlockContext = mock(AddedBlockContext.class); - pool = new LineaLimitedBundlePool(10_000L, eventService); // Max 100 entries, 10 KB size + pool = + new LineaLimitedBundlePool(dataDir, 10_000L, eventService); // Max 100 entries, 10 KB size blockHeader = mock(BlockHeader.class); } @@ -195,6 +222,89 @@ void testOnBlockAdded_RemovesOldBundles() { assert pool.getBundlesByBlockNumber(oldBlockNumber).isEmpty(); } + @Test + void saveToDisk() throws IOException { + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = createBundle(hash1, 1, List.of(TX1, TX2)); + pool.putOrReplace(hash1, bundle1); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + TransactionBundle bundle2 = createBundle(hash2, 2, List.of(TX3)); + pool.putOrReplace(hash2, bundle2); + + pool.saveToDisk(); + + final var savedLines = Files.readAllLines(dataDir.resolve(BUNDLE_SAVE_FILENAME), US_ASCII); + assertThat(savedLines) + .containsExactly( + "1|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$", + "1|2|0x0000000000000000000000000000000000000000000000000000000000005678||||+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx$"); + } + + @Test + void loadFromDisk() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + 1|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$ + 1|2|0x0000000000000000000000000000000000000000000000000000000000005678||||+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx$ + """, + US_ASCII); + + pool.loadFromDisk(); + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = pool.get(hash1); + + assertThat(bundle1.blockNumber()).isEqualTo(1); + assertThat(bundle1.bundleIdentifier()).isEqualTo(hash1); + assertThat(bundle1.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + + TransactionBundle bundle2 = pool.get(hash2); + + assertThat(bundle2.blockNumber()).isEqualTo(2); + assertThat(bundle2.bundleIdentifier()).isEqualTo(hash2); + assertThat(bundle2.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX3.getHash()); + } + + @Test + void partialLoadFromDisk() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + 1|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$ + 1|not a number|0x0000000000000000000000000000000000000000000000000000000000005678||||+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx$ + """, + US_ASCII); + + pool.loadFromDisk(); + + assertThat(pool.size()).isEqualTo(1); + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = pool.get(hash1); + + assertThat(bundle1.blockNumber()).isEqualTo(1); + assertThat(bundle1.bundleIdentifier()).isEqualTo(hash1); + assertThat(bundle1.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + + assertThat(pool.get(hash2)).isNull(); + } + private TransactionBundle createBundle(Hash hash, long blockNumber) { return createBundle(hash, blockNumber, Collections.emptyList()); } diff --git a/sequencer/src/test/java/net/consensys/linea/rpc/services/TransactionBundleTest.java b/sequencer/src/test/java/net/consensys/linea/rpc/services/TransactionBundleTest.java new file mode 100644 index 00000000..3aba2d9c --- /dev/null +++ b/sequencer/src/test/java/net/consensys/linea/rpc/services/TransactionBundleTest.java @@ -0,0 +1,118 @@ +/* + * Copyright Consensys Software Inc. + * + * 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 net.consensys.linea.rpc.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import net.consensys.linea.rpc.services.BundlePoolService.TransactionBundle; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.junit.jupiter.api.Test; + +class TransactionBundleTest { + private static final KeyPair KEY_PAIR_1 = + new KeyPair( + SECPPrivateKey.create(BigInteger.valueOf(Long.MAX_VALUE), SignatureAlgorithm.ALGORITHM), + SECPPublicKey.create(BigInteger.valueOf(Long.MIN_VALUE), SignatureAlgorithm.ALGORITHM)); + + private static final Transaction TX1 = + new TransactionTestFixture().nonce(0).gasLimit(21000).createTransaction(KEY_PAIR_1); + private static final Transaction TX2 = + new TransactionTestFixture().nonce(1).gasLimit(21000).createTransaction(KEY_PAIR_1); + private static final Transaction TX3 = + new TransactionTestFixture().nonce(2).gasLimit(21000).createTransaction(KEY_PAIR_1); + + @Test + void serializeForDisk() { + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = createBundle(hash1, 1, List.of(TX1, TX2)); + + assertThat(bundle1.serializeForDisk()) + .isEqualTo( + "1|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$"); + } + + @Test + void restoreFromSerialized() { + TransactionBundle bundle = + TransactionBundle.restoreFromSerialized( + "1|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$"); + + assertThat(bundle.blockNumber()).isEqualTo(1); + assertThat(bundle.bundleIdentifier()).isEqualTo(Hash.fromHexStringLenient("0x1234")); + assertThat(bundle.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + } + + @Test + void restoreFromSerializedUnsupportedVersion() { + assertThatThrownBy( + () -> + TransactionBundle.restoreFromSerialized( + "2|1|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageEndingWith("Unsupported bundle serialization version 2"); + } + + @Test + void restoreFromSerializedMalformed() { + assertThatThrownBy( + () -> + TransactionBundle.restoreFromSerialized( + "1|1|0x0000000000000000000000000000000000000000000000000000000000001234$")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageEndingWith("Invalid bundle serialization, expected 7 fields but got 3"); + } + + @Test + void restoreFromSerializedParseError() { + assertThatThrownBy( + () -> + TransactionBundle.restoreFromSerialized( + "1|should be a number|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf$")) + .isInstanceOf(NumberFormatException.class) + .hasMessage("For input string: \"should be a number\""); + } + + @Test + void restoreFromSerializedUnterminatedLine() { + assertThatThrownBy( + () -> + TransactionBundle.restoreFromSerialized( + "1|should be a number|0x0000000000000000000000000000000000000000000000000000000000001234||||+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7,+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unterminated bundle serialization, missing terminal $"); + } + + private TransactionBundle createBundle(Hash hash, long blockNumber, List maybeTxs) { + return new TransactionBundle( + hash, maybeTxs, blockNumber, Optional.empty(), Optional.empty(), Optional.empty()); + } +} diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java index 02b4bcec..9403f089 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java @@ -81,6 +81,7 @@ class LineaTransactionSelectorFactoryTest { private LineaTransactionSelectorFactory factory; @TempDir static Path tempDir; + @TempDir Path dataDir; static Path lineLimitsConfPath; @BeforeAll @@ -109,7 +110,7 @@ void setUp() { new LineaL1L2BridgeSharedConfiguration(BRIDGE_CONTRACT, BRIDGE_LOG_TOPIC); mockProfitabilityConfiguration = mock(LineaProfitabilityConfiguration.class); mockEvents = mock(BesuEvents.class); - bundlePool = spy(new LineaLimitedBundlePool(4096, mockEvents)); + bundlePool = spy(new LineaLimitedBundlePool(dataDir, 4096, mockEvents)); factory = new LineaTransactionSelectorFactory(