diff --git a/CHANGELOG.md b/CHANGELOG.md index e0269472a5f..a6b28d0a5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add `block-test` subcommand to the evmtool which runs blockchain reference tests [#7310](https://github.com/hyperledger/besu/pull/7310) - Implement gnark-crypto for eip-2537 [#7316](https://github.com/hyperledger/besu/pull/7316) - Improve blob size transaction selector [#7312](https://github.com/hyperledger/besu/pull/7312) +- Added EIP-7702 [#7237](https://github.com/hyperledger/besu/pull/7237) ### Bug fixes - Fix `eth_call` deserialization to correctly ignore unknown fields in the transaction object. [#7323](https://github.com/hyperledger/besu/pull/7323) diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/PragueAcceptanceTestHelper.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/PragueAcceptanceTestHelper.java new file mode 100644 index 00000000000..fa190f3de17 --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/PragueAcceptanceTestHelper.java @@ -0,0 +1,193 @@ +/* + * 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.tests.acceptance.ethereum; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; + +import java.io.IOException; +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.web3j.protocol.core.methods.response.EthBlock; + +public class PragueAcceptanceTestHelper { + protected static final MediaType MEDIA_TYPE_JSON = + MediaType.parse("application/json; charset=utf-8"); + + private final OkHttpClient httpClient; + private final ObjectMapper mapper; + private final BesuNode besuNode; + private final EthTransactions ethTransactions; + + private long blockTimeStamp = 0; + + PragueAcceptanceTestHelper(final BesuNode besuNode, final EthTransactions ethTransactions) { + this.besuNode = besuNode; + this.ethTransactions = ethTransactions; + httpClient = new OkHttpClient(); + mapper = new ObjectMapper(); + } + + public void buildNewBlock() throws IOException { + final EthBlock.Block block = besuNode.execute(ethTransactions.block()); + + blockTimeStamp += 1; + final Call buildBlockRequest = + createEngineCall(createForkChoiceRequest(block.getHash(), blockTimeStamp)); + + final String payloadId; + try (final Response buildBlockResponse = buildBlockRequest.execute()) { + payloadId = + mapper + .readTree(buildBlockResponse.body().string()) + .get("result") + .get("payloadId") + .asText(); + + assertThat(payloadId).isNotEmpty(); + } + + waitFor(500); + + final Call getPayloadRequest = createEngineCall(createGetPayloadRequest(payloadId)); + + final ObjectNode executionPayload; + final String newBlockHash; + final String parentBeaconBlockRoot; + try (final Response getPayloadResponse = getPayloadRequest.execute()) { + assertThat(getPayloadResponse.code()).isEqualTo(200); + + executionPayload = + (ObjectNode) + mapper + .readTree(getPayloadResponse.body().string()) + .get("result") + .get("executionPayload"); + + newBlockHash = executionPayload.get("blockHash").asText(); + parentBeaconBlockRoot = executionPayload.remove("parentBeaconBlockRoot").asText(); + + assertThat(newBlockHash).isNotEmpty(); + } + + final Call newPayloadRequest = + createEngineCall( + createNewPayloadRequest(executionPayload.toString(), parentBeaconBlockRoot)); + try (final Response newPayloadResponse = newPayloadRequest.execute()) { + assertThat(newPayloadResponse.code()).isEqualTo(200); + } + + final Call moveChainAheadRequest = createEngineCall(createForkChoiceRequest(newBlockHash)); + + try (final Response moveChainAheadResponse = moveChainAheadRequest.execute()) { + assertThat(moveChainAheadResponse.code()).isEqualTo(200); + } + } + + private Call createEngineCall(final String request) { + return httpClient.newCall( + new Request.Builder() + .url(besuNode.engineRpcUrl().get()) + .post(RequestBody.create(request, MEDIA_TYPE_JSON)) + .build()); + } + + private String createForkChoiceRequest(final String blockHash) { + return createForkChoiceRequest(blockHash, null); + } + + private String createForkChoiceRequest(final String parentBlockHash, final Long timeStamp) { + final Optional maybeTimeStamp = Optional.ofNullable(timeStamp); + + String forkChoiceRequest = + "{" + + " \"jsonrpc\": \"2.0\"," + + " \"method\": \"engine_forkchoiceUpdatedV3\"," + + " \"params\": [" + + " {" + + " \"headBlockHash\": \"" + + parentBlockHash + + "\"," + + " \"safeBlockHash\": \"" + + parentBlockHash + + "\"," + + " \"finalizedBlockHash\": \"" + + parentBlockHash + + "\"" + + " }"; + + if (maybeTimeStamp.isPresent()) { + forkChoiceRequest += + " ,{" + + " \"timestamp\": \"" + + Long.toHexString(maybeTimeStamp.get()) + + "\"," + + " \"prevRandao\": \"0x0000000000000000000000000000000000000000000000000000000000000000\"," + + " \"suggestedFeeRecipient\": \"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b\"," + + " \"withdrawals\": []," + + " \"parentBeaconBlockRoot\": \"0x0000000000000000000000000000000000000000000000000000000000000000\"" + + " }"; + } + + forkChoiceRequest += " ]," + " \"id\": 67" + "}"; + + return forkChoiceRequest; + } + + private String createGetPayloadRequest(final String payloadId) { + return "{" + + " \"jsonrpc\": \"2.0\"," + + " \"method\": \"engine_getPayloadV4\"," + + " \"params\": [\"" + + payloadId + + "\"]," + + " \"id\": 67" + + "}"; + } + + private String createNewPayloadRequest( + final String executionPayload, final String parentBeaconBlockRoot) { + return "{" + + " \"jsonrpc\": \"2.0\"," + + " \"method\": \"engine_newPayloadV4\"," + + " \"params\": [" + + executionPayload + + ",[]," + + "\"" + + parentBeaconBlockRoot + + "\"" + + "]," + + " \"id\": 67" + + "}"; + } + + private static void waitFor(final long durationMilliSeconds) { + try { + Thread.sleep(durationMilliSeconds); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/SetCodeTransactionAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/SetCodeTransactionAcceptanceTest.java new file mode 100644 index 00000000000..f1b2558a977 --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/ethereum/SetCodeTransactionAcceptanceTest.java @@ -0,0 +1,125 @@ +/* + * 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.tests.acceptance.ethereum; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.crypto.SECP256K1; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Amount; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.methods.response.TransactionReceipt; + +public class SetCodeTransactionAcceptanceTest extends AcceptanceTestBase { + private static final String GENESIS_FILE = "/dev/dev_prague.json"; + private static final SECP256K1 secp256k1 = new SECP256K1(); + + public static final Address SEND_ALL_ETH_CONTRACT_ADDRESS = + Address.fromHexStringStrict("0000000000000000000000000000000000009999"); + + private final Account authorizer = + accounts.createAccount( + Address.fromHexStringStrict("8da48afC965480220a3dB9244771bd3afcB5d895")); + public static final Bytes AUTHORIZER_PRIVATE_KEY = + Bytes.fromHexString("11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1"); + + private final Account transactionSponsor = + accounts.createAccount( + Address.fromHexStringStrict("a05b21E5186Ce93d2a226722b85D6e550Ac7D6E3")); + public static final Bytes TRANSACTION_SPONSOR_PRIVATE_KEY = + Bytes.fromHexString("3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453"); + + private BesuNode besuNode; + private PragueAcceptanceTestHelper testHelper; + + @BeforeEach + void setUp() throws IOException { + besuNode = besu.createExecutionEngineGenesisNode("besuNode", GENESIS_FILE); + cluster.start(besuNode); + + testHelper = new PragueAcceptanceTestHelper(besuNode, ethTransactions); + } + + /** + * At the beginning of the test both the authorizer and the transaction sponsor have a balance of + * 90000 ETH. The authorizer creates an authorization for a contract that send all its ETH to any + * given address. The transaction sponsor created a 7702 transaction with it and sends all the ETH + * from the authorizer to itself. The authorizer balance should be 0 and the transaction sponsor + * balance should be 180000 ETH minus the transaction costs. + */ + @Test + public void shouldTransferAllEthOfAuthorizerToSponsor() throws IOException { + + // 7702 transaction + final org.hyperledger.besu.datatypes.SetCodeAuthorization authorization = + SetCodeAuthorization.builder() + .chainId(BigInteger.valueOf(20211)) + .address(SEND_ALL_ETH_CONTRACT_ADDRESS) + .signAndBuild( + secp256k1.createKeyPair( + secp256k1.createPrivateKey(AUTHORIZER_PRIVATE_KEY.toUnsignedBigInteger()))); + + final Transaction tx = + Transaction.builder() + .type(TransactionType.SET_CODE) + .chainId(BigInteger.valueOf(20211)) + .nonce(0) + .maxPriorityFeePerGas(Wei.of(1000000000)) + .maxFeePerGas(Wei.fromHexString("0x02540BE400")) + .gasLimit(1000000) + .to(Address.fromHexStringStrict(authorizer.getAddress())) + .value(Wei.ZERO) + .payload(Bytes32.leftPad(Bytes.fromHexString(transactionSponsor.getAddress()))) + .accessList(List.of()) + .setCodeTransactionPayloads(List.of(authorization)) + .signAndBuild( + secp256k1.createKeyPair( + secp256k1.createPrivateKey( + TRANSACTION_SPONSOR_PRIVATE_KEY.toUnsignedBigInteger()))); + + final String txHash = + besuNode.execute(ethTransactions.sendRawTransaction(tx.encoded().toHexString())); + testHelper.buildNewBlock(); + + Optional maybeTransactionReceipt = + besuNode.execute(ethTransactions.getTransactionReceipt(txHash)); + assertThat(maybeTransactionReceipt).isPresent(); + + cluster.verify(authorizer.balanceEquals(0)); + + final String gasPriceWithout0x = + maybeTransactionReceipt.get().getEffectiveGasPrice().substring(2); + final BigInteger txCost = + maybeTransactionReceipt.get().getGasUsed().multiply(new BigInteger(gasPriceWithout0x, 16)); + BigInteger expectedSponsorBalance = new BigInteger("180000000000000000000000").subtract(txCost); + cluster.verify(transactionSponsor.balanceEquals(Amount.wei(expectedSponsorBalance))); + } +} diff --git a/acceptance-tests/tests/src/test/resources/dev/dev_prague.json b/acceptance-tests/tests/src/test/resources/dev/dev_prague.json new file mode 100644 index 00000000000..26e59992d16 --- /dev/null +++ b/acceptance-tests/tests/src/test/resources/dev/dev_prague.json @@ -0,0 +1,112 @@ +{ + "config": { + "chainId":20211, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":0, + "eip158Block":0, + "byzantiumBlock":0, + "constantinopleBlock":0, + "petersburgBlock":0, + "istanbulBlock":0, + "muirGlacierBlock":0, + "berlinBlock":0, + "londonBlock":0, + "terminalTotalDifficulty":0, + "cancunTime":0, + "pragueTime":0, + "clique": { + "period": 5, + "epoch": 30000 + }, + "depositContractAddress": "0x4242424242424242424242424242424242424242" + }, + "nonce":"0x42", + "timestamp":"0x0", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "difficulty":"0x400000000", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "alloc":{ + "a05b21E5186Ce93d2a226722b85D6e550Ac7D6E3": { + "privateKey": "3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453", + "balance": "90000000000000000000000" + }, + "8da48afC965480220a3dB9244771bd3afcB5d895": { + "comment": "This account has signed a authorization for contract 0x0000000000000000000000000000000000009999 to send a 7702 transaction", + "privateKey": "11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1", + "balance": "90000000000000000000000" + }, + "0x0000000000000000000000000000000000009999": { + "comment": "Contract sends all its Ether to the address provided as a call data.", + "balance": "0", + "code": "5F5F5F5F475F355AF100", + "codeDecompiled": "PUSH0 PUSH0 PUSH0 PUSH0 SELFBALANCE PUSH0 CALLDATALOAD GAS CALL STOP", + "storage": {} + }, + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { + "comment": "This is the account used to sign the transaction that creates a validator exit", + "balance": "1000000000000000000000000000" + }, + "0x00A3ca265EBcb825B45F985A16CEFB49958cE017": { + "comment": "This is the runtime bytecode for the Withdrawal Request Smart Contract. It was created from the deployment transaction in EIP-7002 (https://eips.ethereum.org/EIPS/eip-7002#deployment)", + "balance": "0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b36603814156101215760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012157600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f5460015460028282011161010f5750505f610115565b01600290035b5f555f600155604c025ff35b5f5ffd", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000004": "000000000000000000000000a4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb", + "0x0000000000000000000000000000000000000000000000000000000000000005": "b10a4a15bf67b328c9b101d09e5c6ee6672978fdad9ef0d9e2ceffaee9922355", + "0x0000000000000000000000000000000000000000000000000000000000000006": "5d8601f0cb3bcc4ce1af9864779a416e00000000000000000000000000000000" + } + }, + "0x4242424242424242424242424242424242424242": { + "comment": "The deposit contract", + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a26469706673582212201dd26f37a621703009abf16e77e69c93dc50c79db7f6cc37543e3e0e3decdc9764736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + } + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} \ No newline at end of file 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..4ee34295f45 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, SET_CODE] (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..b12e65de412 --- /dev/null +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/SetCodeAuthorization.java @@ -0,0 +1,82 @@ +/* + * 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 java.math.BigInteger; +import java.util.Optional; + +/** + * SetCodeAuthorization is a data structure that represents the authorization to set code on a EOA + * account. + */ +public interface SetCodeAuthorization { + /** + * Return the chain id. + * + * @return chain id + */ + BigInteger chainId(); + + /** + * Return the address of the account which code will be used. + * + * @return address + */ + Address address(); + + /** + * Return the signature. + * + * @return signature + */ + SECPSignature signature(); + + /** + * Return the authorizer address. + * + * @return authorizer address of the EOA which will load the code into its account + */ + Optional
authorizer(); + + /** + * Return a valid nonce or empty otherwise. A nonce is valid if the size of the list is exactly 1 + * + * @return all the optional nonce + */ + Optional nonce(); + + /** + * Return the recovery id. + * + * @return byte + */ + byte v(); + + /** + * Return the r value of the signature. + * + * @return r value + */ + BigInteger r(); + + /** + * Return the s value of the signature. + * + * @return s value + */ + BigInteger s(); +} 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..d6751852bce 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,18 @@ 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. + * + * @return the set code transaction payloads + */ + Optional> getAuthorizationList(); + + /** + * Returns the size of the authorization list. + * + * @return the size of the authorization list + */ + int authorizationListSize(); } 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..df4a07193b8 100644 --- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java @@ -27,10 +27,12 @@ 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); + Set.of(ACCESS_LIST, EIP1559, BLOB, SET_CODE); private static final EnumSet LEGACY_FEE_MARKET_TRANSACTION_TYPES = EnumSet.of(TransactionType.FRONTIER, TransactionType.ACCESS_LIST); @@ -83,7 +85,8 @@ public static TransactionType of(final int serializedTypeValue) { TransactionType.FRONTIER, TransactionType.ACCESS_LIST, TransactionType.EIP1559, - TransactionType.BLOB + TransactionType.BLOB, + TransactionType.SET_CODE }) .filter(transactionType -> transactionType.typeValue == serializedTypeValue) .findFirst() @@ -128,4 +131,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/methods/engine/EngineForkchoiceUpdatedV3.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineForkchoiceUpdatedV3.java index 60ba8990a13..b5aebf4f5d7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineForkchoiceUpdatedV3.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineForkchoiceUpdatedV3.java @@ -33,7 +33,7 @@ public class EngineForkchoiceUpdatedV3 extends AbstractEngineForkchoiceUpdated { - private final Optional cancun; + private final Optional supportedHardFork; private static final Logger LOG = LoggerFactory.getLogger(EngineForkchoiceUpdatedV3.class); public EngineForkchoiceUpdatedV3( @@ -43,7 +43,11 @@ public EngineForkchoiceUpdatedV3( final MergeMiningCoordinator mergeCoordinator, final EngineCallListener engineCallListener) { super(vertx, protocolSchedule, protocolContext, mergeCoordinator, engineCallListener); - this.cancun = protocolSchedule.hardforkFor(s -> s.fork().name().equalsIgnoreCase("Cancun")); + this.supportedHardFork = + protocolSchedule.hardforkFor( + s -> + s.fork().name().equalsIgnoreCase("Cancun") + || s.fork().name().equalsIgnoreCase("Prague")); } @Override @@ -77,12 +81,12 @@ protected ValidationResult validateParameter( @Override protected ValidationResult validateForkSupported(final long blockTimestamp) { if (protocolSchedule.isPresent()) { - if (cancun.isPresent() && blockTimestamp >= cancun.get().milestone()) { + if (supportedHardFork.isPresent() && blockTimestamp >= supportedHardFork.get().milestone()) { return ValidationResult.valid(); } else { return ValidationResult.invalid( RpcErrorType.UNSUPPORTED_FORK, - "Cancun configured to start at timestamp: " + cancun.get().milestone()); + "Cancun configured to start at timestamp: " + supportedHardFork.get().milestone()); } } else { return ValidationResult.invalid( @@ -99,7 +103,7 @@ protected Optional isPayloadAttributesValid( return Optional.of(new JsonRpcErrorResponse(requestId, getInvalidPayloadAttributesError())); } else if (payloadAttributes.getTimestamp().longValue() == 0) { return Optional.of(new JsonRpcErrorResponse(requestId, getInvalidPayloadAttributesError())); - } else if (payloadAttributes.getTimestamp() < cancun.get().milestone()) { + } else if (payloadAttributes.getTimestamp() < supportedHardFork.get().milestone()) { return Optional.of(new JsonRpcErrorResponse(requestId, RpcErrorType.UNSUPPORTED_FORK)); } else { return Optional.empty(); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EnginePayloadStatusResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EnginePayloadStatusResult.java index 4912f81e498..a9b9a17018a 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EnginePayloadStatusResult.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EnginePayloadStatusResult.java @@ -19,7 +19,9 @@ import java.util.Optional; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @JsonPropertyOrder({"status", "latestValidHash", "validationError"}) @@ -28,10 +30,11 @@ public class EnginePayloadStatusResult { Optional latestValidHash; Optional validationError; + @JsonCreator public EnginePayloadStatusResult( - final EngineStatus status, - final Hash latestValidHash, - final Optional validationError) { + @JsonProperty("status") final EngineStatus status, + @JsonProperty("latestValidHash") final Hash latestValidHash, + @JsonProperty("errorMessage") final Optional validationError) { this.status = status; this.latestValidHash = Optional.ofNullable(latestValidHash); this.validationError = validationError; 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..05b323cacb3 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; @@ -30,6 +31,7 @@ @JsonPropertyOrder({ "accessList", + "authorizationList", "blockHash", "blockNumber", "chainId", @@ -91,6 +93,9 @@ public class TransactionCompleteResult implements TransactionResult { @JsonInclude(JsonInclude.Include.NON_NULL) private final List versionedHashes; + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List authorizationList; + public TransactionCompleteResult(final TransactionWithMetadata tx) { final Transaction transaction = tx.getTransaction(); final TransactionType transactionType = transaction.getType(); @@ -125,7 +130,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 +139,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.authorizationList = transaction.getAuthorizationList().orElse(null); } @JsonGetter(value = "accessList") @@ -246,4 +253,9 @@ public String getS() { public List getVersionedHashes() { return versionedHashes; } + + @JsonGetter(value = "authorizationList") + public List getAuthorizationList() { + return authorizationList; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/SetCodeAuthorization.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/SetCodeAuthorization.java new file mode 100644 index 00000000000..ea9cae2fcb2 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/SetCodeAuthorization.java @@ -0,0 +1,260 @@ +/* + * 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; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPSignature; +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.ethereum.core.encoding.SetCodeTransactionEncoder; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +public class SetCodeAuthorization implements org.hyperledger.besu.datatypes.SetCodeAuthorization { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + public static final Bytes MAGIC = Bytes.fromHexString("05"); + + private final BigInteger chainId; + private final Address address; + private final Optional nonce; + private final SECPSignature signature; + private Optional
authorizer = Optional.empty(); + private boolean isAuthorityComputed = false; + + /** + * 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 nonce an optional nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public SetCodeAuthorization( + final BigInteger chainId, + final Address address, + final Optional nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; + } + + /** + * 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 org.hyperledger.besu.datatypes.SetCodeAuthorization createSetCodeAuthorizationEntry( + @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, + Optional.ofNullable(nonces.get(0)), + SIGNATURE_ALGORITHM.get().createSignature(r, s, v)); + } + + @JsonProperty("chainId") + @Override + public BigInteger chainId() { + return chainId; + } + + @JsonProperty("address") + @Override + public Address address() { + return address; + } + + @JsonProperty("signature") + @Override + public SECPSignature signature() { + return signature; + } + + @Override + public Optional
authorizer() { + if (!isAuthorityComputed) { + authorizer = computeAuthority(); + isAuthorityComputed = true; + } + + return authorizer; + } + + @Override + public Optional nonce() { + return nonce; + } + + @JsonProperty("v") + @Override + public byte v() { + return signature.getRecId(); + } + + @JsonProperty("r") + @Override + public BigInteger r() { + return signature.getR(); + } + + @JsonProperty("s") + @Override + public BigInteger s() { + return signature.getS(); + } + + private Optional
computeAuthority() { + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + SetCodeTransactionEncoder.encodeSingleSetCodeWithoutSignature(this, rlpOutput); + + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); + + return SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } + + /** + * Create set code authorization with a builder. + * + * @return SetCodeAuthorization.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for SetCodeAuthorization. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Optional nonce = Optional.empty(); + private SECPSignature signature; + + /** Create a new builder. */ + protected Builder() {} + + /** + * Set the optional chain id. + * + * @param chainId the chain id + * @return this builder + */ + public Builder chainId(final BigInteger chainId) { + this.chainId = chainId; + return this; + } + + /** + * Set the address of the authorized smart contract. + * + * @param address the address + * @return this builder + */ + public Builder address(final Address address) { + this.address = address; + return this; + } + + /** + * Set the optional nonce. + * + * @param nonce the optional nonce. + * @return this builder + */ + public Builder nonces(final Optional nonce) { + this.nonce = nonce; + return this; + } + + /** + * Set the signature of the authorizer account. + * + * @param signature the signature + * @return this builder + */ + public Builder signature(final SECPSignature signature) { + this.signature = signature; + return this; + } + + /** + * Sign the authorization with the given key pair and return the authorization. + * + * @param keyPair the key pair + * @return SetCodeAuthorization + */ + public org.hyperledger.besu.datatypes.SetCodeAuthorization signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.startList(); + nonce.ifPresent(output::writeLongScalar); + output.endList(); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return SetCodeAuthorization + */ + public org.hyperledger.besu.datatypes.SetCodeAuthorization build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new SetCodeAuthorization(chainId, address, nonce, signature); + } + } +} 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..513cfb8319d 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> maybeAuthorizationList; 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> maybeAuthorizationList) { 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( + maybeAuthorizationList.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.maybeAuthorizationList = maybeAuthorizationList; } /** @@ -462,6 +473,7 @@ private Bytes32 getOrComputeSenderRecoveryHash() { payload, maybeAccessList, versionedHashes.orElse(null), + maybeAuthorizationList, chainId); } return hashNoSignature; @@ -668,6 +680,16 @@ public Optional getBlobsWithCommitments() { return blobsWithCommitments; } + @Override + public Optional> getAuthorizationList() { + return maybeAuthorizationList; + } + + @Override + public int authorizationListSize() { + return maybeAuthorizationList.map(List::size).orElse(0); + } + /** * Return the list of transaction hashes extracted from the collection of Transaction passed as * argument @@ -692,6 +714,7 @@ private static Bytes32 computeSenderRecoveryHash( final Bytes payload, final Optional> accessList, final List versionedHashes, + final Optional> authorizationList, final Optional chainId) { if (transactionType.requiresChainId()) { checkArgument(chainId.isPresent(), "Transaction type %s requires chainId", transactionType); @@ -736,6 +759,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, + authorizationList.orElseThrow( + () -> + new IllegalStateException( + "Developer error: the transaction should be guaranteed to have a set code payload here"))); }; return keccak256(preimage); } @@ -873,6 +911,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 authorizationList) { + final Bytes encoded = + RLP.encode( + rlpOutput -> { + rlpOutput.startList(); + eip1559PreimageFields( + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + payload, + chainId, + accessList, + rlpOutput); + SetCodeTransactionEncoder.encodeSetCodeInner(authorizationList, rlpOutput); + rlpOutput.endList(); + }); + return Bytes.concatenate(Bytes.of(TransactionType.SET_CODE.getSerializedType()), encoded); + } + @Override public boolean equals(final Object other) { if (!(other instanceof Transaction that)) { @@ -1040,7 +1110,8 @@ public Transaction detachedCopy() { sender, chainId, detachedVersionedHashes, - detachedBlobsWithCommitments); + detachedBlobsWithCommitments, + maybeAuthorizationList); // copy also the computed fields, to avoid to recompute them copiedTx.sender = this.sender; @@ -1108,6 +1179,7 @@ public static class Builder { protected Optional v = Optional.empty(); protected List versionedHashes = null; private BlobsWithCommitments blobsWithCommitments; + protected Optional> setCodeTransactionPayloads = Optional.empty(); public Builder copiedFrom(final Transaction toCopy) { this.transactionType = toCopy.transactionType; @@ -1126,6 +1198,7 @@ public Builder copiedFrom(final Transaction toCopy) { this.chainId = toCopy.chainId; this.versionedHashes = toCopy.versionedHashes.orElse(null); this.blobsWithCommitments = toCopy.blobsWithCommitments.orElse(null); + this.setCodeTransactionPayloads = toCopy.maybeAuthorizationList; return this; } @@ -1219,6 +1292,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 +1323,8 @@ public Transaction build() { sender, chainId, Optional.ofNullable(versionedHashes), - Optional.ofNullable(blobsWithCommitments)); + Optional.ofNullable(blobsWithCommitments), + setCodeTransactionPayloads); } public Transaction signAndBuild(final KeyPair keys) { @@ -1275,6 +1351,7 @@ SECPSignature computeSignature(final KeyPair keys) { payload, accessList, versionedHashes, + setCodeTransactionPayloads, chainId), keys); } @@ -1298,5 +1375,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..80d59de83be --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoder.java @@ -0,0 +1,116 @@ +/* + * 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.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import java.math.BigInteger; +import java.util.Optional; +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.SET_CODE) + .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 -> + decodeInnerPayload(setCodeTransactionPayloadsRLPInput))); + + final byte recId = (byte) input.readUnsignedByteScalar(); + final BigInteger r = input.readUInt256Scalar().toUnsignedBigInteger(); + final BigInteger s = input.readUInt256Scalar().toUnsignedBigInteger(); + + input.leaveList(); + + final Transaction transaction = + builder.signature(SIGNATURE_ALGORITHM.get().createSignature(r, s, recId)).build(); + + return transaction; + } + + public static org.hyperledger.besu.datatypes.SetCodeAuthorization decodeInnerPayload( + final RLPInput input) { + input.enterList(); + final BigInteger chainId = input.readBigIntegerScalar(); + final Address address = Address.wrap(input.readBytes()); + + Optional nonce = Optional.empty(); + + if (!input.nextIsList()) { + throw new IllegalArgumentException("Optional nonce must be an list, but isn't"); + } + + final long noncesSize = input.nextSize(); + + input.enterList(); + if (noncesSize == 1) { + nonce = Optional.ofNullable(input.readLongScalar()); + } else if (noncesSize > 1) { + throw new IllegalArgumentException("Nonce list may only have 1 member, if any"); + } + input.leaveList(); + + final byte yParity = (byte) input.readUnsignedByteScalar(); + final BigInteger r = input.readUInt256Scalar().toUnsignedBigInteger(); + final BigInteger s = input.readUInt256Scalar().toUnsignedBigInteger(); + + input.leaveList(); + + final SECPSignature signature = SIGNATURE_ALGORITHM.get().createSignature(r, s, yParity); + + return new SetCodeAuthorization(chainId, address, nonce, 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..05808f3129f --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoder.java @@ -0,0 +1,89 @@ +/* + * 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 encodeSingleSetCodeWithoutSignature( + final SetCodeAuthorization payload, final RLPOutput rlpOutput) { + rlpOutput.startList(); + encodeAuthorizationDetails(payload, rlpOutput); + rlpOutput.endList(); + } + + public static void encodeSingleSetCode( + final SetCodeAuthorization payload, final RLPOutput rlpOutput) { + rlpOutput.startList(); + encodeAuthorizationDetails(payload, rlpOutput); + rlpOutput.writeIntScalar(payload.signature().getRecId()); + rlpOutput.writeBigIntegerScalar(payload.signature().getR()); + rlpOutput.writeBigIntegerScalar(payload.signature().getS()); + rlpOutput.endList(); + } + + private static void encodeAuthorizationDetails( + final SetCodeAuthorization payload, final RLPOutput rlpOutput) { + rlpOutput.writeBigIntegerScalar(payload.chainId()); + rlpOutput.writeBytes(payload.address().copy()); + rlpOutput.startList(); + payload.nonce().ifPresent(rlpOutput::writeLongScalar); + 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 + .getAuthorizationList() + .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/AuthorityProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AuthorityProcessor.java new file mode 100644 index 00000000000..f79e2c98e3a --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AuthorityProcessor.java @@ -0,0 +1,88 @@ +/* + * 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.ethereum.core.Transaction; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.account.AccountState; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.math.BigInteger; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthorityProcessor { + private static final Logger LOG = LoggerFactory.getLogger(AuthorityProcessor.class); + + private final Optional maybeChainId; + + public AuthorityProcessor(final Optional maybeChainId) { + this.maybeChainId = maybeChainId; + } + + public void addContractToAuthority( + final WorldUpdater worldUpdater, + final AuthorizedCodeService authorizedCodeService, + final Transaction transaction) { + + transaction + .getAuthorizationList() + .get() + .forEach( + payload -> + payload + .authorizer() + .ifPresent( + authorityAddress -> { + LOG.trace("Set code authority: {}", authorityAddress); + + if (maybeChainId.isPresent() + && !payload.chainId().equals(BigInteger.ZERO) + && !maybeChainId.get().equals(payload.chainId())) { + return; + } + + final Optional maybeAccount = + Optional.ofNullable(worldUpdater.getAccount(authorityAddress)); + final long accountNonce = + maybeAccount.map(AccountState::getNonce).orElse(0L); + + if (payload.nonce().isPresent() + && !payload.nonce().get().equals(accountNonce)) { + return; + } + + if (authorizedCodeService.hasAuthorizedCode(authorityAddress)) { + return; + } + + Optional codeAccount = + Optional.ofNullable(worldUpdater.get(payload.address())); + final Bytes code; + if (codeAccount.isPresent()) { + code = codeAccount.get().getCode(); + } else { + code = Bytes.EMPTY; + } + + authorizedCodeService.addAuthorizedCode(authorityAddress, code); + })); + } +} 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 4ae30f8de9a..6f09df923eb 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 @@ -595,7 +595,8 @@ static ProtocolSpecBuilder cancunDefinition( true, evmConfiguration.evmStackSize(), feeMarket, - CoinbaseFeePriceCalculator.eip1559())) + CoinbaseFeePriceCalculator.eip1559(), + new AuthorityProcessor(chainId))) // change to check for max blob gas per block for EIP-4844 .transactionValidatorFactoryBuilder( (evm, gasLimitCalculator, feeMarket) -> @@ -658,6 +659,23 @@ static ProtocolSpecBuilder pragueDefinition( // EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests .requestProcessorCoordinator(pragueRequestsProcessors(depositContractAddress)) + // change to accept EIP-7702 transactions + .transactionValidatorFactoryBuilder( + (evm, gasLimitCalculator, feeMarket) -> + new TransactionValidatorFactory( + evm.getGasCalculator(), + gasLimitCalculator, + feeMarket, + true, + chainId, + Set.of( + TransactionType.FRONTIER, + TransactionType.ACCESS_LIST, + TransactionType.EIP1559, + TransactionType.BLOB, + TransactionType.SET_CODE), + evm.getEvmVersion().getMaxInitcodeSize())) + // 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 53801d14cd3..9ad1db7d896 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 @@ -42,6 +42,7 @@ import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.processor.AbstractMessageProcessor; import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Deque; @@ -80,6 +81,8 @@ public class MainnetTransactionProcessor { protected final FeeMarket feeMarket; private final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator; + private final Optional maybeAuthorityProcessor; + public MainnetTransactionProcessor( final GasCalculator gasCalculator, final TransactionValidatorFactory transactionValidatorFactory, @@ -90,6 +93,30 @@ public MainnetTransactionProcessor( final int maxStackSize, final FeeMarket feeMarket, final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator) { + this( + gasCalculator, + transactionValidatorFactory, + contractCreationProcessor, + messageCallProcessor, + clearEmptyAccounts, + warmCoinbase, + maxStackSize, + feeMarket, + coinbaseFeePriceCalculator, + null); + } + + public MainnetTransactionProcessor( + final GasCalculator gasCalculator, + final TransactionValidatorFactory transactionValidatorFactory, + final AbstractMessageProcessor contractCreationProcessor, + final AbstractMessageProcessor messageCallProcessor, + final boolean clearEmptyAccounts, + final boolean warmCoinbase, + final int maxStackSize, + final FeeMarket feeMarket, + final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator, + final AuthorityProcessor maybeAuthorityProcessor) { this.gasCalculator = gasCalculator; this.transactionValidatorFactory = transactionValidatorFactory; this.contractCreationProcessor = contractCreationProcessor; @@ -99,6 +126,7 @@ public MainnetTransactionProcessor( this.maxStackSize = maxStackSize; this.feeMarket = feeMarket; this.coinbaseFeePriceCalculator = coinbaseFeePriceCalculator; + this.maybeAuthorityProcessor = Optional.ofNullable(maybeAuthorityProcessor); } /** @@ -259,6 +287,8 @@ public TransactionProcessingResult processTransaction( final PrivateMetadataUpdater privateMetadataUpdater, final Wei blobGasPrice) { try { + final AuthorizedCodeService authorizedCodeService = new AuthorizedCodeService(); + worldState.setAuthorizedCodeService(authorizedCodeService); final var transactionValidator = transactionValidatorFactory.get(); LOG.trace("Starting execution of {}", transaction); ValidationResult validationResult = @@ -332,15 +362,19 @@ public TransactionProcessingResult processTransaction( transaction.getPayload(), transaction.isContractCreation()); final long accessListGas = gasCalculator.accessListGasCost(accessListEntries.size(), accessListStorageCount); - final long gasAvailable = transaction.getGasLimit() - intrinsicGas - accessListGas; + final long setCodeGas = gasCalculator.setCodeListGasCost(transaction.authorizationListSize()); + final long gasAvailable = + transaction.getGasLimit() - intrinsicGas - accessListGas - setCodeGas; LOG.trace( - "Gas available for execution {} = {} - {} - {} (limit - intrinsic - accessList)", + "Gas available for execution {} = {} - {} - {} - {} (limit - intrinsic - accessList - setCode)", gasAvailable, transaction.getGasLimit(), intrinsicGas, - accessListGas); + accessListGas, + setCodeGas); final WorldUpdater worldUpdater = worldState.updater(); + worldUpdater.setAuthorizedCodeService(authorizedCodeService); final ImmutableMap.Builder contextVariablesBuilder = ImmutableMap.builder() .put(KEY_IS_PERSISTING_PRIVATE_STATE, isPersistingPrivateState) @@ -374,6 +408,15 @@ public TransactionProcessingResult processTransaction( if (transaction.getVersionedHashes().isPresent()) { commonMessageFrameBuilder.versionedHashes( Optional.of(transaction.getVersionedHashes().get().stream().toList())); + } else if (transaction.getAuthorizationList().isPresent()) { + if (maybeAuthorityProcessor.isEmpty()) { + throw new RuntimeException("Authority processor is required for 7702 transactions"); + } + + maybeAuthorityProcessor + .get() + .addContractToAuthority(worldUpdater, authorizedCodeService, transaction); + addressList.addAll(authorizedCodeService.getAuthorities()); } else { commonMessageFrameBuilder.versionedHashes(Optional.empty()); } @@ -392,6 +435,7 @@ public TransactionProcessingResult processTransaction( .contract(contractAddress) .inputData(initCodeBytes.slice(code.getSize())) .code(code) + .authorizedCodeService(authorizedCodeService) .build(); } else { @SuppressWarnings("OptionalGetWithoutIsPresent") // isContractCall tests isPresent @@ -407,6 +451,7 @@ public TransactionProcessingResult processTransaction( maybeContract .map(c -> messageCallProcessor.getCodeFromEVM(c.getCodeHash(), c.getCode())) .orElse(CodeV0.EMPTY_CODE)) + .authorizedCodeService(authorizedCodeService) .build(); } Deque messageFrameStack = initialFrame.getMessageFrameStack(); @@ -485,6 +530,7 @@ public TransactionProcessingResult processTransaction( coinbaseCalculator.price(usedGas, transactionGasPrice, blockHeader.getBaseFee()); coinbase.incrementBalance(coinbaseWeiDelta); + authorizedCodeService.resetAuthorities(); operationTracer.traceEndTransaction( worldUpdater, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java index 9d90a409a5e..481ce70e052 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java @@ -189,7 +189,8 @@ private ValidationResult validateCostAndFee( final long intrinsicGasCost = gasCalculator.transactionIntrinsicGasCost( transaction.getPayload(), transaction.isContractCreation()) - + (transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L)); + + (transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L)) + + gasCalculator.setCodeListGasCost(transaction.authorizationListSize()); if (Long.compareUnsigned(intrinsicGasCost, transaction.getGasLimit()) > 0) { return ValidationResult.invalid( TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/privacy/PrivateMutableWorldStateUpdater.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/privacy/PrivateMutableWorldStateUpdater.java index fc20db7f813..b4e8e285617 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/privacy/PrivateMutableWorldStateUpdater.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/privacy/PrivateMutableWorldStateUpdater.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Collection; @@ -30,35 +31,39 @@ public class PrivateMutableWorldStateUpdater implements WorldUpdater { protected final WorldUpdater publicWorldUpdater; protected final WorldUpdater privateWorldUpdater; + private AuthorizedCodeService authorizedCodeService; public PrivateMutableWorldStateUpdater( final WorldUpdater publicWorldUpdater, final WorldUpdater privateWorldUpdater) { this.publicWorldUpdater = publicWorldUpdater; this.privateWorldUpdater = privateWorldUpdater; + this.authorizedCodeService = new AuthorizedCodeService(); } @Override public MutableAccount createAccount(final Address address, final long nonce, final Wei balance) { - return privateWorldUpdater.createAccount(address, nonce, balance); + return authorizedCodeService.processMutableAccount( + this, privateWorldUpdater.createAccount(address, nonce, balance), address); } @Override public MutableAccount createAccount(final Address address) { - return privateWorldUpdater.createAccount(address); + return authorizedCodeService.processMutableAccount( + this, privateWorldUpdater.createAccount(address), address); } @Override public MutableAccount getAccount(final Address address) { final MutableAccount privateAccount = privateWorldUpdater.getAccount(address); if (privateAccount != null && !privateAccount.isEmpty()) { - return privateAccount; + return authorizedCodeService.processMutableAccount(this, privateAccount, address); } final MutableAccount publicAccount = publicWorldUpdater.getAccount(address); if (publicAccount != null && !publicAccount.isEmpty()) { publicAccount.becomeImmutable(); - return publicAccount; + return authorizedCodeService.processMutableAccount(this, publicAccount, address); } - return privateAccount; + return authorizedCodeService.processMutableAccount(this, privateAccount, address); } @Override @@ -104,4 +109,9 @@ public WorldUpdater updater() { public Optional parentUpdater() { return privateWorldUpdater.parentUpdater(); } + + @Override + public void setAuthorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/common/worldview/accumulator/DiffBasedWorldStateUpdateAccumulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/common/worldview/accumulator/DiffBasedWorldStateUpdateAccumulator.java index 149cefbe540..b5bae0aed9b 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/common/worldview/accumulator/DiffBasedWorldStateUpdateAccumulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/common/worldview/accumulator/DiffBasedWorldStateUpdateAccumulator.java @@ -136,7 +136,8 @@ public MutableAccount createAccount(final Address address, final long nonce, fin accountsToUpdate.put(address, diffBasedValue); } else if (diffBasedValue.getUpdated() != null) { if (diffBasedValue.getUpdated().isEmpty()) { - return track(new UpdateTrackingAccount<>(diffBasedValue.getUpdated())); + return authorizedCodeService.processMutableAccount( + this, track(new UpdateTrackingAccount<>(diffBasedValue.getUpdated())), address); } else { throw new IllegalStateException("Cannot create an account when one already exists"); } @@ -153,7 +154,8 @@ public MutableAccount createAccount(final Address address, final long nonce, fin Hash.EMPTY, true); diffBasedValue.setUpdated(newAccount); - return track(new UpdateTrackingAccount<>(newAccount)); + return authorizedCodeService.processMutableAccount( + this, track(new UpdateTrackingAccount<>(newAccount)), address); } @Override 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..2709b94e294 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,7 @@ public Transaction transaction( case EIP1559 -> eip1559Transaction(payload, to); case ACCESS_LIST -> accessListTransaction(payload, to); case BLOB -> blobTransaction(payload, to); + 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..3f7049be399 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,8 @@ public Transaction createTransaction(final KeyPair keys) { builder.versionedHashes(versionedHashes.get()); } break; + case SET_CODE: + break; } to.ifPresent(builder::to); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoderTest.java new file mode 100644 index 00000000000..610b5906f54 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionDecoderTest.java @@ -0,0 +1,120 @@ +/* + * 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.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +class SetCodeTransactionDecoderTest { + + @Test + void shouldDecodeInnerPayloadWithNonce() { + // "0xd80194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c105" + + final BytesValueRLPInput input = + new BytesValueRLPInput( + Bytes.fromHexString( + "0xf85b0194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c18080a0840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5a03b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99"), + true); + final SetCodeAuthorization authorization = SetCodeTransactionDecoder.decodeInnerPayload(input); + + assertThat(authorization.chainId()).isEqualTo(BigInteger.ONE); + assertThat(authorization.address()) + .isEqualTo(Address.fromHexStringStrict("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56")); + assertThat(authorization.nonce().get()).isEqualTo(0L); + + final SECPSignature signature = authorization.signature(); + assertThat(signature.getRecId()).isEqualTo((byte) 0); + assertThat(signature.getR().toString(16)) + .isEqualTo("840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5"); + assertThat(signature.getS().toString(16)) + .isEqualTo("3b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99"); + } + + @Test + void shouldDecodeInnerPayloadWithoutNonce() { + // "0xd70194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c5" + + final BytesValueRLPInput input = + new BytesValueRLPInput( + Bytes.fromHexString( + "0xf85a0194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c001a0dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148a025b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031"), + true); + final SetCodeAuthorization authorization = SetCodeTransactionDecoder.decodeInnerPayload(input); + + assertThat(authorization.chainId()).isEqualTo(BigInteger.ONE); + assertThat(authorization.address()) + .isEqualTo(Address.fromHexStringStrict("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56")); + assertThat(authorization.nonce()).isEmpty(); + + final SECPSignature signature = authorization.signature(); + assertThat(signature.getRecId()).isEqualTo((byte) 1); + assertThat(signature.getR().toString(16)) + .isEqualTo("dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148"); + assertThat(signature.getS().toString(16)) + .isEqualTo("25b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031"); + } + + @Test + void shouldThrowInnerPayloadWithMultipleNonces() { + // "d90194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c20107" + + final BytesValueRLPInput input = + new BytesValueRLPInput( + Bytes.fromHexString( + "0xf85c0194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c2010201a0401b5d4ebe88306448115d1a46a30e5ad1136f2818b4ebb0733d9c4efffd135aa0753ff1dbce6db504ecb9635a64d8c4506ff887e2d2a0d2b7175baf94c849eccc"), + true); + + assertThrows( + IllegalArgumentException.class, + () -> { + SetCodeTransactionDecoder.decodeInnerPayload(input); + }); + } + + @Test + void shouldDecodeInnerPayloadWithoutNonceAndChainIdZero() { + // "d70094633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c5" + + final BytesValueRLPInput input = + new BytesValueRLPInput( + Bytes.fromHexString( + "0xf85a0094633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c001a0025c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2a03c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df"), + true); + final SetCodeAuthorization authorization = SetCodeTransactionDecoder.decodeInnerPayload(input); + + assertThat(authorization.chainId()).isEqualTo(BigInteger.ZERO); + assertThat(authorization.address()) + .isEqualTo(Address.fromHexStringStrict("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56")); + assertThat(authorization.nonce().isEmpty()).isTrue(); + + final SECPSignature signature = authorization.signature(); + assertThat(signature.getRecId()).isEqualTo((byte) 1); + assertThat(signature.getR().toString(16)) + .isEqualTo("25c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2"); + assertThat(signature.getS().toString(16)) + .isEqualTo("3c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df"); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoderTest.java new file mode 100644 index 00000000000..89211f4ba31 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/SetCodeTransactionEncoderTest.java @@ -0,0 +1,122 @@ +/* + * 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.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.core.SetCodeAuthorization; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SetCodeTransactionEncoderTest { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + BytesValueRLPOutput output; + + @BeforeEach + void setUp() { + output = new BytesValueRLPOutput(); + } + + @Test + void shouldEncodeSingleSetCodeWithNonce() { + // "0xd80194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c105" + + final SetCodeAuthorization authorization = + new SetCodeAuthorization( + BigInteger.ONE, + Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), + Optional.of(0L), + SIGNATURE_ALGORITHM + .get() + .createSignature( + new BigInteger( + "840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5", 16), + new BigInteger( + "3b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99", 16), + (byte) 0)); + + SetCodeTransactionEncoder.encodeSingleSetCode(authorization, output); + + assertThat(output.encoded()) + .isEqualTo( + Bytes.fromHexString( + "0xf85b0194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c18080a0840798fa67118e034c1eb7e42fe89e28d7cd5006dc813d5729e5f75b0d1a7ec5a03b1dbace38ceb862a65bf2eac0637693b5c3493bcb2a022dd614c0a74cce0b99")); + } + + @Test + void shouldEncodeSingleSetCodeWithoutNonce() { + // "0xd70194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c5" + + final SetCodeAuthorization authorization = + new SetCodeAuthorization( + BigInteger.ONE, + Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), + Optional.empty(), + SIGNATURE_ALGORITHM + .get() + .createSignature( + new BigInteger( + "dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148", 16), + new BigInteger( + "25b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031", 16), + (byte) 1)); + + SetCodeTransactionEncoder.encodeSingleSetCode(authorization, output); + + assertThat(output.encoded()) + .isEqualTo( + Bytes.fromHexString( + "0xf85a0194633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c001a0dd6b24048be1b7d7fe5bbbb73ffc37eb2ce1997ecb4ae5b6096532ef19363148a025b58a1ff8ad00bddbbfa1d5c2411961cbb6d08dcdc8ae88303db3c6cf983031")); + } + + @Test + void shouldEncodeSingleSetCodeWithoutNonceAndChainIdZero() { + // "d70094633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c5" + + final SetCodeAuthorization authorization = + new SetCodeAuthorization( + BigInteger.ZERO, + Address.fromHexString("0x633688abc3cCf8B0C03088D2d1C6ae4958c2fA56"), + Optional.empty(), + SIGNATURE_ALGORITHM + .get() + .createSignature( + new BigInteger( + "25c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2", 16), + new BigInteger( + "3c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df", 16), + (byte) 1)); + + SetCodeTransactionEncoder.encodeSingleSetCode(authorization, output); + + assertThat(output.encoded()) + .isEqualTo( + Bytes.fromHexString( + "0xf85a8094633688abc3ccf8b0c03088d2d1c6ae4958c2fa56c001a0025c1240d7ffec0daeedb752d3357aff2e3cd58468f0c2d43ee0ee999e02ace2a03c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df")); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java index d541cbd99aa..b2f5718dad3 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessorTest.java @@ -39,6 +39,7 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.evm.worldstate.WorldView; +import java.math.BigInteger; import java.util.List; import java.util.Optional; import java.util.Set; @@ -87,7 +88,8 @@ MainnetTransactionProcessor createTransactionProcessor(final boolean warmCoinbas warmCoinbase, MAX_STACK_SIZE, FeeMarket.legacy(), - CoinbaseFeePriceCalculator.frontier()); + CoinbaseFeePriceCalculator.frontier(), + new AuthorityProcessor(Optional.of(BigInteger.ONE))); } @Test 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..6ce8f47a7aa 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,10 @@ private int computeBlobMemorySize() { + computeBlobWithCommitmentsMemorySize(); } + private int computeSetCodeMemorySize() { + return 0; + } + private int computeBlobWithCommitmentsMemorySize() { final int blobCount = transaction.getBlobCount(); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java index e717adbed12..bbd4e7322ff 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java @@ -128,7 +128,7 @@ protected Transaction createTransaction( final TransactionType txType = TransactionType.values()[randomizeTxType.nextInt(4)]; return switch (txType) { - case FRONTIER, ACCESS_LIST, EIP1559 -> + case FRONTIER, ACCESS_LIST, EIP1559, SET_CODE -> createTransaction(txType, nonce, maxGasPrice, payloadSize, keys); case BLOB -> createTransaction( diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java index a56222345ce..c5c2bdcf976 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java @@ -1377,6 +1377,7 @@ private PendingTransaction getOrCreate( case ACCESS_LIST -> createAccessListPendingTransaction(sender, n); case EIP1559 -> createEIP1559PendingTransaction(sender, n); case BLOB -> createBlobPendingTransaction(sender, n); + case SET_CODE -> throw new UnsupportedOperationException(); }); } diff --git a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java index ee6c5d5f709..6f222166b34 100644 --- a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java +++ b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java @@ -35,6 +35,7 @@ import org.hyperledger.besu.ethereum.core.ConsolidationRequest; import org.hyperledger.besu.ethereum.core.DepositRequest; import org.hyperledger.besu.ethereum.core.Request; +import org.hyperledger.besu.ethereum.core.SetCodeAuthorization; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.WithdrawalRequest; @@ -212,6 +213,69 @@ protected static List extractTransactions( builder.accessList(entries); } + if (txNode.has("authorizationList")) { + JsonNode authorizationList = txNode.get("authorizationList"); + + if (!authorizationList.isArray()) { + out.printf( + "TX json node unparseable: expected authorizationList to be an array - %s%n", + txNode); + continue; + } + + List authorizations = + new ArrayList<>(authorizationList.size()); + for (JsonNode entryAsJson : authorizationList) { + final BigInteger authorizationChainId = + Bytes.fromHexStringLenient(entryAsJson.get("chainId").textValue()) + .toUnsignedBigInteger(); + final Address authorizationAddress = + Address.fromHexString(entryAsJson.get("address").textValue()); + + JsonNode nonces = entryAsJson.get("nonce"); + + if (nonces == null) { + out.printf( + "TX json node unparseable: expected nonce field to be provided - %s%n", + txNode); + continue; + } + + List authorizationNonces; + if (nonces.isArray()) { + authorizationNonces = new ArrayList<>(nonces.size()); + for (JsonNode nonceAsJson : nonces) { + authorizationNonces.add( + Bytes.fromHexStringLenient(nonceAsJson.textValue()).toLong()); + } + } else { + authorizationNonces = + List.of(Bytes.fromHexStringLenient(nonces.textValue()).toLong()); + } + + final byte authorizationV = + Bytes.fromHexStringLenient(entryAsJson.get("v").textValue()) + .toUnsignedBigInteger() + .byteValueExact(); + final BigInteger authorizationR = + Bytes.fromHexStringLenient(entryAsJson.get("r").textValue()) + .toUnsignedBigInteger(); + final BigInteger authorizationS = + Bytes.fromHexStringLenient(entryAsJson.get("s").textValue()) + .toUnsignedBigInteger(); + + authorizations.add( + SetCodeAuthorization.createSetCodeAuthorizationEntry( + authorizationChainId, + authorizationAddress, + authorizationNonces, + authorizationV, + authorizationR, + authorizationS)); + } + builder.setCodeTransactionPayloads(authorizations); + } + if (txNode.has("blobVersionedHashes")) { JsonNode blobVersionedHashes = txNode.get("blobVersionedHashes"); if (!blobVersionedHashes.isArray()) { @@ -255,7 +319,10 @@ protected static List extractTransactions( Bytes.fromHexStringLenient(txNode.get("s").textValue()) .toUnsignedBigInteger(), v.byteValueExact())); - transactions.add(builder.build()); + + final Transaction tx = builder.build(); + + transactions.add(tx); } } } else { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/account/AuthorizedCodeAccount.java b/evm/src/main/java/org/hyperledger/besu/evm/account/AuthorizedCodeAccount.java new file mode 100644 index 00000000000..46acac74f93 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/account/AuthorizedCodeAccount.java @@ -0,0 +1,100 @@ +/* + * 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.evm.account; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; + +import java.util.NavigableMap; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + +/** Wraps an EOA account and includes authorized code to be run on behalf of it. */ +public class AuthorizedCodeAccount implements Account { + private final Account wrappedAccount; + private final Bytes authorizedCode; + + /** The hash of the authorized code. */ + protected Hash codeHash = null; + + /** + * Creates a new AuthorizedCodeAccount. + * + * @param wrappedAccount the account that has authorized code to be loaded into it. + * @param authorizedCode the authorized code. + */ + public AuthorizedCodeAccount(final Account wrappedAccount, final Bytes authorizedCode) { + this.wrappedAccount = wrappedAccount; + this.authorizedCode = authorizedCode; + } + + @Override + public Address getAddress() { + return wrappedAccount.getAddress(); + } + + @Override + public boolean isStorageEmpty() { + return wrappedAccount.isStorageEmpty(); + } + + @Override + public Hash getAddressHash() { + return wrappedAccount.getAddressHash(); + } + + @Override + public long getNonce() { + return wrappedAccount.getNonce(); + } + + @Override + public Wei getBalance() { + return wrappedAccount.getBalance(); + } + + @Override + public Bytes getCode() { + return authorizedCode; + } + + @Override + public Hash getCodeHash() { + if (codeHash == null) { + codeHash = authorizedCode.equals(Bytes.EMPTY) ? Hash.EMPTY : Hash.hash(authorizedCode); + } + + return codeHash; + } + + @Override + public UInt256 getStorageValue(final UInt256 key) { + return wrappedAccount.getStorageValue(key); + } + + @Override + public UInt256 getOriginalStorageValue(final UInt256 key) { + return wrappedAccount.getOriginalStorageValue(key); + } + + @Override + public NavigableMap storageEntriesFrom( + final Bytes32 startKeyHash, final int limit) { + return wrappedAccount.storageEntriesFrom(startKeyHash, limit); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/account/MutableAuthorizedCodeAccount.java b/evm/src/main/java/org/hyperledger/besu/evm/account/MutableAuthorizedCodeAccount.java new file mode 100644 index 00000000000..6d4e30a9c2e --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/account/MutableAuthorizedCodeAccount.java @@ -0,0 +1,138 @@ +/* + * 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.evm.account; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; + +import java.util.Map; +import java.util.NavigableMap; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + +/** Wraps a mutable EOA account and includes authorized code to be run on behalf of it. */ +public class MutableAuthorizedCodeAccount implements MutableAccount { + + private final MutableAccount wrappedAccount; + private final Bytes authorizedCode; + + /** The hash of the authorized code. */ + protected Hash codeHash = null; + + /** + * Creates a new MutableAuthorizedCodeAccount. + * + * @param wrappedAccount the account that has authorized code to be loaded into it. + * @param authorizedCode the authorized code. + */ + public MutableAuthorizedCodeAccount( + final MutableAccount wrappedAccount, final Bytes authorizedCode) { + this.wrappedAccount = wrappedAccount; + this.authorizedCode = authorizedCode; + } + + @Override + public Address getAddress() { + return wrappedAccount.getAddress(); + } + + @Override + public boolean isStorageEmpty() { + return wrappedAccount.isStorageEmpty(); + } + + @Override + public Hash getAddressHash() { + return wrappedAccount.getAddressHash(); + } + + @Override + public long getNonce() { + return wrappedAccount.getNonce(); + } + + @Override + public Wei getBalance() { + return wrappedAccount.getBalance(); + } + + @Override + public Bytes getCode() { + return authorizedCode; + } + + @Override + public Hash getCodeHash() { + if (codeHash == null) { + codeHash = authorizedCode.equals(Bytes.EMPTY) ? Hash.EMPTY : Hash.hash(authorizedCode); + } + + return codeHash; + } + + @Override + public UInt256 getStorageValue(final UInt256 key) { + return wrappedAccount.getStorageValue(key); + } + + @Override + public UInt256 getOriginalStorageValue(final UInt256 key) { + return wrappedAccount.getOriginalStorageValue(key); + } + + @Override + public NavigableMap storageEntriesFrom( + final Bytes32 startKeyHash, final int limit) { + return wrappedAccount.storageEntriesFrom(startKeyHash, limit); + } + + @Override + public void setNonce(final long value) { + wrappedAccount.setNonce(value); + } + + @Override + public void setBalance(final Wei value) { + wrappedAccount.setBalance(value); + } + + @Override + public void setCode(final Bytes code) { + throw new RuntimeException("Cannot set code on an AuthorizedCodeAccount"); + } + + @Override + public void setStorageValue(final UInt256 key, final UInt256 value) { + wrappedAccount.setStorageValue(key, value); + } + + @Override + public void clearStorage() { + wrappedAccount.clearStorage(); + } + + @Override + public Map getUpdatedStorage() { + return wrappedAccount.getUpdatedStorage(); + } + + @Override + public void becomeImmutable() { + wrappedAccount.becomeImmutable(); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/fluent/SimpleWorld.java b/evm/src/main/java/org/hyperledger/besu/evm/fluent/SimpleWorld.java index f52e788babf..c64ca3744fd 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/fluent/SimpleWorld.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/fluent/SimpleWorld.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Collection; @@ -34,6 +35,8 @@ public class SimpleWorld implements WorldUpdater { /** The Accounts. */ Map accounts = new HashMap<>(); + private AuthorizedCodeService authorizedCodeService; + /** Instantiates a new Simple world. */ public SimpleWorld() { this(null); @@ -46,6 +49,7 @@ public SimpleWorld() { */ public SimpleWorld(final SimpleWorld parent) { this.parent = parent; + this.authorizedCodeService = new AuthorizedCodeService(); } @Override @@ -56,11 +60,11 @@ public WorldUpdater updater() { @Override public Account get(final Address address) { if (accounts.containsKey(address)) { - return accounts.get(address); + return authorizedCodeService.processAccount(this, accounts.get(address), address); } else if (parent != null) { - return parent.get(address); + return authorizedCodeService.processAccount(this, parent.get(address), address); } else { - return null; + return authorizedCodeService.processAccount(this, null, address); } } @@ -71,14 +75,14 @@ public MutableAccount createAccount(final Address address, final long nonce, fin } SimpleAccount account = new SimpleAccount(address, nonce, balance); accounts.put(address, account); - return account; + return authorizedCodeService.processMutableAccount(this, account, address); } @Override public MutableAccount getAccount(final Address address) { SimpleAccount account = accounts.get(address); if (account != null) { - return account; + return authorizedCodeService.processMutableAccount(this, account, address); } Account parentAccount = parent == null ? null : parent.getAccount(address); if (parentAccount != null) { @@ -90,9 +94,9 @@ public MutableAccount getAccount(final Address address) { parentAccount.getBalance(), parentAccount.getCode()); accounts.put(address, account); - return account; + return authorizedCodeService.processMutableAccount(this, account, address); } - return null; + return authorizedCodeService.processMutableAccount(this, null, address); } @Override @@ -132,4 +136,9 @@ public void commit() { public Optional parentUpdater() { return Optional.ofNullable(parent); } + + @Override + public void setAuthorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index e01c9155106..cf776192299 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -33,6 +33,7 @@ import org.hyperledger.besu.evm.log.Log; import org.hyperledger.besu.evm.operation.BlockHashOperation.BlockHashLookup; import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.ArrayDeque; @@ -201,6 +202,7 @@ public enum Type { // Global data fields. private final WorldUpdater worldUpdater; + private final AuthorizedCodeService authorizedCodeService; // Metadata fields. private final Type type; @@ -270,7 +272,8 @@ private MessageFrame( final Consumer completer, final Map contextVariables, final Optional revertReason, - final TxValues txValues) { + final TxValues txValues, + final AuthorizedCodeService authorizedCodeService) { this.txValues = txValues; this.type = type; @@ -290,6 +293,7 @@ private MessageFrame( this.completer = completer; this.contextVariables = contextVariables; this.revertReason = revertReason; + this.authorizedCodeService = authorizedCodeService; this.undoMark = txValues.transientStorage().mark(); } @@ -423,6 +427,15 @@ public Bytes getReturnData() { return returnData; } + /** + * Return the authorized account service. + * + * @return the authorized account service + */ + public AuthorizedCodeService getAuthorizedCodeService() { + return authorizedCodeService; + } + /** * Set the return data. * @@ -1347,6 +1360,7 @@ public static class Builder { private Optional reason = Optional.empty(); private Set
accessListWarmAddresses = emptySet(); private Multimap accessListWarmStorage = HashMultimap.create(); + private AuthorizedCodeService authorizedCodeService; private Optional> versionedHashes = Optional.empty(); @@ -1631,6 +1645,17 @@ public Builder versionedHashes(final Optional> versionedHash return this; } + /** + * Sets authorized account service. + * + * @param authorizedCodeService the authorized account service + * @return the builder + */ + public Builder authorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + return this; + } + private void validate() { if (parentMessageFrame == null) { checkState(worldUpdater != null, "Missing message frame world updater"); @@ -1664,6 +1689,11 @@ public MessageFrame build() { WorldUpdater updater; boolean newStatic; TxValues newTxValues; + + if (authorizedCodeService == null) { + authorizedCodeService = new AuthorizedCodeService(); + } + if (parentMessageFrame == null) { newTxValues = new TxValues( @@ -1686,10 +1716,12 @@ public MessageFrame build() { newStatic = isStatic; } else { newTxValues = parentMessageFrame.txValues; - updater = parentMessageFrame.worldUpdater.updater(); + updater = parentMessageFrame.getWorldUpdater().updater(); newStatic = isStatic || parentMessageFrame.isStatic; } + updater.setAuthorizedCodeService(authorizedCodeService); + MessageFrame messageFrame = new MessageFrame( type, @@ -1706,7 +1738,8 @@ public MessageFrame build() { completer, contextVariables == null ? Map.of() : contextVariables, reason, - newTxValues); + newTxValues, + authorizedCodeService); newTxValues.messageFrameStack().addFirst(messageFrame); messageFrame.warmUpAddress(sender); messageFrame.warmUpAddress(contract); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java index 2e1728b2346..0af023851ca 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java @@ -645,4 +645,14 @@ default long computeExcessBlobGas(final long parentExcessBlobGas, final int newB default long computeExcessBlobGas(final long parentExcessBlobGas, final long blobGasUsed) { return 0L; } + + /** + * Returns the upfront gas cost for EIP 7702 operation. + * + * @param authorizationListLength The length of the authorization list + * @return the gas cost + */ + default long setCodeListGasCost(final int authorizationListLength) { + return 0L; + } } 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..0c223d780b0 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,9 @@ public PragueGasCalculator() { protected PragueGasCalculator(final int maxPrecompile) { super(maxPrecompile); } + + @Override + public long setCodeListGasCost(final int authorizationListLength) { + return PER_CONTRACT_CODE_BASE_COST * authorizationListLength; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java index 8a82262204a..762030bf3b5 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCallOperation.java @@ -230,6 +230,7 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { .code(code) .isStatic(isStatic(frame)) .completer(child -> complete(frame, child)) + .authorizedCodeService(frame.getAuthorizedCodeService()) .build(); // see note in stack depth check about incrementing cost frame.incrementRemainingGas(cost); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java index f20c2914a09..16ea218f9c6 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractCreateOperation.java @@ -195,6 +195,7 @@ private void spawnChildMessage(final MessageFrame parent, final Code code, final .apparentValue(value) .code(code) .completer(child -> complete(parent, child, evm)) + .authorizedCodeService(parent.getAuthorizedCodeService()) .build(); parent.setState(MessageFrame.State.CODE_SUSPENDED); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractExtCallOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractExtCallOperation.java index 43d7e343ff1..e19b6cd8a29 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractExtCallOperation.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AbstractExtCallOperation.java @@ -180,6 +180,7 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) { .code(code) .isStatic(isStatic(frame)) .completer(child -> complete(frame, child)) + .authorizedCodeService(frame.getAuthorizedCodeService()) .build(); frame.setState(MessageFrame.State.CODE_SUSPENDED); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AbstractWorldUpdater.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AbstractWorldUpdater.java index 9623eaf89ed..00cd67ddb81 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AbstractWorldUpdater.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AbstractWorldUpdater.java @@ -43,6 +43,9 @@ public abstract class AbstractWorldUpdater> updatedAccounts = new ConcurrentHashMap<>(); @@ -59,6 +62,7 @@ public abstract class AbstractWorldUpdater account = new UpdateTrackingAccount<>(address); account.setNonce(nonce); account.setBalance(balance); - return track(account); + return authorizedCodeService.processMutableAccount(this, track(account), address); } @Override @@ -95,12 +99,12 @@ public Account get(final Address address) { // We may have updated it already, so check that first. final MutableAccount existing = updatedAccounts.get(address); if (existing != null) { - return existing; + return authorizedCodeService.processAccount(this, existing, address); } if (deletedAccounts.contains(address)) { return null; } - return getForMutation(address); + return authorizedCodeService.processAccount(this, getForMutation(address), address); } @Override @@ -108,7 +112,7 @@ public MutableAccount getAccount(final Address address) { // We may have updated it already, so check that first. final MutableAccount existing = updatedAccounts.get(address); if (existing != null) { - return existing; + return authorizedCodeService.processMutableAccount(this, existing, address); } if (deletedAccounts.contains(address)) { return null; @@ -117,9 +121,10 @@ public MutableAccount getAccount(final Address address) { // Otherwise, get it from our wrapped view and create a new update tracker. final A origin = getForMutation(address); if (origin == null) { - return null; + return authorizedCodeService.processMutableAccount(this, null, address); } else { - return track(new UpdateTrackingAccount<>(origin)); + return authorizedCodeService.processMutableAccount( + this, track(new UpdateTrackingAccount<>(origin)), address); } } @@ -165,6 +170,11 @@ public Optional parentUpdater() { } } + @Override + public void setAuthorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + } + /** * The accounts modified in this updater. * diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AuthorizedCodeService.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AuthorizedCodeService.java new file mode 100644 index 00000000000..ff5d94343b4 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/AuthorizedCodeService.java @@ -0,0 +1,116 @@ +/* + * 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.evm.worldstate; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.account.AuthorizedCodeAccount; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.account.MutableAuthorizedCodeAccount; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; + +/** A service that manages the code injection of authorized code. */ +public class AuthorizedCodeService { + private final Map authorizedCode = new HashMap<>(); + + /** Creates a new AuthorizedCodeService. */ + public AuthorizedCodeService() {} + + /** + * Authorizes to load the code of authorizedCode into the authorizer account. + * + * @param authorizer the address that gives the authorization. + * @param authorizedCode the code which will be loaded. + */ + public void addAuthorizedCode(final Address authorizer, final Bytes authorizedCode) { + this.authorizedCode.put(authorizer, authorizedCode); + } + + /** + * Return all the authorities that have given their authorization to load the code of another + * account. + * + * @return the set of authorities. + */ + public Set
getAuthorities() { + return authorizedCode.keySet(); + } + + /** Resets all the authorized accounts. */ + public void resetAuthorities() { + authorizedCode.clear(); + } + + /** + * Checks if the provided address has set an authorized to load code into an EOA account. + * + * @param authority the address to check. + * @return {@code true} if the address has been authorized, {@code false} otherwise. + */ + public boolean hasAuthorizedCode(final Address authority) { + return authorizedCode.containsKey(authority); + } + + /** + * Processes the provided account, injecting the authorized code if authorized. + * + * @param worldUpdater the world updater to retrieve the code account. + * @param originalAccount the account to process. + * @param address the address of the account in case the provided account is null + * @return the processed account, containing the authorized code if authorized. + */ + public Account processAccount( + final WorldUpdater worldUpdater, final Account originalAccount, final Address address) { + if (!authorizedCode.containsKey(address)) { + return originalAccount; + } + + Account account = originalAccount; + if (account == null) { + account = worldUpdater.createAccount(address); + } + + return new AuthorizedCodeAccount(account, authorizedCode.get(address)); + } + + /** + * Processes the provided mutable account, injecting the authorized code if authorized. + * + * @param worldUpdater the world updater to retrieve the code account. + * @param originalAccount the mutable account to process. + * @param address the address of the account in case the provided account is null + * @return the processed mutable account, containing the authorized code if authorized. + */ + public MutableAccount processMutableAccount( + final WorldUpdater worldUpdater, + final MutableAccount originalAccount, + final Address address) { + if (!authorizedCode.containsKey(address)) { + return originalAccount; + } + + MutableAccount account = originalAccount; + if (account == null) { + account = worldUpdater.createAccount(address); + } + + return new MutableAuthorizedCodeAccount(account, authorizedCode.get(address)); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledUpdater.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledUpdater.java index 9987a5be752..2497ed348d7 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledUpdater.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/JournaledUpdater.java @@ -41,6 +41,7 @@ public class JournaledUpdater implements WorldUpdater { final UndoMap accounts; final UndoSet
deleted; final long undoMark; + private AuthorizedCodeService authorizedCodeService; /** * Instantiates a new Stacked updater. @@ -66,6 +67,7 @@ public JournaledUpdater(final WorldUpdater world, final EvmConfiguration evmConf "WorldUpdater must be a JournaledWorldUpdater or an AbstractWorldUpdater"); } undoMark = accounts.mark(); + this.authorizedCodeService = new AuthorizedCodeService(); } /** @@ -126,12 +128,18 @@ public void markTransactionBoundary() { accounts.values().forEach(JournaledAccount::markTransactionBoundary); } + @Override + public void setAuthorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + } + @Override public MutableAccount createAccount(final Address address, final long nonce, final Wei balance) { JournaledAccount journaledAccount = new JournaledAccount(rootWorld.createAccount(address, nonce, balance)); accounts.put(address, journaledAccount); - return new JournaledAccount(journaledAccount); + return authorizedCodeService.processMutableAccount( + this, new JournaledAccount(journaledAccount), address); } @Override @@ -139,7 +147,7 @@ public MutableAccount getAccount(final Address address) { // We may have updated it already, so check that first. final JournaledAccount existing = accounts.get(address); if (existing != null) { - return existing; + return authorizedCodeService.processMutableAccount(this, existing, address); } if (deleted.contains(address)) { return null; @@ -148,11 +156,11 @@ public MutableAccount getAccount(final Address address) { // Otherwise, get it from our wrapped view and create a new update tracker. final MutableAccount origin = rootWorld.getAccount(address); if (origin == null) { - return null; + return authorizedCodeService.processMutableAccount(this, null, address); } else { var newAccount = new JournaledAccount(origin); accounts.put(address, newAccount); - return newAccount; + return authorizedCodeService.processMutableAccount(this, newAccount, address); } } @@ -169,12 +177,12 @@ public void deleteAccount(final Address address) { public Account get(final Address address) { final MutableAccount existing = accounts.get(address); if (existing != null) { - return existing; + return authorizedCodeService.processAccount(this, existing, address); } if (deleted.contains(address)) { - return null; + return authorizedCodeService.processAccount(this, null, address); } - return rootWorld.get(address); + return authorizedCodeService.processAccount(this, rootWorld.get(address), address); } @Override diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldUpdater.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldUpdater.java index 4cbda732798..5144176e799 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldUpdater.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldUpdater.java @@ -179,4 +179,12 @@ default void clearAccountsThatAreEmpty() { default void markTransactionBoundary() { // default is to ignore } + + /** + * Sets the {@link AuthorizedCodeService} associated with this {@link WorldUpdater}. + * + * @param authorizedCodeService the {@link AuthorizedCodeService} to associate with this {@link + * WorldUpdater} + */ + void setAuthorizedCodeService(AuthorizedCodeService authorizedCodeService); } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/toy/ToyWorld.java b/evm/src/test/java/org/hyperledger/besu/evm/toy/ToyWorld.java index e1e9c4c3ad3..479376a8fe3 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/toy/ToyWorld.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/toy/ToyWorld.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.AuthorizedCodeService; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Collection; @@ -32,6 +33,7 @@ public class ToyWorld implements WorldUpdater { ToyWorld parent; Map accounts = new HashMap<>(); + private AuthorizedCodeService authorizedCodeService; public ToyWorld() { this(null); @@ -39,6 +41,7 @@ public ToyWorld() { public ToyWorld(final ToyWorld parent) { this.parent = parent; + this.authorizedCodeService = new AuthorizedCodeService(); } @Override @@ -49,11 +52,11 @@ public WorldUpdater updater() { @Override public Account get(final Address address) { if (accounts.containsKey(address)) { - return accounts.get(address); + return authorizedCodeService.processAccount(this, accounts.get(address), address); } else if (parent != null) { - return parent.get(address); + return authorizedCodeService.processAccount(this, parent.get(address), address); } else { - return null; + return authorizedCodeService.processAccount(this, null, address); } } @@ -70,17 +73,17 @@ public MutableAccount createAccount( final Bytes code) { ToyAccount account = new ToyAccount(parentAccount, address, nonce, balance, code); accounts.put(address, account); - return account; + return authorizedCodeService.processMutableAccount(this, account, address); } @Override public MutableAccount getAccount(final Address address) { if (accounts.containsKey(address)) { - return accounts.get(address); + return authorizedCodeService.processMutableAccount(this, accounts.get(address), address); } else if (parent != null) { Account parentAccount = parent.getAccount(address); if (parentAccount == null) { - return null; + return authorizedCodeService.processMutableAccount(this, null, address); } else { return createAccount( parentAccount, @@ -90,7 +93,7 @@ public MutableAccount getAccount(final Address address) { parentAccount.getCode()); } } else { - return null; + return authorizedCodeService.processMutableAccount(this, null, address); } } @@ -128,4 +131,9 @@ public void commit() { public Optional parentUpdater() { return Optional.empty(); } + + @Override + public void setAuthorizedCodeService(final AuthorizedCodeService authorizedCodeService) { + this.authorizedCodeService = authorizedCodeService; + } }