Transaction.java
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.core;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.hyperledger.besu.crypto.Hash.keccak256;
import static org.hyperledger.besu.datatypes.VersionedHash.SHA256_VERSION_ID;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SECPPublicKey;
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.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.Sha256Hash;
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.AccessListTransactionEncoder;
import org.hyperledger.besu.ethereum.core.encoding.BlobTransactionEncoder;
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.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import org.hyperledger.besu.ethereum.rlp.RLPOutput;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.primitives.Longs;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;
import org.apache.tuweni.units.bigints.UInt256s;
/** An operation submitted by an external actor to be applied to the system. */
public class Transaction
implements org.hyperledger.besu.datatypes.Transaction,
org.hyperledger.besu.plugin.data.UnsignedPrivateMarkerTransaction {
// Used for transactions that are not tied to a specific chain
// (e.g. does not have a chain id associated with it).
public static final BigInteger REPLAY_UNPROTECTED_V_BASE = BigInteger.valueOf(27);
public static final BigInteger REPLAY_UNPROTECTED_V_BASE_PLUS_1 = BigInteger.valueOf(28);
public static final BigInteger REPLAY_PROTECTED_V_BASE = BigInteger.valueOf(35);
// The v signature parameter starts at 36 because 1 is the first valid chainId so:
// chainId > 1 implies that 2 * chainId + V_BASE > 36.
public static final BigInteger REPLAY_PROTECTED_V_MIN = BigInteger.valueOf(36);
public static final BigInteger TWO = BigInteger.valueOf(2);
private static final Cache<Hash, Address> senderCache =
CacheBuilder.newBuilder().recordStats().maximumSize(100_000L).build();
private final long nonce;
private final Optional<Wei> gasPrice;
private final Optional<Wei> maxPriorityFeePerGas;
private final Optional<Wei> maxFeePerGas;
private final Optional<Wei> maxFeePerBlobGas;
private final long gasLimit;
private final Optional<Address> to;
private final Wei value;
private final SECPSignature signature;
private final Bytes payload;
private final Optional<List<AccessListEntry>> maybeAccessList;
private final Optional<BigInteger> chainId;
// Caches a "hash" of a portion of the transaction used for sender recovery.
// Note that this hash does not include the transaction signature, so it does not
// fully identify the transaction (use the result of the {@code hash()} for that).
// It is only used to compute said signature and recover the sender from it.
private volatile Bytes32 hashNoSignature;
// Caches the transaction sender.
protected volatile Address sender;
// Caches the hash used to uniquely identify the transaction.
protected volatile Hash hash;
// Caches the size in bytes of the encoded transaction.
protected volatile int size = -1;
private final TransactionType transactionType;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance();
private final Optional<List<VersionedHash>> versionedHashes;
private final Optional<BlobsWithCommitments> blobsWithCommitments;
public static Builder builder() {
return new Builder();
}
public static Transaction readFrom(final Bytes rlpBytes) {
return readFrom(RLP.input(rlpBytes));
}
public static Transaction readFrom(final RLPInput rlpInput) {
return TransactionDecoder.decodeRLP(rlpInput, EncodingContext.BLOCK_BODY);
}
/**
* 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
* @param maxPriorityFeePerGas the max priority fee per gas
* @param maxFeePerGas the max fee per gas
* @param maxFeePerBlobGas the max fee per blob gas
* @param gasLimit the gas limit
* @param to the transaction recipient
* @param value the value being transferred to the recipient
* @param signature the signature
* @param payload the payload
* @param maybeAccessList the optional list of addresses/storage slots this transaction intends to
* preload
* @param sender the transaction sender
* @param chainId the chain id to apply the transaction to
* <p>The {@code to} will be an {@code Optional.empty()} for a contract creation transaction;
* otherwise it should contain an address.
* <p>The {@code chainId} must be greater than 0 to be applied to a specific chain; otherwise
* it will default to any chain.
*/
private Transaction(
final boolean forCopy,
final TransactionType transactionType,
final long nonce,
final Optional<Wei> gasPrice,
final Optional<Wei> maxPriorityFeePerGas,
final Optional<Wei> maxFeePerGas,
final Optional<Wei> maxFeePerBlobGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final SECPSignature signature,
final Bytes payload,
final Optional<List<AccessListEntry>> maybeAccessList,
final Address sender,
final Optional<BigInteger> chainId,
final Optional<List<VersionedHash>> versionedHashes,
final Optional<BlobsWithCommitments> blobsWithCommitments) {
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 (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 (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;
this.nonce = nonce;
this.gasPrice = gasPrice;
this.maxPriorityFeePerGas = maxPriorityFeePerGas;
this.maxFeePerGas = maxFeePerGas;
this.maxFeePerBlobGas = maxFeePerBlobGas;
this.gasLimit = gasLimit;
this.to = to;
this.value = value;
this.signature = signature;
this.payload = payload;
this.maybeAccessList = maybeAccessList;
this.sender = sender;
this.chainId = chainId;
this.versionedHashes = versionedHashes;
this.blobsWithCommitments = blobsWithCommitments;
}
/**
* Returns the transaction nonce.
*
* @return the transaction nonce
*/
@Override
public long getNonce() {
return nonce;
}
/**
* Return the transaction gas price.
*
* @return the transaction gas price
*/
@Override
public Optional<Wei> getGasPrice() {
return gasPrice;
}
/**
* Return the transaction max priority per gas.
*
* @return the transaction max priority per gas
*/
@Override
public Optional<Wei> getMaxPriorityFeePerGas() {
return maxPriorityFeePerGas;
}
/**
* Return the transaction max fee per gas.
*
* @return the transaction max fee per gas
*/
@Override
public Optional<Wei> getMaxFeePerGas() {
return maxFeePerGas;
}
/**
* Return the transaction max fee per blob gas.
*
* @return the transaction max fee per blob gas
*/
@Override
public Optional<Wei> getMaxFeePerBlobGas() {
return maxFeePerBlobGas;
}
/**
* Return the effective priority fee per gas for this transaction.
*
* @param maybeBaseFee base fee in case of EIP-1559 transaction
* @return priority fee per gas in wei
*/
public Wei getEffectivePriorityFeePerGas(final Optional<Wei> maybeBaseFee) {
return maybeBaseFee
.map(
baseFee -> {
if (getType().supports1559FeeMarket()) {
if (baseFee.greaterOrEqualThan(getMaxFeePerGas().get())) {
return Wei.ZERO;
}
return UInt256s.min(
getMaxPriorityFeePerGas().get(), getMaxFeePerGas().get().subtract(baseFee));
} else {
if (baseFee.greaterOrEqualThan(getGasPrice().get())) {
return Wei.ZERO;
}
return getGasPrice().get().subtract(baseFee);
}
})
.orElseGet(() -> getGasPrice().orElse(Wei.ZERO));
}
/**
* Returns the transaction gas limit.
*
* @return the transaction gas limit
*/
@Override
public long getGasLimit() {
return gasLimit;
}
/**
* Returns the number of blobs this transaction has, or 0 if not a blob transaction type
*
* @return return the count
*/
public int getBlobCount() {
return versionedHashes.map(List::size).orElse(0);
}
/**
* Returns the transaction recipient.
*
* <p>The {@code Optional<Address>} will be {@code Optional.empty()} if the transaction is a
* contract creation; otherwise it will contain the message call transaction recipient.
*
* @return the transaction recipient if a message call; otherwise {@code Optional.empty()}
*/
@Override
public Optional<Address> getTo() {
return to;
}
/**
* Returns the value transferred in the transaction.
*
* @return the value transferred in the transaction
*/
@Override
public Wei getValue() {
return value;
}
/**
* Returns the signature used to sign the transaction.
*
* @return the signature used to sign the transaction
*/
public SECPSignature getSignature() {
return signature;
}
/**
* Returns the transaction payload.
*
* @return the transaction payload
*/
@Override
public Bytes getPayload() {
return payload;
}
/**
* Returns the payload if this is a contract creation transaction.
*
* @return if present the init code
*/
@Override
public Optional<Bytes> getInit() {
return getTo().isPresent() ? Optional.empty() : Optional.of(payload);
}
/**
* Returns the payload if this is a message call transaction.
*
* @return if present the init code
*/
@Override
public Optional<Bytes> getData() {
return getTo().isPresent() ? Optional.of(payload) : Optional.empty();
}
@Override
public Optional<List<AccessListEntry>> getAccessList() {
return maybeAccessList;
}
/**
* Return the transaction chain id (if it exists)
*
* <p>The {@code OptionalInt} will be {@code OptionalInt.empty()} if the transaction is not tied
* to a specific chain.
*
* @return the transaction chain id if it exists; otherwise {@code OptionalInt.empty()}
*/
@Override
public Optional<BigInteger> getChainId() {
return chainId;
}
/**
* Returns the transaction sender.
*
* @return the transaction sender
*/
@Override
public Address getSender() {
if (sender == null) {
Optional<Address> cachedSender = Optional.ofNullable(senderCache.getIfPresent(getHash()));
sender = cachedSender.orElseGet(this::computeSender);
}
return sender;
}
private Address computeSender() {
final SECPPublicKey publicKey =
signatureAlgorithm
.recoverPublicKeyFromSignature(getOrComputeSenderRecoveryHash(), signature)
.orElseThrow(
() ->
new IllegalStateException(
"Cannot recover public key from signature for " + this));
final Address calculatedSender = Address.extract(Hash.hash(publicKey.getEncodedBytes()));
senderCache.put(this.hash, calculatedSender);
return calculatedSender;
}
/**
* Returns the public key extracted from the signature.
*
* @return the public key
*/
public Optional<String> getPublicKey() {
return signatureAlgorithm
.recoverPublicKeyFromSignature(getOrComputeSenderRecoveryHash(), signature)
.map(SECPPublicKey::toString);
}
private Bytes32 getOrComputeSenderRecoveryHash() {
if (hashNoSignature == null) {
hashNoSignature =
computeSenderRecoveryHash(
transactionType,
nonce,
gasPrice.orElse(null),
maxPriorityFeePerGas.orElse(null),
maxFeePerGas.orElse(null),
maxFeePerBlobGas.orElse(null),
gasLimit,
to,
value,
payload,
maybeAccessList,
versionedHashes.orElse(null),
chainId);
}
return hashNoSignature;
}
/**
* Writes the transaction to RLP
*
* @param out the output to write the transaction to
*/
public void writeTo(final RLPOutput out) {
TransactionEncoder.encodeRLP(this, out, EncodingContext.BLOCK_BODY);
}
@Override
public Bytes encoded() {
final BytesValueRLPOutput rplOutput = new BytesValueRLPOutput();
writeTo(rplOutput);
return rplOutput.encoded();
}
@Override
public BigInteger getR() {
return signature.getR();
}
@Override
public BigInteger getS() {
return signature.getS();
}
@Override
public BigInteger getV() {
if (transactionType != null
&& transactionType != TransactionType.FRONTIER
&& transactionType != TransactionType.ACCESS_LIST
&& transactionType != TransactionType.EIP1559) {
// Newer transaction type lacks V, so return null
return null;
} else {
// Mandatory for legacy, optional for EIP-2930 and EIP-1559 TXes, prohibited for all others.
final BigInteger recId = BigInteger.valueOf(signature.getRecId());
return chainId
.map(bigInteger -> recId.add(REPLAY_PROTECTED_V_BASE).add(TWO.multiply(bigInteger)))
.orElseGet(() -> recId.add(REPLAY_UNPROTECTED_V_BASE));
}
}
@Override
public BigInteger getYParity() {
if (transactionType != null && transactionType != TransactionType.FRONTIER) {
// EIP-2718 typed transaction, return yParity:
return BigInteger.valueOf(signature.getRecId());
} else {
// legacy types never return yParity
return null;
}
}
/**
* Returns the transaction hash.
*
* @return the transaction hash
*/
@Override
public Hash getHash() {
if (hash == null) {
memoizeHashAndSize();
}
return hash;
}
/**
* Returns the size in bytes of the encoded transaction.
*
* @return the size in bytes of the encoded transaction.
*/
@Override
public int getSize() {
if (size == -1) {
memoizeHashAndSize();
}
return size;
}
private void memoizeHashAndSize() {
final Bytes bytes = TransactionEncoder.encodeOpaqueBytes(this, EncodingContext.BLOCK_BODY);
hash = Hash.hash(bytes);
if (transactionType.supportsBlob() && getBlobsWithCommitments().isPresent()) {
final Bytes pooledBytes =
TransactionEncoder.encodeOpaqueBytes(this, EncodingContext.POOLED_TRANSACTION);
size = pooledBytes.size();
return;
}
size = bytes.size();
}
/**
* Returns whether the transaction is a contract creation
*
* @return {@code true} if this is a contract-creation transaction; otherwise {@code false}
*/
public boolean isContractCreation() {
return getTo().isEmpty();
}
/**
* Calculates the max up-front cost for the gas the transaction can use.
*
* @return the max up-front cost for the gas the transaction can use.
*/
private Wei getMaxUpfrontGasCost(final long blobGasPerBlock) {
return getUpfrontGasCost(
getMaxGasPrice(), getMaxFeePerBlobGas().orElse(Wei.ZERO), blobGasPerBlock);
}
/**
* Calculates the up-front cost for the gas and blob gas the transaction can use.
*
* @param gasPrice the gas price to use
* @param blobGasPrice the blob gas price to use
* @return the up-front cost for the gas the transaction can use.
*/
public Wei getUpfrontGasCost(
final Wei gasPrice, final Wei blobGasPrice, final long totalBlobGas) {
if (gasPrice == null || gasPrice.isZero()) {
return Wei.ZERO;
}
final var cost = calculateUpfrontGasCost(gasPrice, blobGasPrice, totalBlobGas);
if (cost.bitLength() > 256) {
return Wei.MAX_WEI;
} else {
return Wei.of(cost);
}
}
public BigInteger calculateUpfrontGasCost(
final Wei gasPrice, final Wei blobGasPrice, final long totalBlobGas) {
var cost =
new BigInteger(1, Longs.toByteArray(getGasLimit())).multiply(gasPrice.getAsBigInteger());
if (transactionType.supportsBlob()) {
cost = cost.add(blobGasPrice.getAsBigInteger().multiply(BigInteger.valueOf(totalBlobGas)));
}
return cost;
}
/**
* Calculates the up-front cost for the transaction.
*
* <p>The up-front cost is paid by the sender account before the transaction is executed. The
* sender must have the amount in its account balance to execute and some of this amount may be
* refunded after the transaction has executed.
*
* @return the up-front gas cost for the transaction
*/
public Wei getUpfrontCost(final long totalBlobGas) {
Wei maxUpfrontGasCost = getMaxUpfrontGasCost(totalBlobGas);
Wei result = maxUpfrontGasCost.add(getValue());
return (maxUpfrontGasCost.compareTo(result) > 0) ? Wei.MAX_WEI : result;
}
/**
* Return the maximum fee per gas the sender is willing to pay for this transaction.
*
* @return max fee per gas in wei
*/
public Wei getMaxGasPrice() {
return maxFeePerGas.orElseGet(
() ->
gasPrice.orElseThrow(
() ->
new IllegalStateException(
"Transaction requires either gasPrice or maxFeePerGas")));
}
/**
* Calculates the effectiveGasPrice of a transaction on the basis of an {@code Optional<Long>}
* baseFee and handles unwrapping Optional fee parameters. If baseFee is present, effective gas is
* calculated as:
*
* <p>min((baseFeePerGas + maxPriorityFeePerGas), maxFeePerGas)
*
* <p>Otherwise, return gasPrice for legacy transactions.
*
* @param baseFeePerGas optional baseFee from the block header, if we are post-london
* @return the effective gas price.
*/
public final Wei getEffectiveGasPrice(final Optional<Wei> baseFeePerGas) {
return getEffectivePriorityFeePerGas(baseFeePerGas).addExact(baseFeePerGas.orElse(Wei.ZERO));
}
@Override
public TransactionType getType() {
return this.transactionType;
}
@Override
public Optional<List<VersionedHash>> getVersionedHashes() {
return versionedHashes;
}
@Override
public Optional<BlobsWithCommitments> getBlobsWithCommitments() {
return blobsWithCommitments;
}
/**
* Return the list of transaction hashes extracted from the collection of Transaction passed as
* argument
*
* @param transactions a collection of transactions
* @return the list of transaction hashes
*/
public static List<Hash> toHashList(final Collection<Transaction> transactions) {
return transactions.stream().map(Transaction::getHash).toList();
}
private static Bytes32 computeSenderRecoveryHash(
final TransactionType transactionType,
final long nonce,
final Wei gasPrice,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final Wei maxFeePerBlobGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<List<AccessListEntry>> accessList,
final List<VersionedHash> versionedHashes,
final Optional<BigInteger> chainId) {
if (transactionType.requiresChainId()) {
checkArgument(chainId.isPresent(), "Transaction type %s requires chainId", transactionType);
}
final Bytes preimage =
switch (transactionType) {
case FRONTIER -> frontierPreimage(nonce, gasPrice, gasLimit, to, value, payload, chainId);
case EIP1559 ->
eip1559Preimage(
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
to,
value,
payload,
chainId,
accessList);
case BLOB ->
blobPreimage(
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
maxFeePerBlobGas,
gasLimit,
to,
value,
payload,
chainId,
accessList,
versionedHashes);
case ACCESS_LIST ->
accessListPreimage(
nonce,
gasPrice,
gasLimit,
to,
value,
payload,
accessList.orElseThrow(
() ->
new IllegalStateException(
"Developer error: the transaction should be guaranteed to have an access list here")),
chainId);
};
return keccak256(preimage);
}
private static Bytes frontierPreimage(
final long nonce,
final Wei gasPrice,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<BigInteger> chainId) {
return RLP.encode(
rlpOutput -> {
rlpOutput.startList();
rlpOutput.writeLongScalar(nonce);
rlpOutput.writeUInt256Scalar(gasPrice);
rlpOutput.writeLongScalar(gasLimit);
rlpOutput.writeBytes(to.map(Bytes::copy).orElse(Bytes.EMPTY));
rlpOutput.writeUInt256Scalar(value);
rlpOutput.writeBytes(payload);
if (chainId.isPresent()) {
rlpOutput.writeBigIntegerScalar(chainId.get());
rlpOutput.writeUInt256Scalar(UInt256.ZERO);
rlpOutput.writeUInt256Scalar(UInt256.ZERO);
}
rlpOutput.endList();
});
}
private static Bytes eip1559Preimage(
final long nonce,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<BigInteger> chainId,
final Optional<List<AccessListEntry>> accessList) {
final Bytes encoded =
RLP.encode(
rlpOutput -> {
rlpOutput.startList();
eip1559PreimageFields(
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
to,
value,
payload,
chainId,
accessList,
rlpOutput);
rlpOutput.endList();
});
return Bytes.concatenate(Bytes.of(TransactionType.EIP1559.getSerializedType()), encoded);
}
private static void eip1559PreimageFields(
final long nonce,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<BigInteger> chainId,
final Optional<List<AccessListEntry>> accessList,
final RLPOutput rlpOutput) {
rlpOutput.writeBigIntegerScalar(chainId.orElseThrow());
rlpOutput.writeLongScalar(nonce);
rlpOutput.writeUInt256Scalar(maxPriorityFeePerGas);
rlpOutput.writeUInt256Scalar(maxFeePerGas);
rlpOutput.writeLongScalar(gasLimit);
rlpOutput.writeBytes(to.map(Bytes::copy).orElse(Bytes.EMPTY));
rlpOutput.writeUInt256Scalar(value);
rlpOutput.writeBytes(payload);
AccessListTransactionEncoder.writeAccessList(rlpOutput, accessList);
}
private static Bytes blobPreimage(
final long nonce,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final Wei maxFeePerBlobGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<BigInteger> chainId,
final Optional<List<AccessListEntry>> accessList,
final List<VersionedHash> versionedHashes) {
final Bytes encoded =
RLP.encode(
rlpOutput -> {
rlpOutput.startList();
eip1559PreimageFields(
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
to,
value,
payload,
chainId,
accessList,
rlpOutput);
rlpOutput.writeUInt256Scalar(maxFeePerBlobGas);
BlobTransactionEncoder.writeBlobVersionedHashes(rlpOutput, versionedHashes);
rlpOutput.endList();
});
return Bytes.concatenate(Bytes.of(TransactionType.BLOB.getSerializedType()), encoded);
}
private static Bytes accessListPreimage(
final long nonce,
final Wei gasPrice,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final List<AccessListEntry> accessList,
final Optional<BigInteger> chainId) {
final Bytes encode =
RLP.encode(
rlpOutput -> {
rlpOutput.startList();
AccessListTransactionEncoder.encodeAccessListInner(
chainId, nonce, gasPrice, gasLimit, to, value, payload, accessList, rlpOutput);
rlpOutput.endList();
});
return Bytes.concatenate(Bytes.of(TransactionType.ACCESS_LIST.getSerializedType()), encode);
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof Transaction that)) {
return false;
}
return Objects.equals(this.chainId, that.chainId)
&& this.gasLimit == that.gasLimit
&& Objects.equals(this.gasPrice, that.gasPrice)
&& Objects.equals(this.maxPriorityFeePerGas, that.maxPriorityFeePerGas)
&& Objects.equals(this.maxFeePerGas, that.maxFeePerGas)
&& Objects.equals(this.maxFeePerBlobGas, that.maxFeePerBlobGas)
&& this.nonce == that.nonce
&& Objects.equals(this.payload, that.payload)
&& Objects.equals(this.signature, that.signature)
&& Objects.equals(this.to, that.to)
&& Objects.equals(this.value, that.value)
&& Objects.equals(this.getV(), that.getV());
}
@Override
public int hashCode() {
return Objects.hash(
nonce,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
maxFeePerBlobGas,
gasLimit,
to,
value,
payload,
signature,
chainId);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(
transactionType.supportsBlob()
? "Blob"
: isContractCreation() ? "ContractCreation" : "MessageCall")
.append("{");
sb.append("type=").append(getType()).append(", ");
sb.append("nonce=").append(getNonce()).append(", ");
getGasPrice()
.ifPresent(gp -> sb.append("gasPrice=").append(gp.toHumanReadableString()).append(", "));
if (getMaxPriorityFeePerGas().isPresent() && getMaxFeePerGas().isPresent()) {
sb.append("maxPriorityFeePerGas=")
.append(getMaxPriorityFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
sb.append("maxFeePerGas=")
.append(getMaxFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
getMaxFeePerBlobGas()
.ifPresent(
wei ->
sb.append("maxFeePerBlobGas=").append(wei.toHumanReadableString()).append(", "));
}
sb.append("gasLimit=").append(getGasLimit()).append(", ");
if (getTo().isPresent()) sb.append("to=").append(getTo().get()).append(", ");
sb.append("value=").append(getValue()).append(", ");
sb.append("sig=").append(getSignature()).append(", ");
if (chainId.isPresent()) sb.append("chainId=").append(getChainId().get()).append(", ");
if (transactionType.equals(TransactionType.ACCESS_LIST)) {
sb.append("accessList=").append(maybeAccessList).append(", ");
}
if (versionedHashes.isPresent()) {
final List<VersionedHash> vhs = versionedHashes.get();
if (!vhs.isEmpty()) {
sb.append("versionedHashes=[");
sb.append(
vhs.get(0)
.toString()); // can't be empty if present, as this is checked in the constructor
for (int i = 1; i < vhs.size(); i++) {
sb.append(", ").append(vhs.get(i).toString());
}
sb.append("], ");
}
}
if (transactionType.supportsBlob() && this.blobsWithCommitments.isPresent()) {
sb.append("numberOfBlobs=").append(blobsWithCommitments.get().getBlobs().size()).append(", ");
}
sb.append("payload=").append(getPayload());
return sb.append("}").toString();
}
public String toTraceLog() {
final StringBuilder sb = new StringBuilder();
sb.append(getHash()).append("={");
sb.append(
transactionType.supportsBlob()
? "Blob"
: isContractCreation() ? "ContractCreation" : "MessageCall")
.append(", ");
sb.append(getNonce()).append(", ");
sb.append(getSender()).append(", ");
sb.append(getType()).append(", ");
getGasPrice()
.ifPresent(gp -> sb.append("gp: ").append(gp.toHumanReadableString()).append(", "));
if (getMaxPriorityFeePerGas().isPresent() && getMaxFeePerGas().isPresent()) {
sb.append("mf: ")
.append(getMaxFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
sb.append("pf: ")
.append(getMaxPriorityFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
getMaxFeePerBlobGas()
.ifPresent(wei -> sb.append("df: ").append(wei.toHumanReadableString()).append(", "));
}
sb.append("gl: ").append(getGasLimit()).append(", ");
sb.append("v: ").append(getValue().toHumanReadableString()).append(", ");
getTo().ifPresent(t -> sb.append("to: ").append(t));
return sb.append("}").toString();
}
@Override
public Optional<Address> contractAddress() {
if (isContractCreation()) {
return Optional.of(Address.contractAddress(getSender(), getNonce()));
}
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.map(address -> Address.wrap(address.copy()));
final Optional<List<AccessListEntry>> detachedAccessList =
maybeAccessList.map(
accessListEntries ->
accessListEntries.stream().map(this::accessListDetachedCopy).toList());
final Optional<List<VersionedHash>> detachedVersionedHashes =
versionedHashes.map(
hashes -> hashes.stream().map(vh -> new VersionedHash(vh.toBytes().copy())).toList());
final Optional<BlobsWithCommitments> detachedBlobsWithCommitments =
blobsWithCommitments.map(
withCommitments ->
blobsWithCommitmentsDetachedCopy(withCommitments, detachedVersionedHashes.get()));
final var copiedTx =
new Transaction(
true,
transactionType,
nonce,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
maxFeePerBlobGas,
gasLimit,
detachedTo,
value,
signature,
payload.copy(),
detachedAccessList,
sender,
chainId,
detachedVersionedHashes,
detachedBlobsWithCommitments);
// copy also the computed fields, to avoid to recompute them
copiedTx.sender = this.sender;
copiedTx.hash = this.hash;
copiedTx.hashNoSignature = this.hashNoSignature;
copiedTx.size = this.size;
return copiedTx;
}
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());
protected TransactionType transactionType;
protected long nonce = -1L;
protected Wei gasPrice;
protected Wei maxPriorityFeePerGas;
protected Wei maxFeePerGas;
protected Wei maxFeePerBlobGas;
protected long gasLimit = -1L;
protected Optional<Address> to = Optional.empty();
protected Wei value;
protected SECPSignature signature;
protected Bytes payload;
protected Optional<List<AccessListEntry>> accessList = Optional.empty();
protected Address sender;
protected Optional<BigInteger> chainId = Optional.empty();
protected Optional<BigInteger> v = Optional.empty();
protected List<VersionedHash> versionedHashes = null;
private BlobsWithCommitments blobsWithCommitments;
public Builder copiedFrom(final Transaction toCopy) {
this.transactionType = toCopy.transactionType;
this.nonce = toCopy.nonce;
this.gasPrice = toCopy.gasPrice.orElse(null);
this.maxPriorityFeePerGas = toCopy.maxPriorityFeePerGas.orElse(null);
this.maxFeePerGas = toCopy.maxFeePerGas.orElse(null);
this.maxFeePerBlobGas = toCopy.maxFeePerBlobGas.orElse(null);
this.gasLimit = toCopy.gasLimit;
this.to = toCopy.to;
this.value = toCopy.value;
this.signature = toCopy.signature;
this.payload = toCopy.payload;
this.accessList = toCopy.maybeAccessList;
this.sender = toCopy.sender;
this.chainId = toCopy.chainId;
this.versionedHashes = toCopy.versionedHashes.orElse(null);
this.blobsWithCommitments = toCopy.blobsWithCommitments.orElse(null);
return this;
}
public Builder type(final TransactionType transactionType) {
this.transactionType = transactionType;
return this;
}
public Builder chainId(final BigInteger chainId) {
this.chainId = Optional.of(chainId);
return this;
}
public Builder v(final BigInteger v) {
this.v = Optional.of(v);
return this;
}
public Builder gasPrice(final Wei gasPrice) {
this.gasPrice = gasPrice;
return this;
}
public Builder maxPriorityFeePerGas(final Wei maxPriorityFeePerGas) {
this.maxPriorityFeePerGas = maxPriorityFeePerGas;
return this;
}
public Builder maxFeePerGas(final Wei maxFeePerGas) {
this.maxFeePerGas = maxFeePerGas;
return this;
}
public Builder maxFeePerBlobGas(final Wei maxFeePerBlobGas) {
this.maxFeePerBlobGas = maxFeePerBlobGas;
return this;
}
public Builder gasLimit(final long gasLimit) {
this.gasLimit = gasLimit;
return this;
}
public Builder nonce(final long nonce) {
this.nonce = nonce;
return this;
}
public Builder value(final Wei value) {
this.value = value;
return this;
}
public Builder to(final Address to) {
this.to = Optional.ofNullable(to);
return this;
}
public Builder payload(final Bytes payload) {
this.payload = payload;
return this;
}
public Builder accessList(final List<AccessListEntry> accessList) {
this.accessList =
accessList == null
? Optional.empty()
: accessList.isEmpty() ? EMPTY_ACCESS_LIST : Optional.of(accessList);
return this;
}
public Builder sender(final Address sender) {
this.sender = sender;
return this;
}
public Builder signature(final SECPSignature signature) {
this.signature = signature;
return this;
}
public Builder versionedHashes(final List<VersionedHash> versionedHashes) {
this.versionedHashes = versionedHashes;
return this;
}
public Builder guessType() {
if (versionedHashes != null && !versionedHashes.isEmpty()) {
transactionType = TransactionType.BLOB;
} else if (maxPriorityFeePerGas != null || maxFeePerGas != null) {
transactionType = TransactionType.EIP1559;
} else if (accessList.isPresent()) {
transactionType = TransactionType.ACCESS_LIST;
} else {
transactionType = TransactionType.FRONTIER;
}
return this;
}
public TransactionType getTransactionType() {
return transactionType;
}
public Transaction build() {
if (transactionType == null) guessType();
return new Transaction(
false,
transactionType,
nonce,
Optional.ofNullable(gasPrice),
Optional.ofNullable(maxPriorityFeePerGas),
Optional.ofNullable(maxFeePerGas),
Optional.ofNullable(maxFeePerBlobGas),
gasLimit,
to,
value,
signature,
payload,
accessList,
sender,
chainId,
Optional.ofNullable(versionedHashes),
Optional.ofNullable(blobsWithCommitments));
}
public Transaction signAndBuild(final KeyPair keys) {
checkState(
signature == null, "The transaction signature has already been provided to this builder");
signature(computeSignature(keys));
sender(Address.extract(Hash.hash(keys.getPublicKey().getEncodedBytes())));
return build();
}
SECPSignature computeSignature(final KeyPair keys) {
return SignatureAlgorithmFactory.getInstance()
.sign(
computeSenderRecoveryHash(
transactionType,
nonce,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
maxFeePerBlobGas,
gasLimit,
to,
value,
payload,
accessList,
versionedHashes,
chainId),
keys);
}
public Builder kzgBlobs(
final List<KZGCommitment> kzgCommitments,
final List<Blob> blobs,
final List<KZGProof> kzgProofs) {
if (this.versionedHashes == null || this.versionedHashes.isEmpty()) {
this.versionedHashes =
kzgCommitments.stream()
.map(c -> new VersionedHash(SHA256_VERSION_ID, Sha256Hash.sha256(c.getData())))
.toList();
}
this.blobsWithCommitments =
new BlobsWithCommitments(kzgCommitments, blobs, kzgProofs, versionedHashes);
return this;
}
public Builder blobsWithCommitments(final BlobsWithCommitments blobsWithCommitments) {
this.blobsWithCommitments = blobsWithCommitments;
return this;
}
}
}