Transaction detachedCopy to optimize txpool memory usage (#5985)

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
Co-authored-by: Justin Florentine <justin+github@florentine.us>
pull/6024/head
Fabio Di Fabio 1 year ago committed by GitHub
parent dc2289eada
commit 9ec055caac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 142
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java
  2. 6
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/BlobPooledTransactionDecoder.java
  3. 83
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java
  4. 2
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java
  5. 457
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java
  6. 51
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java
  7. 2
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReplayTest.java

@ -134,6 +134,8 @@ public class Transaction
/**
* Instantiates a transaction instance.
*
* @param forCopy true when using to create a copy of an already validated transaction avoid to
* redo the validation
* @param transactionType the transaction type
* @param nonce the nonce
* @param gasPrice the gas price
@ -154,7 +156,8 @@ public class Transaction
* <p>The {@code chainId} must be greater than 0 to be applied to a specific chain; otherwise
* it will default to any chain.
*/
public Transaction(
private Transaction(
final boolean forCopy,
final TransactionType transactionType,
final long nonce,
final Optional<Wei> gasPrice,
@ -172,36 +175,40 @@ public class Transaction
final Optional<List<VersionedHash>> versionedHashes,
final Optional<BlobsWithCommitments> blobsWithCommitments) {
if (transactionType.requiresChainId()) {
checkArgument(
chainId.isPresent(), "Chain id must be present for transaction type %s", transactionType);
}
if (!forCopy) {
if (transactionType.requiresChainId()) {
checkArgument(
chainId.isPresent(),
"Chain id must be present for transaction type %s",
transactionType);
}
if (maybeAccessList.isPresent()) {
checkArgument(
transactionType.supportsAccessList(),
"Must not specify access list for transaction not supporting it");
}
if (maybeAccessList.isPresent()) {
checkArgument(
transactionType.supportsAccessList(),
"Must not specify access list for transaction not supporting it");
}
if (Objects.equals(transactionType, TransactionType.ACCESS_LIST)) {
checkArgument(
maybeAccessList.isPresent(), "Must specify access list for access list transaction");
}
if (Objects.equals(transactionType, TransactionType.ACCESS_LIST)) {
checkArgument(
maybeAccessList.isPresent(), "Must specify access list for access list transaction");
}
if (versionedHashes.isPresent() || maxFeePerBlobGas.isPresent()) {
checkArgument(
transactionType.supportsBlob(),
"Must not specify blob versioned hashes or max fee per blob gas for transaction not supporting it");
}
if (versionedHashes.isPresent() || maxFeePerBlobGas.isPresent()) {
checkArgument(
transactionType.supportsBlob(),
"Must not specify blob versioned hashes or max fee per blob gas for transaction not supporting it");
}
if (transactionType.supportsBlob()) {
checkArgument(
versionedHashes.isPresent(), "Must specify blob versioned hashes for blob transaction");
checkArgument(
!versionedHashes.get().isEmpty(),
"Blob transaction must have at least one versioned hash");
checkArgument(
maxFeePerBlobGas.isPresent(), "Must specify max fee per blob gas for blob transaction");
if (transactionType.supportsBlob()) {
checkArgument(
versionedHashes.isPresent(), "Must specify blob versioned hashes for blob transaction");
checkArgument(
!versionedHashes.get().isEmpty(),
"Blob transaction must have at least one versioned hash");
checkArgument(
maxFeePerBlobGas.isPresent(), "Must specify max fee per blob gas for blob transaction");
}
}
this.transactionType = transactionType;
@ -221,7 +228,7 @@ public class Transaction
this.versionedHashes = versionedHashes;
this.blobsWithCommitments = blobsWithCommitments;
if (isUpfrontGasCostTooHigh()) {
if (!forCopy && isUpfrontGasCostTooHigh()) {
throw new IllegalArgumentException("Upfront gas cost exceeds UInt256");
}
}
@ -998,6 +1005,84 @@ public class Transaction
return Optional.empty();
}
/**
* Creates a copy of this transaction that does not share any underlying byte array.
*
* <p>This is useful in case the transaction is built from a block body and fields, like to or
* payload, are wrapping (and so keeping references) sections of the large RPL encoded block body,
* and we plan to keep the transaction around for some time, like in the txpool in case of a
* reorg, and do not want to keep all the block body in memory for a long time, but only the
* actual transaction.
*
* @return a copy of the transaction
*/
public Transaction detachedCopy() {
final Optional<Address> detachedTo =
to.isEmpty() ? to : Optional.of(Address.wrap(to.get().copy()));
final Optional<List<AccessListEntry>> detachedAccessList =
maybeAccessList.isEmpty()
? maybeAccessList
: Optional.of(
maybeAccessList.get().stream().map(this::accessListDetachedCopy).toList());
final Optional<List<VersionedHash>> detachedVersionedHashes =
versionedHashes.isEmpty()
? versionedHashes
: Optional.of(
versionedHashes.get().stream()
.map(vh -> new VersionedHash(vh.toBytes().copy()))
.toList());
final Optional<BlobsWithCommitments> detachedBlobsWithCommitments =
blobsWithCommitments.isEmpty()
? blobsWithCommitments
: Optional.of(
blobsWithCommitmentsDetachedCopy(
blobsWithCommitments.get(), detachedVersionedHashes.get()));
return new Transaction(
true,
transactionType,
nonce,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
maxFeePerBlobGas,
gasLimit,
detachedTo,
value,
signature,
payload.copy(),
detachedAccessList,
sender,
chainId,
detachedVersionedHashes,
detachedBlobsWithCommitments);
}
private AccessListEntry accessListDetachedCopy(final AccessListEntry accessListEntry) {
final Address detachedAddress = Address.wrap(accessListEntry.address().copy());
final var detachedStorage = accessListEntry.storageKeys().stream().map(Bytes32::copy).toList();
return new AccessListEntry(detachedAddress, detachedStorage);
}
private BlobsWithCommitments blobsWithCommitmentsDetachedCopy(
final BlobsWithCommitments blobsWithCommitments, final List<VersionedHash> versionedHashes) {
final var detachedCommitments =
blobsWithCommitments.getKzgCommitments().stream()
.map(kc -> new KZGCommitment(kc.getData().copy()))
.toList();
final var detachedBlobs =
blobsWithCommitments.getBlobs().stream()
.map(blob -> new Blob(blob.getData().copy()))
.toList();
final var detachedProofs =
blobsWithCommitments.getKzgProofs().stream()
.map(proof -> new KZGProof(proof.getData().copy()))
.toList();
return new BlobsWithCommitments(
detachedCommitments, detachedBlobs, detachedProofs, versionedHashes);
}
public static class Builder {
private static final Optional<List<AccessListEntry>> EMPTY_ACCESS_LIST = Optional.of(List.of());
@ -1134,6 +1219,7 @@ public class Transaction
public Transaction build() {
if (transactionType == null) guessType();
return new Transaction(
false,
transactionType,
nonce,
Optional.ofNullable(gasPrice),

@ -24,9 +24,9 @@ import java.util.List;
/**
* Class responsible for decoding blob transactions from the transaction pool. Blob transactions
* have two network representations. During transaction gossip responses (PooledTransactions), the
* EIP-2718 TransactionPayload of the blob transaction is wrapped to become: rlp([tx_payload_body,
* blobs, commitments, proofs]).
* have two representations. The network representation is used during transaction gossip responses
* (PooledTransactions), the EIP-2718 TransactionPayload of the blob transaction is wrapped to
* become: rlp([tx_payload_body, blobs, commitments, proofs]).
*/
public class BlobPooledTransactionDecoder {

@ -31,14 +31,20 @@ import java.util.concurrent.atomic.AtomicLong;
public abstract class PendingTransaction
implements org.hyperledger.besu.datatypes.PendingTransaction {
static final int NOT_INITIALIZED = -1;
static final int FRONTIER_BASE_MEMORY_SIZE = 944;
static final int ACCESS_LIST_BASE_MEMORY_SIZE = 944;
static final int EIP1559_BASE_MEMORY_SIZE = 1056;
static final int OPTIONAL_TO_MEMORY_SIZE = 92;
static final int FRONTIER_AND_ACCESS_LIST_BASE_MEMORY_SIZE = 872;
static final int EIP1559_AND_EIP4844_BASE_MEMORY_SIZE = 984;
static final int OPTIONAL_TO_MEMORY_SIZE = 112;
static final int OPTIONAL_CHAIN_ID_MEMORY_SIZE = 80;
static final int PAYLOAD_BASE_MEMORY_SIZE = 32;
static final int ACCESS_LIST_STORAGE_KEY_MEMORY_SIZE = 32;
static final int ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE = 128;
static final int ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE = 248;
static final int OPTIONAL_ACCESS_LIST_MEMORY_SIZE = 24;
static final int VERSIONED_HASH_SIZE = 96;
static final int BASE_LIST_SIZE = 48;
static final int BASE_OPTIONAL_SIZE = 16;
static final int KZG_COMMITMENT_OR_PROOF_SIZE = 112;
static final int BLOB_SIZE = 131136;
static final int BLOBS_WITH_COMMITMENTS_SIZE = 32;
static final int PENDING_TRANSACTION_MEMORY_SIZE = 40;
private static final AtomicLong TRANSACTIONS_ADDED = new AtomicLong();
private final Transaction transaction;
@ -47,10 +53,15 @@ public abstract class PendingTransaction
private int memorySize = NOT_INITIALIZED;
protected PendingTransaction(final Transaction transaction, final long addedAt) {
private PendingTransaction(
final Transaction transaction, final long addedAt, final long sequence) {
this.transaction = transaction;
this.addedAt = addedAt;
this.sequence = TRANSACTIONS_ADDED.getAndIncrement();
this.sequence = sequence;
}
private PendingTransaction(final Transaction transaction, final long addedAt) {
this(transaction, addedAt, TRANSACTIONS_ADDED.getAndIncrement());
}
@Override
@ -90,6 +101,8 @@ public abstract class PendingTransaction
return memorySize;
}
public abstract PendingTransaction detachedCopy();
private int computeMemorySize() {
return switch (transaction.getType()) {
case FRONTIER -> computeFrontierMemorySize();
@ -101,30 +114,49 @@ public abstract class PendingTransaction
}
private int computeFrontierMemorySize() {
return FRONTIER_BASE_MEMORY_SIZE + computePayloadMemorySize() + computeToMemorySize();
return FRONTIER_AND_ACCESS_LIST_BASE_MEMORY_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize();
}
private int computeAccessListMemorySize() {
return ACCESS_LIST_BASE_MEMORY_SIZE
return FRONTIER_AND_ACCESS_LIST_BASE_MEMORY_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeEIP1559MemorySize() {
return EIP1559_BASE_MEMORY_SIZE
return EIP1559_AND_EIP4844_BASE_MEMORY_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeBlobMemorySize() {
// ToDo 4844: adapt for blobs
return computeEIP1559MemorySize();
return computeEIP1559MemorySize()
+ BASE_OPTIONAL_SIZE // for the versionedHashes field
+ computeBlobWithCommitmentsMemorySize();
}
private int computeBlobWithCommitmentsMemorySize() {
final int blobCount = transaction.getBlobCount();
return BASE_OPTIONAL_SIZE
+ BLOBS_WITH_COMMITMENTS_SIZE
+ (BASE_LIST_SIZE * 4)
+ (KZG_COMMITMENT_OR_PROOF_SIZE * blobCount * 2)
+ (VERSIONED_HASH_SIZE * blobCount)
+ (BLOB_SIZE * blobCount);
}
private int computePayloadMemorySize() {
return PAYLOAD_BASE_MEMORY_SIZE + transaction.getPayload().size();
return transaction.getPayload().size() > 0
? PAYLOAD_BASE_MEMORY_SIZE + transaction.getPayload().size()
: 0;
}
private int computeToMemorySize() {
@ -134,6 +166,13 @@ public abstract class PendingTransaction
return 0;
}
private int computeChainIdMemorySize() {
if (transaction.getChainId().isPresent()) {
return OPTIONAL_CHAIN_ID_MEMORY_SIZE;
}
return 0;
}
private int computeAccessListEntriesMemorySize() {
return transaction
.getAccessList()
@ -212,6 +251,15 @@ public abstract class PendingTransaction
this(transaction, System.currentTimeMillis());
}
private Local(final long sequence, final Transaction transaction) {
super(transaction, System.currentTimeMillis(), sequence);
}
@Override
public PendingTransaction detachedCopy() {
return new Local(getSequence(), getTransaction().detachedCopy());
}
@Override
public boolean isReceivedFromLocalSource() {
return true;
@ -228,6 +276,15 @@ public abstract class PendingTransaction
this(transaction, System.currentTimeMillis());
}
private Remote(final long sequence, final Transaction transaction) {
super(transaction, System.currentTimeMillis(), sequence);
}
@Override
public PendingTransaction detachedCopy() {
return new Remote(getSequence(), getTransaction().detachedCopy());
}
@Override
public boolean isReceivedFromLocalSource() {
return false;

@ -150,7 +150,7 @@ public abstract class AbstractTransactionsLayer implements TransactionsLayer {
}
if (addStatus.isSuccess()) {
processAdded(pendingTransaction);
processAdded(pendingTransaction.detachedCopy());
addStatus.maybeReplacedTransaction().ifPresent(this::replaced);
nextLayer.notifyAdded(pendingTransaction);

@ -19,52 +19,63 @@ import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.BlobsWithCommitments;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.ethereum.core.encoding.EncodingContext;
import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder;
import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder;
import org.hyperledger.besu.ethereum.eth.transactions.layered.BaseTransactionPoolTest;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;
import com.google.common.collect.Sets;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphPathRecord;
import org.openjdk.jol.info.GraphVisitor;
import org.openjdk.jol.info.GraphWalker;
@Disabled("Need to handle different results on different OS")
@EnabledOnOs(OS.LINUX)
public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPoolTest {
private static final Set<Class<?>> SHARED_CLASSES =
Set.of(SignatureAlgorithm.class, TransactionType.class);
private static final Set<String> EIP1559_CONSTANT_FIELD_PATHS = Set.of(".gasPrice");
private static final Set<String> EIP1559_VARIABLE_SIZE_PATHS =
Set.of(".to", ".payload", ".maybeAccessList");
private static final Set<String> COMMON_CONSTANT_FIELD_PATHS =
Set.of(".value.ctor", ".hashNoSignature");
private static final Set<String> EIP1559_EIP4844_CONSTANT_FIELD_PATHS =
Sets.union(COMMON_CONSTANT_FIELD_PATHS, Set.of(".gasPrice"));
private static final Set<String> FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS =
Set.of(".maxFeePerGas", ".maxPriorityFeePerGas");
private static final Set<String> FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS =
Set.of(".to", ".payload", ".maybeAccessList");
Sets.union(COMMON_CONSTANT_FIELD_PATHS, Set.of(".maxFeePerGas", ".maxPriorityFeePerGas"));
private static final Set<String> VARIABLE_SIZE_PATHS =
Set.of(".chainId", ".to", ".payload", ".maybeAccessList");
@Test
public void toSize() {
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10);
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10, 0);
Transaction txTo =
preparedTx.to(Optional.of(Address.extract(Bytes32.random()))).createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txTo.writeTo(rlpOut);
txTo = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
txTo = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txTo.getSender());
System.out.println(txTo.getHash());
System.out.println(txTo.getSize());
@ -78,34 +89,17 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
GraphVisitor gv =
gpr -> {
// byte[] is shared so only count the specific part for each field
if (gpr.path().endsWith(".bytes")) {
if (gpr.path().contains("delegate")) {
size.add(20);
System.out.println(
"("
+ size
+ ")[20 = fixed address size; overrides: "
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
} else {
size.add(gpr.size());
System.out.println(
"("
+ size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
size.add(gpr.size());
System.out.println(
"("
+ size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
};
GraphWalker gw = new GraphWalker(gv);
@ -121,12 +115,13 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
public void payloadSize() {
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10);
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10, 0);
Transaction txPayload = preparedTx.createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txPayload.writeTo(rlpOut);
txPayload = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
txPayload =
Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txPayload.getSender());
System.out.println(txPayload.getHash());
System.out.println(txPayload.getSize());
@ -141,16 +136,141 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
assertThat(size.sum()).isEqualTo(PendingTransaction.PAYLOAD_BASE_MEMORY_SIZE);
}
@Test
public void chainIdSize() {
BigInteger chainId = BigInteger.valueOf(1);
Optional<BigInteger> maybeChainId = Optional.of(chainId);
final ClassLayout cl = ClassLayout.parseInstance(maybeChainId);
System.out.println(cl.toPrintable());
LongAdder size = new LongAdder();
size.add(cl.instanceSize());
System.out.println("Base chainId size: " + size);
GraphVisitor gv =
gpr -> {
size.add(gpr.size());
System.out.println(
"("
+ size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
};
GraphWalker gw = new GraphWalker(gv);
gw.walk(maybeChainId);
assertThat(size.sum()).isEqualTo(PendingTransaction.OPTIONAL_CHAIN_ID_MEMORY_SIZE);
}
@Test
public void kgzCommitmentsSize() {
blobsWithCommitmentsFieldSize(
t -> t.getBlobsWithCommitments().get().getKzgCommitments(),
PendingTransaction.BASE_LIST_SIZE,
PendingTransaction.KZG_COMMITMENT_OR_PROOF_SIZE);
}
@Test
public void kgzProofsSize() {
blobsWithCommitmentsFieldSize(
t -> t.getBlobsWithCommitments().get().getKzgProofs(),
PendingTransaction.BASE_LIST_SIZE,
PendingTransaction.KZG_COMMITMENT_OR_PROOF_SIZE);
}
@Test
public void blobsSize() {
blobsWithCommitmentsFieldSize(
t -> t.getBlobsWithCommitments().get().getBlobs(),
PendingTransaction.BASE_LIST_SIZE,
PendingTransaction.BLOB_SIZE);
}
@Test
public void versionedHashesSize() {
blobsWithCommitmentsFieldSize(
t -> t.getBlobsWithCommitments().get().getVersionedHashes(),
PendingTransaction.BASE_LIST_SIZE,
PendingTransaction.VERSIONED_HASH_SIZE);
}
private void blobsWithCommitmentsFieldSize(
final Function<Transaction, List<? extends Object>> containerExtractor,
final long containerSize,
final long itemSize) {
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), 10, 1);
Transaction txBlob = preparedTx.createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION);
txBlob =
TransactionDecoder.decodeRLP(
new BytesValueRLPInput(rlpOut.encoded(), false), EncodingContext.POOLED_TRANSACTION)
.detachedCopy();
System.out.println(txBlob.getSender());
System.out.println(txBlob.getHash());
System.out.println(txBlob.getSize());
final List<? extends Object> list = containerExtractor.apply(txBlob);
final long cSize = sizeOfField(list, ".elements[");
System.out.println("Container size: " + cSize);
assertThat(cSize).isEqualTo(containerSize);
final Object item = list.get(0);
final long iSize = sizeOfField(item);
System.out.println("Item size: " + iSize);
assertThat(iSize).isEqualTo(itemSize);
}
@Test
public void blobsWithCommitmentsSize() {
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.BLOB, 10, Wei.of(500), 10, 1);
Transaction txBlob = preparedTx.createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
TransactionEncoder.encodeRLP(txBlob, rlpOut, EncodingContext.POOLED_TRANSACTION);
txBlob =
TransactionDecoder.decodeRLP(
new BytesValueRLPInput(rlpOut.encoded(), false), EncodingContext.POOLED_TRANSACTION)
.detachedCopy();
System.out.println(txBlob.getSender());
System.out.println(txBlob.getHash());
System.out.println(txBlob.getSize());
final BlobsWithCommitments bwc = txBlob.getBlobsWithCommitments().get();
final ClassLayout cl = ClassLayout.parseInstance(bwc);
System.out.println(cl.toPrintable());
System.out.println("BlobsWithCommitments size: " + cl.instanceSize());
assertThat(cl.instanceSize()).isEqualTo(PendingTransaction.BLOBS_WITH_COMMITMENTS_SIZE);
}
@Test
public void pendingTransactionSize() {
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10);
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10, 0);
Transaction txPayload = preparedTx.createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txPayload.writeTo(rlpOut);
txPayload = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
txPayload =
Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txPayload.getSender());
System.out.println(txPayload.getHash());
System.out.println(txPayload.getSize());
@ -176,12 +296,13 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
final List<AccessListEntry> ales = List.of(ale1);
TransactionTestFixture preparedTx =
prepareTransaction(TransactionType.ACCESS_LIST, 0, Wei.of(500), 0);
prepareTransaction(TransactionType.ACCESS_LIST, 0, Wei.of(500), 0, 0);
Transaction txAccessList = preparedTx.accessList(ales).createTransaction(KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txAccessList.writeTo(rlpOut);
txAccessList = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
txAccessList =
Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txAccessList.getSender());
System.out.println(txAccessList.getHash());
System.out.println(txAccessList.getSize());
@ -200,55 +321,11 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
final AccessListEntry ale = optAL.get().get(0);
final ClassLayout cl3 = ClassLayout.parseInstance(ale);
System.out.println(cl3.toPrintable());
System.out.println("AccessListEntry size: " + cl3.instanceSize());
LongAdder size = new LongAdder();
size.add(cl3.instanceSize());
GraphVisitor gv =
gpr -> {
// byte[] is shared so only count the specific part for each field
if (gpr.path().endsWith(".bytes")) {
if (gpr.path().contains("address")) {
size.add(20);
System.out.println(
"("
+ size
+ ")[20 = fixed address size; overrides: "
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
} else if (!gpr.path()
.contains(
"storageKeys.elementData[")) { // exclude elements since we want the container
// size
size.add(gpr.size());
System.out.println(
"("
+ size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
};
GraphWalker gw = new GraphWalker(gv);
gw.walk(ale);
long aleSize = sizeOfField(ale, "storageKeys.elementData[");
System.out.println("AccessListEntry container size: " + size);
System.out.println("AccessListEntry container size: " + aleSize);
assertThat(size.sum()).isEqualTo(PendingTransaction.ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE);
assertThat(aleSize).isEqualTo(PendingTransaction.ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE);
final Bytes32 storageKey = ale.storageKeys().get(0);
final ClassLayout cl4 = ClassLayout.parseInstance(storageKey);
@ -260,13 +337,14 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
}
@Test
public void baseEIP1559TransactionMemorySize() {
public void baseEIP1559AndEIP4844TransactionMemorySize() {
System.setProperty("jol.magicFieldOffset", "true");
Transaction txEip1559 = createEIP1559Transaction(1, KEYS1, 10);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txEip1559.writeTo(rlpOut);
txEip1559 = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
txEip1559 =
Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txEip1559.getSender());
System.out.println(txEip1559.getHash());
System.out.println(txEip1559.getSize());
@ -277,138 +355,141 @@ public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPo
eip1559size.add(cl.instanceSize());
System.out.println(eip1559size);
final Set<String> skipPrefixes = new HashSet<>();
GraphVisitor gv =
gpr -> {
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) {
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) {
skipPrefixes.add(gpr.path());
} else if (!startWithAnyOf(EIP1559_CONSTANT_FIELD_PATHS, gpr)
&& !startWithAnyOf(EIP1559_VARIABLE_SIZE_PATHS, gpr)) {
eip1559size.add(gpr.size());
System.out.println(
"("
+ eip1559size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
}
};
GraphWalker gw = new GraphWalker(gv);
final SortedSet<FieldSize> fieldSizes = new TreeSet<>();
GraphWalker gw = getGraphWalker(EIP1559_EIP4844_CONSTANT_FIELD_PATHS, fieldSizes);
gw.walk(txEip1559);
fieldSizes.forEach(
fieldSize -> {
eip1559size.add(fieldSize.size());
System.out.println(
"("
+ eip1559size
+ ")["
+ fieldSize.size()
+ ", "
+ fieldSize.path()
+ ", "
+ fieldSize
+ "]");
});
System.out.println("Base EIP1559 size: " + eip1559size);
assertThat(eip1559size.sum()).isEqualTo(PendingTransaction.EIP1559_BASE_MEMORY_SIZE);
assertThat(eip1559size.sum())
.isEqualTo(PendingTransaction.EIP1559_AND_EIP4844_BASE_MEMORY_SIZE);
}
@Test
public void baseAccessListTransactionMemorySize() {
public void baseFrontierAndAccessListTransactionMemorySize() {
System.setProperty("jol.magicFieldOffset", "true");
Transaction txAccessList =
createTransaction(TransactionType.ACCESS_LIST, 1, Wei.of(500), 0, KEYS1);
Transaction txFrontier = createTransaction(TransactionType.FRONTIER, 1, Wei.of(500), 0, KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txAccessList.writeTo(rlpOut);
txFrontier.writeTo(rlpOut);
txAccessList = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
System.out.println(txAccessList.getSender());
System.out.println(txAccessList.getHash());
System.out.println(txAccessList.getSize());
txFrontier =
Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)).detachedCopy();
System.out.println(txFrontier.getSender());
System.out.println(txFrontier.getHash());
System.out.println(txFrontier.getSize());
final ClassLayout cl = ClassLayout.parseInstance(txAccessList);
final ClassLayout cl = ClassLayout.parseInstance(txFrontier);
System.out.println(cl.toPrintable());
LongAdder accessListSize = new LongAdder();
accessListSize.add(cl.instanceSize());
System.out.println(accessListSize);
LongAdder frontierSize = new LongAdder();
frontierSize.add(cl.instanceSize());
System.out.println(frontierSize);
final Set<String> skipPrefixes = new HashSet<>();
final SortedSet<FieldSize> fieldSizes = new TreeSet<>();
GraphWalker gw = getGraphWalker(FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS, fieldSizes);
gw.walk(txFrontier);
fieldSizes.forEach(
fieldSize -> {
frontierSize.add(fieldSize.size());
System.out.println(
"("
+ frontierSize
+ ")["
+ fieldSize.size()
+ ", "
+ fieldSize.path()
+ ", "
+ fieldSize
+ "]");
});
System.out.println("Base Frontier size: " + frontierSize);
assertThat(frontierSize.sum())
.isEqualTo(PendingTransaction.FRONTIER_AND_ACCESS_LIST_BASE_MEMORY_SIZE);
}
private GraphWalker getGraphWalker(
final Set<String> constantFieldPaths, final SortedSet<FieldSize> fieldSizes) {
final Set<String> skipPrefixes = new HashSet<>();
GraphVisitor gv =
gpr -> {
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) {
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) {
skipPrefixes.add(gpr.path());
} else if (!startWithAnyOf(FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS, gpr)
&& !startWithAnyOf(FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS, gpr)) {
accessListSize.add(gpr.size());
System.out.println(
"("
+ accessListSize
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
} else if (!startWithAnyOf(constantFieldPaths, gpr)
&& !startWithAnyOf(VARIABLE_SIZE_PATHS, gpr)) {
fieldSizes.add(new FieldSize(gpr.path(), gpr.klass(), gpr.size()));
}
}
};
GraphWalker gw = new GraphWalker(gv);
gw.walk(txAccessList);
System.out.println("Base Access List size: " + accessListSize);
assertThat(accessListSize.sum()).isEqualTo(PendingTransaction.ACCESS_LIST_BASE_MEMORY_SIZE);
return gw;
}
@Test
public void baseFrontierTransactionMemorySize() {
System.setProperty("jol.magicFieldOffset", "true");
Transaction txFrontier = createTransaction(TransactionType.FRONTIER, 1, Wei.of(500), 0, KEYS1);
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
txFrontier.writeTo(rlpOut);
txFrontier = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false));
System.out.println(txFrontier.getSender());
System.out.println(txFrontier.getHash());
System.out.println(txFrontier.getSize());
private boolean startWithAnyOf(final Set<String> prefixes, final GraphPathRecord path) {
return prefixes.stream().anyMatch(prefix -> path.path().startsWith(prefix));
}
final ClassLayout cl = ClassLayout.parseInstance(txFrontier);
private long sizeOfField(final Object container, final String... excludePaths) {
final ClassLayout cl = ClassLayout.parseInstance(container);
System.out.println(cl.toPrintable());
LongAdder frontierSize = new LongAdder();
frontierSize.add(cl.instanceSize());
System.out.println(frontierSize);
System.out.println("Base container size: " + cl.instanceSize());
final Set<String> skipPrefixes = new HashSet<>();
LongAdder size = new LongAdder();
size.add(cl.instanceSize());
GraphVisitor gv =
gpr -> {
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) {
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) {
skipPrefixes.add(gpr.path());
} else if (!startWithAnyOf(FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS, gpr)
&& !startWithAnyOf(FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS, gpr)) {
frontierSize.add(gpr.size());
System.out.println(
"("
+ frontierSize
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
if (Arrays.stream(excludePaths)
.anyMatch(excludePath -> gpr.path().contains(excludePath))) {
System.out.println("Excluded path " + gpr.path());
} else {
size.add(gpr.size());
System.out.println(
"("
+ size
+ ")["
+ gpr.size()
+ ", "
+ gpr.path()
+ ", "
+ gpr.klass().toString()
+ "]");
}
};
GraphWalker gw = new GraphWalker(gv);
gw.walk(txFrontier);
System.out.println("Base Frontier size: " + frontierSize);
assertThat(frontierSize.sum()).isEqualTo(PendingTransaction.FRONTIER_BASE_MEMORY_SIZE);
gw.walk(container);
System.out.println("Container size: " + size);
return size.sum();
}
private boolean startWithAnyOf(final Set<String> prefixes, final GraphPathRecord path) {
return prefixes.stream().anyMatch(prefix -> path.path().startsWith(prefix));
record FieldSize(String path, Class<?> clazz, long size) implements Comparable<FieldSize> {
@Override
public int compareTo(final FieldSize o) {
return path.compareTo(o.path);
}
}
}

@ -20,7 +20,13 @@ 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.Hash;
import org.hyperledger.besu.datatypes.KZGCommitment;
import org.hyperledger.besu.datatypes.KZGProof;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.VersionedHash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
@ -33,10 +39,12 @@ import org.hyperledger.besu.metrics.StubMetricsSystem;
import java.util.Optional;
import java.util.Random;
import java.util.stream.IntStream;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes48;
public class BaseTransactionPoolTest {
@ -82,6 +90,12 @@ public class BaseTransactionPoolTest {
TransactionType.EIP1559, nonce, Wei.of(5000L).multiply(gasFeeMultiplier), 0, keys);
}
protected Transaction createEIP4844Transaction(
final long nonce, final KeyPair keys, final int gasFeeMultiplier, final int blobCount) {
return createTransaction(
TransactionType.BLOB, nonce, Wei.of(5000L).multiply(gasFeeMultiplier), 0, blobCount, keys);
}
protected Transaction createTransaction(
final long nonce, final Wei maxGasPrice, final int payloadSize, final KeyPair keys) {
@ -97,11 +111,26 @@ public class BaseTransactionPoolTest {
final Wei maxGasPrice,
final int payloadSize,
final KeyPair keys) {
return prepareTransaction(type, nonce, maxGasPrice, payloadSize).createTransaction(keys);
return createTransaction(type, nonce, maxGasPrice, payloadSize, 0, keys);
}
protected Transaction createTransaction(
final TransactionType type,
final long nonce,
final Wei maxGasPrice,
final int payloadSize,
final int blobCount,
final KeyPair keys) {
return prepareTransaction(type, nonce, maxGasPrice, payloadSize, blobCount)
.createTransaction(keys);
}
protected TransactionTestFixture prepareTransaction(
final TransactionType type, final long nonce, final Wei maxGasPrice, final int payloadSize) {
final TransactionType type,
final long nonce,
final Wei maxGasPrice,
final int payloadSize,
final int blobCount) {
var tx =
new TransactionTestFixture()
@ -116,6 +145,24 @@ public class BaseTransactionPoolTest {
if (type.supports1559FeeMarket()) {
tx.maxFeePerGas(Optional.of(maxGasPrice))
.maxPriorityFeePerGas(Optional.of(maxGasPrice.divide(10)));
if (type.supportsBlob() && blobCount > 0) {
final var versionHashes =
IntStream.range(0, blobCount)
.mapToObj(i -> new VersionedHash((byte) 1, Hash.ZERO))
.toList();
final var kgzCommitments =
IntStream.range(0, blobCount)
.mapToObj(i -> new KZGCommitment(Bytes48.random()))
.toList();
final var kzgProofs =
IntStream.range(0, blobCount).mapToObj(i -> new KZGProof(Bytes48.random())).toList();
final var blobs =
IntStream.range(0, blobCount).mapToObj(i -> new Blob(Bytes.random(32 * 4096))).toList();
tx.versionedHashes(Optional.of(versionHashes));
final var blobsWithCommitments =
new BlobsWithCommitments(kgzCommitments, blobs, kzgProofs, versionHashes);
tx.blobsWithCommitments(Optional.of(blobsWithCommitments));
}
} else {
tx.gasPrice(maxGasPrice);
}

@ -20,6 +20,7 @@ import static org.hyperledger.besu.ethereum.eth.transactions.layered.Transaction
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.BlockHeader;
@ -103,6 +104,7 @@ public class ReplayTest {
@Test
@Disabled("Provide a replay file to run the test on demand")
public void replay() throws IOException {
SignatureAlgorithmFactory.setDefaultInstance();
try (BufferedReader br =
new BufferedReader(
new InputStreamReader(

Loading…
Cancel
Save