diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java index c029983a33e..72cfea22eb4 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java @@ -413,7 +413,7 @@ public void maxPrioritizedTxsPerTypeConfigFile() throws IOException { @Test public void maxPrioritizedTxsPerTypeWrongTxType() { internalTestFailure( - "Invalid value for option '--tx-pool-max-prioritized-by-type' (MAP): expected one of [FRONTIER, ACCESS_LIST, EIP1559, BLOB] (case-insensitive) but was 'WRONG_TYPE'", + "Invalid value for option '--tx-pool-max-prioritized-by-type' (MAP): expected one of [FRONTIER, ACCESS_LIST, EIP1559, BLOB, EIP7702] (case-insensitive) but was 'WRONG_TYPE'", "--tx-pool-max-prioritized-by-type", "WRONG_TYPE=1"); } diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/SetCodeAuthorization.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/SetCodeAuthorization.java new file mode 100644 index 00000000000..6edda39ac04 --- /dev/null +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/SetCodeAuthorization.java @@ -0,0 +1,65 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.datatypes; + +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + +import java.math.BigInteger; +import java.util.List; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Suppliers; + +/** + * An access list entry as defined in EIP-7702 + * + * @param chainId can be either the current chain id or zero + * @param address the address from which the code will be set into the EOA account + * @param nonces the list of nonces + * @param signature the signature of the EOA account which will be used to set the code + */ +public record SetCodeAuthorization( + BigInteger chainId, Address address, List nonces, SECPSignature signature) { + + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + /** + * Create access list entry. + * + * @param chainId can be either the current chain id or zero + * @param address the address from which the code will be set into the EOA account + * @param nonces the list of nonces + * @param v the recovery id + * @param r the r value of the signature + * @param s the s value of the signature + * @return SetCodeTransactionEntry + */ + @JsonCreator + public static SetCodeAuthorization createAccessListEntry( + @JsonProperty("chainId") final BigInteger chainId, + @JsonProperty("address") final Address address, + @JsonProperty("nonce") final List nonces, + @JsonProperty("v") final byte v, + @JsonProperty("r") final BigInteger r, + @JsonProperty("s") final BigInteger s) { + return new SetCodeAuthorization( + chainId, address, nonces, SIGNATURE_ALGORITHM.get().createSignature(r, s, v)); + } +} diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java index 4a77a898de9..4704a790f08 100644 --- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java @@ -234,4 +234,7 @@ default Optional getMaxFeePerBlobGas() { * @return the size in bytes of the encoded transaction. */ int getSize(); + + /** Returns the set code transaction payload if this transaction is a 7702 transaction. */ + Optional> getSetCodeTransactionPayloads(); } diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java index 984a4cc7467..db3c08619e5 100644 --- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java @@ -27,7 +27,9 @@ public enum TransactionType { /** Eip1559 transaction type. */ EIP1559(0x02), /** Blob transaction type. */ - BLOB(0x03); + BLOB(0x03), + /** Eip7702 transaction type. */ + SET_CODE(0x04); private static final Set ACCESS_LIST_SUPPORTED_TRANSACTION_TYPES = Set.of(ACCESS_LIST, EIP1559, BLOB); @@ -128,4 +130,13 @@ public boolean requiresChainId() { public boolean supportsBlob() { return this.equals(BLOB); } + + /** + * Does transaction type require code. + * + * @return the boolean + */ + public boolean requiresSetCode() { + return this.equals(SET_CODE); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionCompleteResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionCompleteResult.java index c8260d090c0..3341940f3c3 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionCompleteResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionCompleteResult.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; import org.hyperledger.besu.datatypes.AccessListEntry; +import org.hyperledger.besu.datatypes.SetCodeAuthorization; import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.VersionedHash; import org.hyperledger.besu.datatypes.Wei; @@ -91,6 +92,9 @@ public class TransactionCompleteResult implements TransactionResult { @JsonInclude(JsonInclude.Include.NON_NULL) private final List versionedHashes; + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List setCodeAuthorizationList; + public TransactionCompleteResult(final TransactionWithMetadata tx) { final Transaction transaction = tx.getTransaction(); final TransactionType transactionType = transaction.getType(); @@ -125,7 +129,8 @@ public TransactionCompleteResult(final TransactionWithMetadata tx) { this.yParity = Quantity.create(transaction.getYParity()); this.v = (transactionType == TransactionType.ACCESS_LIST - || transactionType == TransactionType.EIP1559) + || transactionType == TransactionType.EIP1559) + || transactionType == TransactionType.SET_CODE ? Quantity.create(transaction.getYParity()) : null; } @@ -133,6 +138,7 @@ public TransactionCompleteResult(final TransactionWithMetadata tx) { this.r = Quantity.create(transaction.getR()); this.s = Quantity.create(transaction.getS()); this.versionedHashes = transaction.getVersionedHashes().orElse(null); + this.setCodeAuthorizationList = transaction.getSetCodeTransactionPayloads().orElse(null); } @JsonGetter(value = "accessList") @@ -246,4 +252,9 @@ public String getS() { public List getVersionedHashes() { return versionedHashes; } + + @JsonGetter(value = "setCodeTransactionPayloadList") + public List getSetCodeTransactionPayloadList() { + return setCodeAuthorizationList; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java index 7ca349721f9..70c132b9634 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.KZGCommitment; import org.hyperledger.besu.datatypes.KZGProof; +import org.hyperledger.besu.datatypes.SetCodeAuthorization; import org.hyperledger.besu.datatypes.Sha256Hash; import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.VersionedHash; @@ -38,6 +39,7 @@ 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.SetCodeTransactionEncoder; import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder; import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; @@ -122,6 +124,7 @@ public class Transaction private final Optional> versionedHashes; private final Optional blobsWithCommitments; + private final Optional> maybeSetCodeTransactionPayloads; public static Builder builder() { return new Builder(); @@ -177,7 +180,8 @@ private Transaction( final Address sender, final Optional chainId, final Optional> versionedHashes, - final Optional blobsWithCommitments) { + final Optional blobsWithCommitments, + final Optional> maybeSetCodeTransactionPayloads) { if (!forCopy) { if (transactionType.requiresChainId()) { @@ -213,6 +217,12 @@ private Transaction( checkArgument( maxFeePerBlobGas.isPresent(), "Must specify max fee per blob gas for blob transaction"); } + + if (transactionType.requiresSetCode()) { + checkArgument( + maybeSetCodeTransactionPayloads.isPresent(), + "Must specify set code transaction payload for set code transaction"); + } } this.transactionType = transactionType; @@ -231,6 +241,7 @@ private Transaction( this.chainId = chainId; this.versionedHashes = versionedHashes; this.blobsWithCommitments = blobsWithCommitments; + this.maybeSetCodeTransactionPayloads = maybeSetCodeTransactionPayloads; } /** @@ -462,6 +473,7 @@ private Bytes32 getOrComputeSenderRecoveryHash() { payload, maybeAccessList, versionedHashes.orElse(null), + maybeSetCodeTransactionPayloads, chainId); } return hashNoSignature; @@ -668,6 +680,11 @@ public Optional getBlobsWithCommitments() { return blobsWithCommitments; } + @Override + public Optional> getSetCodeTransactionPayloads() { + return maybeSetCodeTransactionPayloads; + } + /** * Return the list of transaction hashes extracted from the collection of Transaction passed as * argument @@ -692,6 +709,7 @@ private static Bytes32 computeSenderRecoveryHash( final Bytes payload, final Optional> accessList, final List versionedHashes, + final Optional> setCodePayloads, final Optional chainId) { if (transactionType.requiresChainId()) { checkArgument(chainId.isPresent(), "Transaction type %s requires chainId", transactionType); @@ -736,6 +754,21 @@ private static Bytes32 computeSenderRecoveryHash( new IllegalStateException( "Developer error: the transaction should be guaranteed to have an access list here")), chainId); + case SET_CODE -> + setCodePreimage( + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + payload, + chainId, + accessList, + setCodePayloads.orElseThrow( + () -> + new IllegalStateException( + "Developer error: the transaction should be guaranteed to have a set code payload here"))); }; return keccak256(preimage); } @@ -873,6 +906,38 @@ private static Bytes accessListPreimage( return Bytes.concatenate(Bytes.of(TransactionType.ACCESS_LIST.getSerializedType()), encode); } + private static Bytes setCodePreimage( + final long nonce, + final Wei maxPriorityFeePerGas, + final Wei maxFeePerGas, + final long gasLimit, + final Optional
to, + final Wei value, + final Bytes payload, + final Optional chainId, + final Optional> accessList, + final List setCodePayloads) { + final Bytes encoded = + RLP.encode( + rlpOutput -> { + rlpOutput.startList(); + eip1559PreimageFields( + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + payload, + chainId, + accessList, + rlpOutput); + SetCodeTransactionEncoder.encodeSetCodeInner(setCodePayloads, rlpOutput); + rlpOutput.endList(); + }); + return Bytes.concatenate(Bytes.of(TransactionType.BLOB.getSerializedType()), encoded); + } + @Override public boolean equals(final Object other) { if (!(other instanceof Transaction that)) { @@ -1040,7 +1105,8 @@ public Transaction detachedCopy() { sender, chainId, detachedVersionedHashes, - detachedBlobsWithCommitments); + detachedBlobsWithCommitments, + maybeSetCodeTransactionPayloads); // copy also the computed fields, to avoid to recompute them copiedTx.sender = this.sender; @@ -1108,6 +1174,7 @@ public static class Builder { protected Optional v = Optional.empty(); protected List versionedHashes = null; private BlobsWithCommitments blobsWithCommitments; + protected Optional> setCodeTransactionPayloads; public Builder copiedFrom(final Transaction toCopy) { this.transactionType = toCopy.transactionType; @@ -1219,6 +1286,8 @@ public Builder guessType() { transactionType = TransactionType.EIP1559; } else if (accessList.isPresent()) { transactionType = TransactionType.ACCESS_LIST; + } else if (setCodeTransactionPayloads.isPresent()) { + transactionType = TransactionType.SET_CODE; } else { transactionType = TransactionType.FRONTIER; } @@ -1248,7 +1317,8 @@ public Transaction build() { sender, chainId, Optional.ofNullable(versionedHashes), - Optional.ofNullable(blobsWithCommitments)); + Optional.ofNullable(blobsWithCommitments), + setCodeTransactionPayloads); } public Transaction signAndBuild(final KeyPair keys) { @@ -1275,6 +1345,7 @@ SECPSignature computeSignature(final KeyPair keys) { payload, accessList, versionedHashes, + setCodeTransactionPayloads, chainId), keys); } @@ -1298,5 +1369,11 @@ public Builder blobsWithCommitments(final BlobsWithCommitments blobsWithCommitme this.blobsWithCommitments = blobsWithCommitments; return this; } + + public Builder setCodeTransactionPayloads( + final List setCodeTransactionEntries) { + this.setCodeTransactionPayloads = Optional.ofNullable(setCodeTransactionEntries); + return this; + } } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoder.java new file mode 100644 index 00000000000..005926f6b3b --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoder.java @@ -0,0 +1,118 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.encoding; + +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.SetCodeAuthorization; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; + +public class SetCodeTransactionDecoder { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + private SetCodeTransactionDecoder() { + // private constructor + } + + public static Transaction decode(final RLPInput input) { + input.enterList(); + final BigInteger chainId = input.readBigIntegerScalar(); + final Transaction.Builder builder = + Transaction.builder() + .type(TransactionType.EIP1559) + .chainId(chainId) + .nonce(input.readLongScalar()) + .maxPriorityFeePerGas(Wei.of(input.readUInt256Scalar())) + .maxFeePerGas(Wei.of(input.readUInt256Scalar())) + .gasLimit(input.readLongScalar()) + .to(input.readBytes(v -> v.isEmpty() ? null : Address.wrap(v))) + .value(Wei.of(input.readUInt256Scalar())) + .payload(input.readBytes()) + .accessList( + input.readList( + accessListEntryRLPInput -> { + accessListEntryRLPInput.enterList(); + final AccessListEntry accessListEntry = + new AccessListEntry( + Address.wrap(accessListEntryRLPInput.readBytes()), + accessListEntryRLPInput.readList(RLPInput::readBytes32)); + accessListEntryRLPInput.leaveList(); + return accessListEntry; + })) + .setCodeTransactionPayloads( + input.readList( + setCodeTransactionPayloadsRLPInput -> { + setCodeTransactionPayloadsRLPInput.enterList(); + final SetCodeAuthorization setCodeAuthorization = + decodeInnerPayload(setCodeTransactionPayloadsRLPInput); + setCodeTransactionPayloadsRLPInput.leaveList(); + return setCodeAuthorization; + })); + + final byte recId = (byte) input.readUnsignedByteScalar(); + final Transaction transaction = + builder + .signature( + SIGNATURE_ALGORITHM + .get() + .createSignature( + input.readUInt256Scalar().toUnsignedBigInteger(), + input.readUInt256Scalar().toUnsignedBigInteger(), + recId)) + .build(); + input.leaveList(); + return transaction; + } + + private static SetCodeAuthorization decodeInnerPayload(final RLPInput input) { + final BigInteger chainId = input.readBigIntegerScalar(); + final Address address = Address.wrap(input.readBytes()); + + final List nonces = new ArrayList<>(); + if (!input.nextIsList()) { + throw new IllegalArgumentException("Optional nonce must be an empty list, but isn't"); + } + + input.enterList(); + while (input.nextSize() != 0) { + nonces.add(input.readLongScalar()); + } + input.leaveList(); + + final SECPSignature signature = + SIGNATURE_ALGORITHM + .get() + .createSignature( + input.readUInt256Scalar().toUnsignedBigInteger(), + input.readUInt256Scalar().toUnsignedBigInteger(), + (byte) input.readUnsignedByteScalar()); + + return new SetCodeAuthorization(chainId, address, nonces, signature); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoder.java new file mode 100644 index 00000000000..23a5eea8878 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoder.java @@ -0,0 +1,77 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.encoding; + +import static org.hyperledger.besu.ethereum.core.encoding.AccessListTransactionEncoder.writeAccessList; +import static org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder.writeSignatureAndRecoveryId; + +import org.hyperledger.besu.datatypes.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.RLPOutput; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes; + +public class SetCodeTransactionEncoder { + + private SetCodeTransactionEncoder() { + // private constructor + } + + public static void encodeSetCodeInner( + final List payloads, final RLPOutput rlpOutput) { + rlpOutput.startList(); + payloads.forEach(payload -> encodeSingleSetCode(payload, rlpOutput)); + rlpOutput.endList(); + } + + public static void encodeSingleSetCode( + final SetCodeAuthorization payload, final RLPOutput rlpOutput) { + rlpOutput.startList(); + rlpOutput.writeBigIntegerScalar(payload.chainId()); + rlpOutput.writeBytes(payload.address().copy()); + rlpOutput.startList(); + payload.nonces().forEach(rlpOutput::writeLongScalar); + rlpOutput.endList(); + rlpOutput.writeIntScalar(payload.signature().getRecId()); + rlpOutput.writeBigIntegerScalar(payload.signature().getR()); + rlpOutput.writeBigIntegerScalar(payload.signature().getS()); + rlpOutput.endList(); + } + + public static void encode(final Transaction transaction, final RLPOutput out) { + out.startList(); + out.writeBigIntegerScalar(transaction.getChainId().orElseThrow()); + out.writeLongScalar(transaction.getNonce()); + out.writeUInt256Scalar(transaction.getMaxPriorityFeePerGas().orElseThrow()); + out.writeUInt256Scalar(transaction.getMaxFeePerGas().orElseThrow()); + out.writeLongScalar(transaction.getGasLimit()); + out.writeBytes(transaction.getTo().map(Bytes::copy).orElse(Bytes.EMPTY)); + out.writeUInt256Scalar(transaction.getValue()); + out.writeBytes(transaction.getPayload()); + writeAccessList(out, transaction.getAccessList()); + encodeSetCodeInner( + transaction + .getSetCodeTransactionPayloads() + .orElseThrow( + () -> + new IllegalStateException( + "Developer error: the transaction should be guaranteed to have a set code payload here")), + out); + writeSignatureAndRecoveryId(transaction, out); + out.endList(); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java index fe6687cdb5c..2a63d8f24ba 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java @@ -40,7 +40,9 @@ interface Decoder { TransactionType.EIP1559, EIP1559TransactionDecoder::decode, TransactionType.BLOB, - BlobTransactionDecoder::decode); + BlobTransactionDecoder::decode, + TransactionType.SET_CODE, + SetCodeTransactionDecoder::decode); private static final ImmutableMap POOLED_TRANSACTION_DECODERS = ImmutableMap.of(TransactionType.BLOB, BlobPooledTransactionDecoder::decode); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java index 20fe75885e4..49959756219 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java @@ -39,7 +39,9 @@ interface Encoder { TransactionType.EIP1559, EIP1559TransactionEncoder::encode, TransactionType.BLOB, - BlobTransactionEncoder::encode); + BlobTransactionEncoder::encode, + TransactionType.SET_CODE, + SetCodeTransactionEncoder::encode); private static final ImmutableMap POOLED_TRANSACTION_ENCODERS = ImmutableMap.of(TransactionType.BLOB, BlobPooledTransactionEncoder::encode); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ClassicProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ClassicProtocolSpecs.java index 7575c7fe314..61da6145a9f 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ClassicProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ClassicProtocolSpecs.java @@ -185,7 +185,8 @@ public static ProtocolSpecBuilder atlantisDefinition( false, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.frontier())) + CoinbaseFeePriceCalculator.frontier(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) .name("Atlantis"); } @@ -374,7 +375,8 @@ public static ProtocolSpecBuilder spiralDefinition( true, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.frontier())) + CoinbaseFeePriceCalculator.frontier(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) .name("Spiral"); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index 16fee28bb50..3aa4384e26a 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -146,7 +146,8 @@ public static ProtocolSpecBuilder frontierDefinition( false, stackSizeLimit, FeeMarket.legacy(), - CoinbaseFeePriceCalculator.frontier())) + CoinbaseFeePriceCalculator.frontier(), + new SetCodeTransactionProcessor(BigInteger.ZERO))) .privateTransactionProcessorBuilder( (transactionValidatorFactory, contractCreationProcessor, @@ -300,7 +301,8 @@ public static ProtocolSpecBuilder spuriousDragonDefinition( false, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.frontier())) + CoinbaseFeePriceCalculator.frontier(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) .name("SpuriousDragon"); } @@ -493,7 +495,8 @@ static ProtocolSpecBuilder londonDefinition( false, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.eip1559())) + CoinbaseFeePriceCalculator.eip1559(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) .contractCreationProcessorBuilder( (gasCalculator, evm) -> new ContractCreationProcessor( @@ -630,7 +633,8 @@ static ProtocolSpecBuilder shanghaiDefinition( true, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.eip1559())) + CoinbaseFeePriceCalculator.eip1559(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) // Contract creation rules for EIP-3860 Limit and meter intitcode .transactionValidatorFactoryBuilder( (gasCalculator, gasLimitCalculator, feeMarket) -> @@ -706,7 +710,8 @@ static ProtocolSpecBuilder cancunDefinition( true, stackSizeLimit, feeMarket, - CoinbaseFeePriceCalculator.eip1559())) + CoinbaseFeePriceCalculator.eip1559(), + new SetCodeTransactionProcessor(chainId.orElse(BigInteger.ZERO)))) // change to check for max blob gas per block for EIP-4844 .transactionValidatorFactoryBuilder( (gasCalculator, gasLimitCalculator, feeMarket) -> @@ -764,6 +769,23 @@ static ProtocolSpecBuilder pragueDefinition( // EIP-7002 Withdrawls / EIP-6610 Deposits / EIP-7685 Requests .requestProcessorCoordinator(pragueRequestsProcessors(depositContractAddress)) + // change to accept EIP-7702 transactions + .transactionValidatorFactoryBuilder( + (gasCalculator, gasLimitCalculator, feeMarket) -> + new TransactionValidatorFactory( + gasCalculator, + gasLimitCalculator, + feeMarket, + true, + chainId, + Set.of( + TransactionType.FRONTIER, + TransactionType.ACCESS_LIST, + TransactionType.EIP1559, + TransactionType.BLOB, + TransactionType.SET_CODE), + SHANGHAI_INIT_CODE_SIZE_LIMIT)) + // EIP-2935 Blockhash processor .blockHashProcessor(new PragueBlockHashProcessor()) .name("Prague"); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java index d982265f242..c12ac1f0930 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java @@ -80,6 +80,8 @@ public class MainnetTransactionProcessor { protected final FeeMarket feeMarket; private final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator; + private final SetCodeTransactionProcessor setCodeTransactionProcessor; + public MainnetTransactionProcessor( final GasCalculator gasCalculator, final TransactionValidatorFactory transactionValidatorFactory, @@ -89,7 +91,8 @@ public MainnetTransactionProcessor( final boolean warmCoinbase, final int maxStackSize, final FeeMarket feeMarket, - final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator) { + final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator, + final SetCodeTransactionProcessor setCodeTransactionProcessor) { this.gasCalculator = gasCalculator; this.transactionValidatorFactory = transactionValidatorFactory; this.contractCreationProcessor = contractCreationProcessor; @@ -99,6 +102,7 @@ public MainnetTransactionProcessor( this.maxStackSize = maxStackSize; this.feeMarket = feeMarket; this.coinbaseFeePriceCalculator = coinbaseFeePriceCalculator; + this.setCodeTransactionProcessor = setCodeTransactionProcessor; } /** @@ -374,6 +378,9 @@ public TransactionProcessingResult processTransaction( if (transaction.getVersionedHashes().isPresent()) { commonMessageFrameBuilder.versionedHashes( Optional.of(transaction.getVersionedHashes().get().stream().toList())); + } else if (transaction.getSetCodeTransactionPayloads().isPresent()) { + addressList.addAll( + setCodeTransactionProcessor.addContractToAuthority(worldUpdater, transaction)); } else { commonMessageFrameBuilder.versionedHashes(Optional.empty()); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/SetCodeTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/SetCodeTransactionProcessor.java new file mode 100644 index 00000000000..38ec065f456 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/SetCodeTransactionProcessor.java @@ -0,0 +1,97 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.mainnet; + +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.encoding.SetCodeTransactionEncoder; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +public class SetCodeTransactionProcessor { + private static final Bytes MAGIC = Bytes.of(0x05); + + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + private final BigInteger chainId; + + public SetCodeTransactionProcessor(final BigInteger chainId) { + this.chainId = chainId; + } + + public Set
addContractToAuthority( + final WorldUpdater worldUpdater, final Transaction transaction) { + final Set
authorityList = new HashSet<>(); + + transaction + .getSetCodeTransactionPayloads() + .get() + .forEach( + payload -> { + recoverAuthority(payload) + .ifPresent( + authorityAddress -> { + if (!chainId.equals(BigInteger.ZERO) + && !payload.chainId().equals(BigInteger.ZERO) + && !chainId.equals(payload.chainId())) { + return; + } + + final MutableAccount account = worldUpdater.getAccount(authorityAddress); + + if (payload.nonces().size() == 1 + && !payload.nonces().getFirst().equals(account.getNonce())) { + return; + } + + if (!account.getCode().isEmpty()) { + return; + } + + account.setCode(worldUpdater.getAccount(payload.address()).getCode()); + authorityList.add(authorityAddress); + }); + }); + + return authorityList; + } + + private Optional
recoverAuthority(final SetCodeAuthorization authorization) { + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + SetCodeTransactionEncoder.encodeSingleSetCode(authorization, rlpOutput); + + final Hash hash = Hash.hash(MAGIC.or(rlpOutput.encoded())); + + return SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, authorization.signature()) + .map(Address::extract); + } +} diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java index c5b797403a8..839416e0c5e 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java @@ -376,6 +376,8 @@ public Transaction transaction( case EIP1559 -> eip1559Transaction(payload, to); case ACCESS_LIST -> accessListTransaction(payload, to); case BLOB -> blobTransaction(payload, to); + // TODO 7702 + case SET_CODE -> null; // no default, all types accounted for. }; } diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java index d016b7f4e5e..716acf7461a 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java @@ -92,6 +92,9 @@ public Transaction createTransaction(final KeyPair keys) { builder.versionedHashes(versionedHashes.get()); } break; + case SET_CODE: + // TODO 7702 + break; } to.ifPresent(builder::to); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java index 79e8c368e06..14f324b1566 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java @@ -131,6 +131,7 @@ private int computeMemorySize() { case ACCESS_LIST -> computeAccessListMemorySize(); case EIP1559 -> computeEIP1559MemorySize(); case BLOB -> computeBlobMemorySize(); + case SET_CODE -> computeSetCodeMemorySize(); } + PENDING_TRANSACTION_MEMORY_SIZE; } @@ -164,6 +165,11 @@ private int computeBlobMemorySize() { + computeBlobWithCommitmentsMemorySize(); } + private int computeSetCodeMemorySize() { + // TODO: Implement this for 7702 + return 0; + } + private int computeBlobWithCommitmentsMemorySize() { final int blobCount = transaction.getBlobCount(); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java index e2789bba336..2b888c11bd0 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java @@ -28,6 +28,8 @@ */ public class PragueGasCalculator extends CancunGasCalculator { + static final long PER_CONTRACT_CODE_BASE_COST = 2500L; + /** Instantiates a new Prague Gas Calculator. */ public PragueGasCalculator() { this(BLS12_MAP_FP2_TO_G2.toArrayUnsafe()[19]); @@ -41,4 +43,8 @@ public PragueGasCalculator() { protected PragueGasCalculator(final int maxPrecompile) { super(maxPrecompile); } + + public long getSetCodeListGasCost(final int authorizationListLength) { + return PER_CONTRACT_CODE_BASE_COST * authorizationListLength; + } }