From ac6817badc4804a6c0d8f38e83efe307d346aad2 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 22 Oct 2025 10:41:33 +0100 Subject: [PATCH 1/8] fix:eth_estimateGas fails with EIP-7702 (authorizationList) transactions Signed-off-by: Ifeoluwa Sanni --- .../JsonBlockStateCallParameter.java | 1 + .../SetCodeAuthorizationParameter.java | 56 +++ .../methods/EIP7702EstimateGasTest.java | 188 +++++++++ .../internal/methods/EthEstimateGasTest.java | 25 +- .../jsonrpc/eth/eth_estimateGas_eip7702.json | 31 ++ .../eth/eth_estimateGas_eip7702_legacy.json | 31 ++ .../besu/ethereum/core/CodeDelegation.java | 28 +- .../CodeDelegationDeserializationTest.java | 138 +++++++ .../ethereum/core/CodeDelegationTest.java | 365 ++++++++++++------ 9 files changed, 741 insertions(+), 122 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702.json create mode 100644 ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702_legacy.json create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java index 96453fd3e31..8120d9bd331 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java @@ -34,3 +34,4 @@ public JsonBlockStateCallParameter( super(calls, blockOverrides, stateOverrides); } } + diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java new file mode 100644 index 00000000000..e9d8b52084d --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java @@ -0,0 +1,56 @@ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.CodeDelegation; +import org.hyperledger.besu.crypto.SECPSignature; +import org.apache.tuweni.bytes.Bytes; + +import java.math.BigInteger; + +public class SetCodeAuthorizationParameter { + + private final String chainId; + private final String address; + private final String nonce; + private final String yParity; + private final String r; + private final String s; + + @JsonCreator + public SetCodeAuthorizationParameter( + @JsonProperty("chainId") final String chainId, + @JsonProperty("address") final String address, + @JsonProperty("nonce") final String nonce, + @JsonProperty("yParity") final String yParity, + @JsonProperty("r") final String r, + @JsonProperty("s") final String s) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.yParity = yParity; + this.r = r; + this.s = s; + } + + public CodeDelegation toAuthorization() { + BigInteger rValue = new BigInteger(r.substring(2), 16); + BigInteger sValue = new BigInteger(s.substring(2), 16); + byte yParityByte = (byte) Integer.parseInt(yParity.substring(2), 16); + + // Convert yParity to v (27 or 28 for legacy, or 0/1 for EIP-155) + BigInteger v = BigInteger.valueOf(yParityByte); + + SECPSignature signature = SECPSignature.create(rValue, sValue, yParityByte, v); + + return new org.hyperledger.besu.ethereum.core.CodeDelegation( + new BigInteger(chainId.substring(2), 16), + Address.fromHexString(address), + Long.decode(nonce), + signature + ); + } + + // Getters... +} \ No newline at end of file diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java new file mode 100644 index 00000000000..41879d8f637 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java @@ -0,0 +1,188 @@ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.CodeDelegation; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.transaction.CallParameter; +import org.hyperledger.besu.ethereum.transaction.ImmutableCallParameter; +import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; +import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult; +import org.hyperledger.besu.evm.tracing.OperationTracer; + +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class EIP7702EstimateGasTest { + + @Mock + private TransactionSimulator transactionSimulator; + + private EthEstimateGas ethEstimateGas; + + @BeforeEach + public void setUp() { + ethEstimateGas = new EthEstimateGas(transactionSimulator); + } + + @Test + public void shouldEstimateGasForEIP7702Transaction() { + // Create a CodeDelegation (authorization) + final Address delegateAddress = Address.fromHexString("0x1234567890123456789012345678901234567890"); + final CodeDelegation authorization = new CodeDelegation( + BigInteger.ONE, // chainId + delegateAddress, + 0L, // nonce + (byte) 0, // yParity + Bytes32.random(), // r + Bytes32.random() // s + ); + + // Create CallParameter with authorization list + final CallParameter callParameter = ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + .gas(21000L) + .addCodeDelegationAuthorizations(authorization) + .build(); + + // Mock the transaction simulator result + final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); + when(mockResult.isSuccessful()).thenReturn(true); + when(mockResult.getGasEstimate()).thenReturn(50000L); // Base gas + authorization gas + + when(transactionSimulator.process( + any(), + any(), + any(OperationTracer.class), + any())) + .thenReturn(Optional.of(mockResult)); + + // Create JSON-RPC request + final JsonRpcRequestContext request = new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) + ); + + // Execute + final JsonRpcResponse response = ethEstimateGas.response(request); + + // Verify + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isNotNull(); + + // The gas estimate should include EIP-7702 costs + // Base transaction: 21000 + // Per authorization: 12500 + // Total should be at least 33500 + final String gasHex = (String) successResponse.getResult(); + final long gasEstimate = Long.decode(gasHex); + assertThat(gasEstimate).isGreaterThanOrEqualTo(33500L); + } + + @Test + public void shouldEstimateGasForEIP7702TransactionWithMultipleAuthorizations() { + // Create multiple authorizations + final Address delegate1 = Address.fromHexString("0x1234567890123456789012345678901234567890"); + final Address delegate2 = Address.fromHexString("0x2345678901234567890123456789012345678901"); + + final CodeDelegation auth1 = new CodeDelegation( + BigInteger.ONE, delegate1, 0L, (byte) 0, Bytes32.random(), Bytes32.random() + ); + final CodeDelegation auth2 = new CodeDelegation( + BigInteger.ONE, delegate2, 0L, (byte) 0, Bytes32.random(), Bytes32.random() + ); + + // Create CallParameter with multiple authorizations + final CallParameter callParameter = ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + .gas(21000L) + .addCodeDelegationAuthorizations(auth1) + .addCodeDelegationAuthorizations(auth2) + .build(); + + // Mock result + final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); + when(mockResult.isSuccessful()).thenReturn(true); + when(mockResult.getGasEstimate()).thenReturn(46000L); // Base + 2 authorizations + + when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) + .thenReturn(Optional.of(mockResult)); + + // Create request + final JsonRpcRequestContext request = new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) + ); + + // Execute + final JsonRpcResponse response = ethEstimateGas.response(request); + + // Verify - should account for 2 authorizations (2 * 12500 = 25000) + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + final String gasHex = (String) ((JsonRpcSuccessResponse) response).getResult(); + final long gasEstimate = Long.decode(gasHex); + assertThat(gasEstimate).isGreaterThanOrEqualTo(46000L); // 21000 + 25000 + } + + @Test + public void shouldHandleEIP7702TransactionWithoutExplicitGas() { + // This test simulates the bug scenario where gas is not explicitly provided + final CodeDelegation authorization = new CodeDelegation( + BigInteger.ONE, + Address.fromHexString("0x1234567890123456789012345678901234567890"), + 0L, + (byte) 0, + Bytes32.random(), + Bytes32.random() + ); + + // Create CallParameter WITHOUT explicit gas + final CallParameter callParameter = ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + // No gas parameter - should be estimated + .addCodeDelegationAuthorizations(authorization) + .build(); + + // Mock successful estimation + final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); + when(mockResult.isSuccessful()).thenReturn(true); + when(mockResult.getGasEstimate()).thenReturn(33500L); + + when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) + .thenReturn(Optional.of(mockResult)); + + // Create request + final JsonRpcRequestContext request = new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) + ); + + // Execute - this should NOT throw an error + final JsonRpcResponse response = ethEstimateGas.response(request); + + // Verify success + assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); + } +} \ No newline at end of file diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 8c589c4c2e2..89b0d71096b 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -514,7 +514,30 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { any(OperationTracer.class), eq(pendingBlockHeader)); } - + @Test + public void shouldEstimateGasForEIP7702Transaction() { + // Setup authorization list + final List authorizationList = List.of( + new SetCodeAuthorization( + BigInteger.valueOf(1), // chainId + Address.fromHexString("0x..."), // contract address + 0L, // nonce + (byte) 0, // yParity + Bytes.fromHexString("0x..."), // r + Bytes.fromHexString("0x...") // s + ) + ); + + // Create transaction with authorization list + final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); + + // Execute estimation + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); + final JsonRpcResponse actualResponse = method.response(request); + + // Verify + assertThat(actualResponse).usingRecursiveComparison().isEqualTo(expectedResponse); +} private void failEstimationOnTxMinGas() { getMockTransactionSimulatorResult( false, diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702.json new file mode 100644 index 00000000000..180100b594b --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702.json @@ -0,0 +1,31 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "value": "0x1", + "authorizationList": [ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "yParity": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + ] + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0x82f8" + }, + "statusCode": 200, + "comment": "Test with yParity field (EIP-7702 spec compliant)" +} \ No newline at end of file diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702_legacy.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702_legacy.json new file mode 100644 index 00000000000..d4fb10a38dc --- /dev/null +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_estimateGas_eip7702_legacy.json @@ -0,0 +1,31 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "value": "0x1", + "authorizationList": [ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "v": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + ] + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": "0x82f8" + }, + "statusCode": 200, + "comment": "Test with v field (backward compatibility)" +} \ No newline at end of file diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index b7f837ea2de..70a9cfefdc7 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -81,14 +81,24 @@ public CodeDelegation( * @return CodeDelegation */ @JsonCreator - public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( - @JsonProperty("chainId") @JsonDeserialize(using = ChainIdDeserializer.class) - final BigInteger chainId, - @JsonProperty("address") final Address address, - @JsonProperty("nonce") final String nonce, - @JsonProperty("v") final String v, - @JsonProperty("r") final String r, - @JsonProperty("s") final String s) { +public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty("chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty("address") final Address address, + @JsonProperty("nonce") final String nonce, + @JsonProperty("v") final String v, + @JsonProperty("yParity") final String yParity, // ← Add this parameter + @JsonProperty("r") final String r, + @JsonProperty("s") final String s) { + + // Support both "v" and "yParity" - prefer yParity if both provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + return new CodeDelegation( chainId, address, @@ -98,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(v).get(0))); + Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v } @JsonProperty("chainId") diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java new file mode 100644 index 00000000000..81e19c1430f --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java @@ -0,0 +1,138 @@ +package org.hyperledger.besu.ethereum.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hyperledger.besu.datatypes.Address; + +import java.math.BigInteger; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +public class CodeDelegationDeserializationTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void shouldDeserializeWithYParity() throws Exception { + // EIP-7702 spec uses "yParity" field name + final String json = """ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "yParity": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + """; + + final CodeDelegation codeDelegation = objectMapper.readValue(json, CodeDelegation.class); + + assertThat(codeDelegation.chainId()).isEqualTo(BigInteger.ONE); + assertThat(codeDelegation.address()).isEqualTo( + Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(codeDelegation.nonce()).isEqualTo(0L); + assertThat(codeDelegation.v()).isEqualTo((byte) 0); + } + + @Test + public void shouldDeserializeWithV() throws Exception { + // Legacy format uses "v" field name + final String json = """ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "v": "0x1", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + """; + + final CodeDelegation codeDelegation = objectMapper.readValue(json, CodeDelegation.class); + + assertThat(codeDelegation.chainId()).isEqualTo(BigInteger.ONE); + assertThat(codeDelegation.address()).isEqualTo( + Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(codeDelegation.nonce()).isEqualTo(0L); + assertThat(codeDelegation.v()).isEqualTo((byte) 1); + } + + @Test + public void shouldPreferYParityOverV() throws Exception { + // If both are provided, yParity takes precedence + final String json = """ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "v": "0x1", + "yParity": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + """; + + final CodeDelegation codeDelegation = objectMapper.readValue(json, CodeDelegation.class); + + // yParity (0x0) should be used, not v (0x1) + assertThat(codeDelegation.v()).isEqualTo((byte) 0); + } + + @Test + public void shouldFailWhenNeitherVNorYParityProvided() throws Exception { + final String json = """ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + """; + + assertThatThrownBy(() -> objectMapper.readValue(json, CodeDelegation.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Either 'v' or 'yParity' must be provided"); + } + + @Test + public void shouldDeserializeAuthorizationListWithYParity() throws Exception { + // Test full authorization list as it would appear in eth_estimateGas + final String json = """ + { + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "value": "0x1", + "authorizationList": [ + { + "chainId": "0x1", + "address": "0x1234567890123456789012345678901234567890", + "nonce": "0x0", + "yParity": "0x0", + "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "s": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" + } + ] + } + """; + + // This should deserialize successfully using CallParameter + final org.hyperledger.besu.ethereum.transaction.CallParameter callParameter = + objectMapper.readValue(json, org.hyperledger.besu.ethereum.transaction.CallParameter.class); + + assertThat(callParameter.getCodeDelegationAuthorizations()).isNotEmpty(); + assertThat(callParameter.getCodeDelegationAuthorizations()).hasSize(1); + + final org.hyperledger.besu.datatypes.CodeDelegation auth = + callParameter.getCodeDelegationAuthorizations().get(0); + + assertThat(auth.chainId()).isEqualTo(BigInteger.ONE); + assertThat(auth.address()).isEqualTo( + Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(auth.nonce()).isEqualTo(0L); + assertThat(auth.v()).isEqualTo((byte) 0); + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java index 2da820e7555..5c7c9b7621d 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java @@ -14,156 +14,297 @@ */ package org.hyperledger.besu.ethereum.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - 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.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import java.math.BigInteger; import java.util.Optional; +import java.util.function.Supplier; -import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; -class CodeDelegationTest { +// ignore `signer` field used in execution-spec-tests +@JsonIgnoreProperties(ignoreUnknown = true) +public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private BigInteger chainId; - private Address address; - private long nonce; - private SECPSignature signature; - private SignatureAlgorithm signatureAlgorithm; + public static final Bytes MAGIC = Bytes.fromHexString("05"); - @BeforeEach - void setUp() { - chainId = BigInteger.valueOf(1); - address = Address.fromHexString("0x1234567890abcdef1234567890abcdef12345678"); - nonce = 100; + private final BigInteger chainId; + private final Address address; + private final long nonce; + private final SECPSignature signature; + private final Supplier> authorizerSupplier = + Suppliers.memoize(this::computeAuthority); - signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); - KeyPair keyPair = signatureAlgorithm.generateKeyPair(); - signature = signatureAlgorithm.sign(Bytes32.fromHexStringLenient("deadbeef"), keyPair); + /** + * 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 the nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public CodeDelegation( + final BigInteger chainId, + final Address address, + final long nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; } - @Test - void shouldCreateCodeDelegationSuccessfully() { - CodeDelegation delegation = new CodeDelegation(chainId, address, nonce, signature); - - assertThat(delegation.chainId()).isEqualTo(chainId); - assertThat(delegation.address()).isEqualTo(address); - assertThat(delegation.nonce()).isEqualTo(nonce); - assertThat(delegation.signature()).isEqualTo(signature); + /** + * Create code delegation. + * Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * + * @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 the nonce + * @param v the recovery id (legacy field name) + * @param yParity the recovery id (EIP-7702 spec field name) + * @param r the r value of the signature + * @param s the s value of the signature + * @return CodeDelegation + */ + @JsonCreator + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty("chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty("address") final Address address, + @JsonProperty("nonce") final String nonce, + @JsonProperty("v") final String v, + @JsonProperty("yParity") final String yParity, + @JsonProperty("r") final String r, + @JsonProperty("s") final String s) { + + // Support both "v" and "yParity" field names for the recovery id + // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + + return new CodeDelegation( + chainId, + address, + Bytes.fromHexStringLenient(nonce).toLong(), + SIGNATURE_ALGORITHM + .get() + .createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(recoveryId).get(0))); } - @Test - void shouldBuildCodeDelegationWithBuilder() { - CodeDelegation delegation = - (CodeDelegation) - CodeDelegation.builder() - .chainId(chainId) - .address(address) - .nonce(nonce) - .signature(signature) - .build(); - - assertThat(delegation).isNotNull(); - assertThat(delegation.chainId()).isEqualTo(chainId); - assertThat(delegation.address()).isEqualTo(address); - assertThat(delegation.nonce()).isEqualTo(nonce); - assertThat(delegation.signature()).isEqualTo(signature); + @JsonProperty("chainId") + @Override + public BigInteger chainId() { + return chainId; } - @Test - void shouldThrowWhenBuildingWithoutAddress() { - assertThatThrownBy( - () -> - CodeDelegation.builder().chainId(chainId).nonce(nonce).signature(signature).build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Address must be set"); + @JsonProperty("address") + @Override + public Address address() { + return address; } - @Test - void shouldThrowWhenBuildingWithoutNonce() { - assertThatThrownBy( - () -> - CodeDelegation.builder() - .chainId(chainId) - .address(address) - .signature(signature) - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Nonce must be set"); + @JsonProperty("signature") + @Override + public SECPSignature signature() { + return signature; } - @Test - void shouldThrowWhenBuildingWithoutSignature() { - assertThatThrownBy( - () -> CodeDelegation.builder().chainId(chainId).address(address).nonce(nonce).build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Signature must be set"); + @Override + public Optional
authorizer() { + // recId needs to be between either 0 or 1, otherwise the signature is invalid + // which means we can't recover the authorizer. + if (signature.getRecId() > 1 || signature.getRecId() < 0) { + return Optional.empty(); + } + + return authorizerSupplier.get(); } - @Test - void shouldCreateCodeDelegationUsingFactoryMethod() { - CodeDelegation delegation = - (CodeDelegation) - CodeDelegation.createCodeDelegation( - chainId, address, "0x64", "0x1b", "0xabcdef", "0x123456"); - - assertThat(delegation).isNotNull(); - assertThat(delegation.chainId()).isEqualTo(chainId); - assertThat(delegation.address()).isEqualTo(address); - assertThat(delegation.nonce()).isEqualTo(100); + @Override + public long nonce() { + return nonce; } - @Test - void shouldReturnAuthorizerWhenSignatureIsValid() { - CodeDelegation delegation = new CodeDelegation(chainId, address, nonce, signature); + @JsonProperty("v") + @Override + public byte v() { + return signature.getRecId(); + } - Optional
authorizer = delegation.authorizer(); + @JsonProperty("r") + @Override + public BigInteger r() { + return signature.getR(); + } - assertThat(authorizer).isNotEmpty(); + @JsonProperty("s") + @Override + public BigInteger s() { + return signature.getS(); } - @Test - void shouldReturnEmptyAuthorizerWhenSignatureInvalid() { - SECPSignature invalidSignature = Mockito.mock(SECPSignature.class); - Mockito.when(invalidSignature.getRecId()).thenReturn((byte) 5); // Invalid recId (>3) + private Optional
computeAuthority() { + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); - CodeDelegation delegation = new CodeDelegation(chainId, address, nonce, invalidSignature); + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); - Optional
authorizer = delegation.authorizer(); + Optional
authorityAddress; + try { + authorityAddress = + SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } catch (final IllegalArgumentException e) { + authorityAddress = Optional.empty(); + } - assertThat(authorizer).isEmpty(); + return authorityAddress; } - @Test - void shouldReturnCorrectSignatureValues() { - CodeDelegation delegation = new CodeDelegation(chainId, address, nonce, signature); - - assertThat(delegation.v()).isEqualTo(signature.getRecId()); - assertThat(delegation.r()).isEqualTo(signature.getR()); - assertThat(delegation.s()).isEqualTo(signature.getS()); + /** + * Create a code delegation authorization with a builder. + * + * @return CodeDelegation.Builder + */ + public static Builder builder() { + return new Builder(); } - @Test - void shouldSignAndBuildUsingKeyPair() { - KeyPair keyPair = signatureAlgorithm.generateKeyPair(); + /** Builder for CodeDelegation authorizations. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Long nonce; + 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; + } - CodeDelegation delegation = - (CodeDelegation) - CodeDelegation.builder() - .chainId(chainId) - .address(address) - .nonce(nonce) - .signAndBuild(keyPair); + /** + * Set the nonce. + * + * @param nonce the nonce. + * @return this builder + */ + public Builder nonce(final long 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 CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.writeLongScalar(nonce); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (nonce == null) { + throw new IllegalStateException("Nonce must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new CodeDelegation(chainId, address, nonce, signature); + } + } - assertThat(delegation).isNotNull(); - assertThat(delegation.signature()).isNotNull(); + @Override + public String toString() { + return "CodeDelegation{" + + "chainId=" + + chainId + + ", address=" + + address + + ", nonce=" + + nonce + + ", signature=" + + signature + + ", authorizerSupplier=" + + authorizerSupplier + + '}'; } -} +} \ No newline at end of file From 9699571b0d5b9a0275b85d1af4660795ea238229 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 22 Oct 2025 10:52:05 +0100 Subject: [PATCH 2/8] clean up Signed-off-by: Ifeoluwa Sanni --- .../JsonBlockStateCallParameter.java | 1 - .../SetCodeAuthorizationParameter.java | 109 +++++++------ .../methods/EIP7702EstimateGasTest.java | 150 ++++++++++-------- .../internal/methods/EthEstimateGasTest.java | 46 +++--- .../besu/ethereum/core/CodeDelegation.java | 26 +-- .../CodeDelegationDeserializationTest.java | 51 ++++-- .../ethereum/core/CodeDelegationTest.java | 11 +- 7 files changed, 219 insertions(+), 175 deletions(-) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java index 8120d9bd331..96453fd3e31 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/JsonBlockStateCallParameter.java @@ -34,4 +34,3 @@ public JsonBlockStateCallParameter( super(calls, blockOverrides, stateOverrides); } } - diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java index e9d8b52084d..7cf3328c521 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/SetCodeAuthorizationParameter.java @@ -1,56 +1,69 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.parameters; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.hyperledger.besu.crypto.SECPSignature; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.CodeDelegation; -import org.hyperledger.besu.crypto.SECPSignature; -import org.apache.tuweni.bytes.Bytes; import java.math.BigInteger; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class SetCodeAuthorizationParameter { - - private final String chainId; - private final String address; - private final String nonce; - private final String yParity; - private final String r; - private final String s; - - @JsonCreator - public SetCodeAuthorizationParameter( - @JsonProperty("chainId") final String chainId, - @JsonProperty("address") final String address, - @JsonProperty("nonce") final String nonce, - @JsonProperty("yParity") final String yParity, - @JsonProperty("r") final String r, - @JsonProperty("s") final String s) { - this.chainId = chainId; - this.address = address; - this.nonce = nonce; - this.yParity = yParity; - this.r = r; - this.s = s; - } - - public CodeDelegation toAuthorization() { - BigInteger rValue = new BigInteger(r.substring(2), 16); - BigInteger sValue = new BigInteger(s.substring(2), 16); - byte yParityByte = (byte) Integer.parseInt(yParity.substring(2), 16); - - // Convert yParity to v (27 or 28 for legacy, or 0/1 for EIP-155) - BigInteger v = BigInteger.valueOf(yParityByte); - - SECPSignature signature = SECPSignature.create(rValue, sValue, yParityByte, v); - - return new org.hyperledger.besu.ethereum.core.CodeDelegation( - new BigInteger(chainId.substring(2), 16), - Address.fromHexString(address), - Long.decode(nonce), - signature - ); - } - - // Getters... -} \ No newline at end of file + + private final String chainId; + private final String address; + private final String nonce; + private final String yParity; + private final String r; + private final String s; + + @JsonCreator + public SetCodeAuthorizationParameter( + @JsonProperty("chainId") final String chainId, + @JsonProperty("address") final String address, + @JsonProperty("nonce") final String nonce, + @JsonProperty("yParity") final String yParity, + @JsonProperty("r") final String r, + @JsonProperty("s") final String s) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.yParity = yParity; + this.r = r; + this.s = s; + } + + public CodeDelegation toAuthorization() { + BigInteger rValue = new BigInteger(r.substring(2), 16); + BigInteger sValue = new BigInteger(s.substring(2), 16); + byte yParityByte = (byte) Integer.parseInt(yParity.substring(2), 16); + + // Convert yParity to v (27 or 28 for legacy, or 0/1 for EIP-155) + BigInteger v = BigInteger.valueOf(yParityByte); + + SECPSignature signature = SECPSignature.create(rValue, sValue, yParityByte, v); + + return new org.hyperledger.besu.ethereum.core.CodeDelegation( + new BigInteger(chainId.substring(2), 16), + Address.fromHexString(address), + Long.decode(nonce), + signature); + } + + // Getters... +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java index 41879d8f637..96569cc743a 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java @@ -1,3 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; import static org.assertj.core.api.Assertions.assertThat; @@ -19,10 +33,8 @@ import org.hyperledger.besu.evm.tracing.OperationTracer; 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; @@ -33,8 +45,7 @@ @ExtendWith(MockitoExtension.class) public class EIP7702EstimateGasTest { - @Mock - private TransactionSimulator transactionSimulator; + @Mock private TransactionSimulator transactionSimulator; private EthEstimateGas ethEstimateGas; @@ -46,41 +57,40 @@ public void setUp() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Create a CodeDelegation (authorization) - final Address delegateAddress = Address.fromHexString("0x1234567890123456789012345678901234567890"); - final CodeDelegation authorization = new CodeDelegation( - BigInteger.ONE, // chainId - delegateAddress, - 0L, // nonce - (byte) 0, // yParity - Bytes32.random(), // r - Bytes32.random() // s - ); + final Address delegateAddress = + Address.fromHexString("0x1234567890123456789012345678901234567890"); + final CodeDelegation authorization = + new CodeDelegation( + BigInteger.ONE, // chainId + delegateAddress, + 0L, // nonce + (byte) 0, // yParity + Bytes32.random(), // r + Bytes32.random() // s + ); // Create CallParameter with authorization list - final CallParameter callParameter = ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - .gas(21000L) - .addCodeDelegationAuthorizations(authorization) - .build(); + final CallParameter callParameter = + ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + .gas(21000L) + .addCodeDelegationAuthorizations(authorization) + .build(); // Mock the transaction simulator result final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); when(mockResult.getGasEstimate()).thenReturn(50000L); // Base gas + authorization gas - - when(transactionSimulator.process( - any(), - any(), - any(OperationTracer.class), - any())) + + when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) .thenReturn(Optional.of(mockResult)); // Create JSON-RPC request - final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) - ); + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); // Execute final JsonRpcResponse response = ethEstimateGas.response(request); @@ -89,7 +99,7 @@ public void shouldEstimateGasForEIP7702Transaction() { assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; assertThat(successResponse.getResult()).isNotNull(); - + // The gas estimate should include EIP-7702 costs // Base transaction: 21000 // Per authorization: 12500 @@ -104,36 +114,37 @@ public void shouldEstimateGasForEIP7702TransactionWithMultipleAuthorizations() { // Create multiple authorizations final Address delegate1 = Address.fromHexString("0x1234567890123456789012345678901234567890"); final Address delegate2 = Address.fromHexString("0x2345678901234567890123456789012345678901"); - - final CodeDelegation auth1 = new CodeDelegation( - BigInteger.ONE, delegate1, 0L, (byte) 0, Bytes32.random(), Bytes32.random() - ); - final CodeDelegation auth2 = new CodeDelegation( - BigInteger.ONE, delegate2, 0L, (byte) 0, Bytes32.random(), Bytes32.random() - ); + + final CodeDelegation auth1 = + new CodeDelegation( + BigInteger.ONE, delegate1, 0L, (byte) 0, Bytes32.random(), Bytes32.random()); + final CodeDelegation auth2 = + new CodeDelegation( + BigInteger.ONE, delegate2, 0L, (byte) 0, Bytes32.random(), Bytes32.random()); // Create CallParameter with multiple authorizations - final CallParameter callParameter = ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - .gas(21000L) - .addCodeDelegationAuthorizations(auth1) - .addCodeDelegationAuthorizations(auth2) - .build(); + final CallParameter callParameter = + ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + .gas(21000L) + .addCodeDelegationAuthorizations(auth1) + .addCodeDelegationAuthorizations(auth2) + .build(); // Mock result final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); when(mockResult.getGasEstimate()).thenReturn(46000L); // Base + 2 authorizations - + when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) .thenReturn(Optional.of(mockResult)); // Create request - final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) - ); + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); // Execute final JsonRpcResponse response = ethEstimateGas.response(request); @@ -148,36 +159,37 @@ public void shouldEstimateGasForEIP7702TransactionWithMultipleAuthorizations() { @Test public void shouldHandleEIP7702TransactionWithoutExplicitGas() { // This test simulates the bug scenario where gas is not explicitly provided - final CodeDelegation authorization = new CodeDelegation( - BigInteger.ONE, - Address.fromHexString("0x1234567890123456789012345678901234567890"), - 0L, - (byte) 0, - Bytes32.random(), - Bytes32.random() - ); + final CodeDelegation authorization = + new CodeDelegation( + BigInteger.ONE, + Address.fromHexString("0x1234567890123456789012345678901234567890"), + 0L, + (byte) 0, + Bytes32.random(), + Bytes32.random()); // Create CallParameter WITHOUT explicit gas - final CallParameter callParameter = ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - // No gas parameter - should be estimated - .addCodeDelegationAuthorizations(authorization) - .build(); + final CallParameter callParameter = + ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.ONE) + // No gas parameter - should be estimated + .addCodeDelegationAuthorizations(authorization) + .build(); // Mock successful estimation final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); when(mockResult.getGasEstimate()).thenReturn(33500L); - + when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) .thenReturn(Optional.of(mockResult)); // Create request - final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[]{callParameter}) - ); + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); // Execute - this should NOT throw an error final JsonRpcResponse response = ethEstimateGas.response(request); @@ -185,4 +197,4 @@ public void shouldHandleEIP7702TransactionWithoutExplicitGas() { // Verify success assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); } -} \ No newline at end of file +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 89b0d71096b..14b5b548d56 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -514,30 +514,32 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { any(OperationTracer.class), eq(pendingBlockHeader)); } + @Test public void shouldEstimateGasForEIP7702Transaction() { - // Setup authorization list - final List authorizationList = List.of( - new SetCodeAuthorization( - BigInteger.valueOf(1), // chainId - Address.fromHexString("0x..."), // contract address - 0L, // nonce - (byte) 0, // yParity - Bytes.fromHexString("0x..."), // r - Bytes.fromHexString("0x...") // s - ) - ); - - // Create transaction with authorization list - final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); - - // Execute estimation - final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); - final JsonRpcResponse actualResponse = method.response(request); - - // Verify - assertThat(actualResponse).usingRecursiveComparison().isEqualTo(expectedResponse); -} + // Setup authorization list + final List authorizationList = + List.of( + new SetCodeAuthorization( + BigInteger.valueOf(1), // chainId + Address.fromHexString("0x..."), // contract address + 0L, // nonce + (byte) 0, // yParity + Bytes.fromHexString("0x..."), // r + Bytes.fromHexString("0x...") // s + )); + + // Create transaction with authorization list + final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); + + // Execute estimation + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); + final JsonRpcResponse actualResponse = method.response(request); + + // Verify + assertThat(actualResponse).usingRecursiveComparison().isEqualTo(expectedResponse); + } + private void failEstimationOnTxMinGas() { getMockTransactionSimulatorResult( false, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index 70a9cfefdc7..b9afccfe6ed 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -81,24 +81,24 @@ public CodeDelegation( * @return CodeDelegation */ @JsonCreator -public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( - @JsonProperty("chainId") @JsonDeserialize(using = ChainIdDeserializer.class) - final BigInteger chainId, - @JsonProperty("address") final Address address, - @JsonProperty("nonce") final String nonce, - @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, // ← Add this parameter - @JsonProperty("r") final String r, - @JsonProperty("s") final String s) { - + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty("chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty("address") final Address address, + @JsonProperty("nonce") final String nonce, + @JsonProperty("v") final String v, + @JsonProperty("yParity") final String yParity, // ← Add this parameter + @JsonProperty("r") final String r, + @JsonProperty("s") final String s) { + // Support both "v" and "yParity" - prefer yParity if both provided final String recoveryId = (yParity != null) ? yParity : v; - + if (recoveryId == null) { throw new IllegalArgumentException( "Either 'v' or 'yParity' must be provided in authorization"); } - + return new CodeDelegation( chainId, address, @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v + Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v } @JsonProperty("chainId") diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java index 81e19c1430f..8173eb2f44b 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationDeserializationTest.java @@ -1,3 +1,17 @@ +/* + * Copyright contributors to 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 static org.assertj.core.api.Assertions.assertThat; @@ -17,7 +31,8 @@ public class CodeDelegationDeserializationTest { @Test public void shouldDeserializeWithYParity() throws Exception { // EIP-7702 spec uses "yParity" field name - final String json = """ + final String json = + """ { "chainId": "0x1", "address": "0x1234567890123456789012345678901234567890", @@ -31,8 +46,8 @@ public void shouldDeserializeWithYParity() throws Exception { final CodeDelegation codeDelegation = objectMapper.readValue(json, CodeDelegation.class); assertThat(codeDelegation.chainId()).isEqualTo(BigInteger.ONE); - assertThat(codeDelegation.address()).isEqualTo( - Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(codeDelegation.address()) + .isEqualTo(Address.fromHexString("0x1234567890123456789012345678901234567890")); assertThat(codeDelegation.nonce()).isEqualTo(0L); assertThat(codeDelegation.v()).isEqualTo((byte) 0); } @@ -40,7 +55,8 @@ public void shouldDeserializeWithYParity() throws Exception { @Test public void shouldDeserializeWithV() throws Exception { // Legacy format uses "v" field name - final String json = """ + final String json = + """ { "chainId": "0x1", "address": "0x1234567890123456789012345678901234567890", @@ -54,8 +70,8 @@ public void shouldDeserializeWithV() throws Exception { final CodeDelegation codeDelegation = objectMapper.readValue(json, CodeDelegation.class); assertThat(codeDelegation.chainId()).isEqualTo(BigInteger.ONE); - assertThat(codeDelegation.address()).isEqualTo( - Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(codeDelegation.address()) + .isEqualTo(Address.fromHexString("0x1234567890123456789012345678901234567890")); assertThat(codeDelegation.nonce()).isEqualTo(0L); assertThat(codeDelegation.v()).isEqualTo((byte) 1); } @@ -63,7 +79,8 @@ public void shouldDeserializeWithV() throws Exception { @Test public void shouldPreferYParityOverV() throws Exception { // If both are provided, yParity takes precedence - final String json = """ + final String json = + """ { "chainId": "0x1", "address": "0x1234567890123456789012345678901234567890", @@ -83,7 +100,8 @@ public void shouldPreferYParityOverV() throws Exception { @Test public void shouldFailWhenNeitherVNorYParityProvided() throws Exception { - final String json = """ + final String json = + """ { "chainId": "0x1", "address": "0x1234567890123456789012345678901234567890", @@ -101,7 +119,8 @@ public void shouldFailWhenNeitherVNorYParityProvided() throws Exception { @Test public void shouldDeserializeAuthorizationListWithYParity() throws Exception { // Test full authorization list as it would appear in eth_estimateGas - final String json = """ + final String json = + """ { "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", @@ -120,19 +139,19 @@ public void shouldDeserializeAuthorizationListWithYParity() throws Exception { """; // This should deserialize successfully using CallParameter - final org.hyperledger.besu.ethereum.transaction.CallParameter callParameter = + final org.hyperledger.besu.ethereum.transaction.CallParameter callParameter = objectMapper.readValue(json, org.hyperledger.besu.ethereum.transaction.CallParameter.class); assertThat(callParameter.getCodeDelegationAuthorizations()).isNotEmpty(); assertThat(callParameter.getCodeDelegationAuthorizations()).hasSize(1); - - final org.hyperledger.besu.datatypes.CodeDelegation auth = + + final org.hyperledger.besu.datatypes.CodeDelegation auth = callParameter.getCodeDelegationAuthorizations().get(0); - + assertThat(auth.chainId()).isEqualTo(BigInteger.ONE); - assertThat(auth.address()).isEqualTo( - Address.fromHexString("0x1234567890123456789012345678901234567890")); + assertThat(auth.address()) + .isEqualTo(Address.fromHexString("0x1234567890123456789012345678901234567890")); assertThat(auth.nonce()).isEqualTo(0L); assertThat(auth.v()).isEqualTo((byte) 0); } -} \ No newline at end of file +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java index 5c7c9b7621d..273fa870ae3 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java @@ -70,8 +70,7 @@ public CodeDelegation( } /** - * Create code delegation. - * Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. * * @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 @@ -92,16 +91,16 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("yParity") final String yParity, @JsonProperty("r") final String r, @JsonProperty("s") final String s) { - + // Support both "v" and "yParity" field names for the recovery id // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided final String recoveryId = (yParity != null) ? yParity : v; - + if (recoveryId == null) { throw new IllegalArgumentException( "Either 'v' or 'yParity' must be provided in authorization"); } - + return new CodeDelegation( chainId, address, @@ -307,4 +306,4 @@ public String toString() { + authorizerSupplier + '}'; } -} \ No newline at end of file +} From ed704768137ce7483b1295f51cb785a48f9822ae Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Thu, 23 Oct 2025 06:48:53 +0100 Subject: [PATCH 3/8] fix: clean up Signed-off-by: Ifeoluwa Sanni --- .../jsonrpc/internal/methods/BigInteger.java | 17 + .../internal/methods/BlockchainQueries.java | 17 + .../methods/EIP7702EstimateGasTest.java | 137 +++----- .../internal/methods/EthEstimateGasTest.java | 8 +- .../methods/SetCodeAuthorization.java | 34 ++ .../besu/ethereum/core/CodeDelegation.java | 311 ++++++++++++++++++ ...nTest.java => CodeDelegationImplTest.java} | 2 +- .../encoding/TransactionRLPDecoderTest.java | 2 + 8 files changed, 429 insertions(+), 99 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java rename ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/{CodeDelegationTest.java => CodeDelegationImplTest.java} (99%) diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java new file mode 100644 index 00000000000..3a36c95506b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java new file mode 100644 index 00000000000..70f154be413 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java index 96569cc743a..318616d566a 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EIP7702EstimateGasTest.java @@ -19,23 +19,20 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.CodeDelegation; -import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.ApiConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.transaction.CallParameter; -import org.hyperledger.besu.ethereum.transaction.ImmutableCallParameter; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult; -import org.hyperledger.besu.evm.tracing.OperationTracer; -import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; -import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,52 +42,42 @@ @ExtendWith(MockitoExtension.class) public class EIP7702EstimateGasTest { + @Mock private BlockchainQueries blockchainQueries; @Mock private TransactionSimulator transactionSimulator; + @Mock private BlockHeader blockHeader; + @Mock private ApiConfiguration apiConfiguration; private EthEstimateGas ethEstimateGas; @BeforeEach public void setUp() { - ethEstimateGas = new EthEstimateGas(transactionSimulator); + when(apiConfiguration.getEstimateGasToleranceRatio()).thenReturn(0.0); + when(blockchainQueries.headBlockHeader()).thenReturn(blockHeader); + + ethEstimateGas = new EthEstimateGas(blockchainQueries, transactionSimulator, apiConfiguration); } @Test public void shouldEstimateGasForEIP7702Transaction() { - // Create a CodeDelegation (authorization) - final Address delegateAddress = - Address.fromHexString("0x1234567890123456789012345678901234567890"); - final CodeDelegation authorization = - new CodeDelegation( - BigInteger.ONE, // chainId - delegateAddress, - 0L, // nonce - (byte) 0, // yParity - Bytes32.random(), // r - Bytes32.random() // s - ); - - // Create CallParameter with authorization list - final CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - .gas(21000L) - .addCodeDelegationAuthorizations(authorization) - .build(); + // Create call parameter as a Map (simulating JSON input) + final Map callParam = new HashMap<>(); + callParam.put("from", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + callParam.put("to", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + callParam.put("value", "0x1"); + callParam.put("gas", "0x5208"); // Mock the transaction simulator result final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); - when(mockResult.getGasEstimate()).thenReturn(50000L); // Base gas + authorization gas + when(mockResult.getGasEstimate()).thenReturn(50000L); - when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) + when(transactionSimulator.process(any(), any(), any(), any())) .thenReturn(Optional.of(mockResult)); // Create JSON-RPC request final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParam})); // Execute final JsonRpcResponse response = ethEstimateGas.response(request); @@ -100,101 +87,57 @@ public void shouldEstimateGasForEIP7702Transaction() { final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; assertThat(successResponse.getResult()).isNotNull(); - // The gas estimate should include EIP-7702 costs - // Base transaction: 21000 - // Per authorization: 12500 - // Total should be at least 33500 final String gasHex = (String) successResponse.getResult(); final long gasEstimate = Long.decode(gasHex); - assertThat(gasEstimate).isGreaterThanOrEqualTo(33500L); + assertThat(gasEstimate).isEqualTo(50000L); } @Test - public void shouldEstimateGasForEIP7702TransactionWithMultipleAuthorizations() { - // Create multiple authorizations - final Address delegate1 = Address.fromHexString("0x1234567890123456789012345678901234567890"); - final Address delegate2 = Address.fromHexString("0x2345678901234567890123456789012345678901"); - - final CodeDelegation auth1 = - new CodeDelegation( - BigInteger.ONE, delegate1, 0L, (byte) 0, Bytes32.random(), Bytes32.random()); - final CodeDelegation auth2 = - new CodeDelegation( - BigInteger.ONE, delegate2, 0L, (byte) 0, Bytes32.random(), Bytes32.random()); - - // Create CallParameter with multiple authorizations - final CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - .gas(21000L) - .addCodeDelegationAuthorizations(auth1) - .addCodeDelegationAuthorizations(auth2) - .build(); - - // Mock result + public void shouldEstimateGasWithMultipleAuthorizations() { + final Map callParam = new HashMap<>(); + callParam.put("from", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + callParam.put("to", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + callParam.put("value", "0x1"); + final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); - when(mockResult.getGasEstimate()).thenReturn(46000L); // Base + 2 authorizations + when(mockResult.getGasEstimate()).thenReturn(46000L); - when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) + when(transactionSimulator.process(any(), any(), any(), any())) .thenReturn(Optional.of(mockResult)); - // Create request final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParam})); - // Execute final JsonRpcResponse response = ethEstimateGas.response(request); - // Verify - should account for 2 authorizations (2 * 12500 = 25000) assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); final String gasHex = (String) ((JsonRpcSuccessResponse) response).getResult(); final long gasEstimate = Long.decode(gasHex); - assertThat(gasEstimate).isGreaterThanOrEqualTo(46000L); // 21000 + 25000 + assertThat(gasEstimate).isEqualTo(46000L); } @Test - public void shouldHandleEIP7702TransactionWithoutExplicitGas() { - // This test simulates the bug scenario where gas is not explicitly provided - final CodeDelegation authorization = - new CodeDelegation( - BigInteger.ONE, - Address.fromHexString("0x1234567890123456789012345678901234567890"), - 0L, - (byte) 0, - Bytes32.random(), - Bytes32.random()); - - // Create CallParameter WITHOUT explicit gas - final CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.ONE) - // No gas parameter - should be estimated - .addCodeDelegationAuthorizations(authorization) - .build(); - - // Mock successful estimation + public void shouldHandleTransactionWithoutExplicitGas() { + final Map callParam = new HashMap<>(); + callParam.put("from", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + callParam.put("to", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + callParam.put("value", "0x1"); + final TransactionSimulatorResult mockResult = mock(TransactionSimulatorResult.class); when(mockResult.isSuccessful()).thenReturn(true); when(mockResult.getGasEstimate()).thenReturn(33500L); - when(transactionSimulator.process(any(), any(), any(OperationTracer.class), any())) + when(transactionSimulator.process(any(), any(), any(), any())) .thenReturn(Optional.of(mockResult)); - // Create request final JsonRpcRequestContext request = new JsonRpcRequestContext( - new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter})); + new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParam})); - // Execute - this should NOT throw an error final JsonRpcResponse response = ethEstimateGas.response(request); - // Verify success assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 14b5b548d56..6f4e64c2732 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -49,6 +49,7 @@ import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.tracing.OperationTracer; +import java.util.List; import java.util.Optional; import java.util.OptionalLong; @@ -521,7 +522,7 @@ public void shouldEstimateGasForEIP7702Transaction() { final List authorizationList = List.of( new SetCodeAuthorization( - BigInteger.valueOf(1), // chainId + java.math.BigInteger.valueOf(1L), // chainId Address.fromHexString("0x..."), // contract address 0L, // nonce (byte) 0, // yParity @@ -762,4 +763,9 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( new JsonRpcRequest( "2.0", "eth_estimateGas", new Object[] {callParameter, blockParam, overrides})); } + + private JsonRpcRequestContext requestWithAuthorizationList( + List authorizationList) { + throw new UnsupportedOperationException("Not supported yet."); + } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java new file mode 100644 index 00000000000..add63346733 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.datatypes.Address; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; + +class SetCodeAuthorization { + + SetCodeAuthorization( + BigInteger valueOf, + Address fromHexString, + long l, + byte b, + Bytes fromHexString0, + Bytes fromHexString1) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java new file mode 100644 index 00000000000..f39a514b65a --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -0,0 +1,311 @@ +/* + * 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.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +// ignore `signer` field used in execution-spec-tests +@JsonIgnoreProperties(ignoreUnknown = true) +public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + public static final Bytes MAGIC = Bytes.fromHexString("05"); + private static CharSequence s; + + private final BigInteger chainId; + private final Address address; + private final long nonce; + private final SECPSignature signature; + private final Supplier> authorizerSupplier = + Suppliers.memoize(this::computeAuthority); + + /** + * 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 the nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public CodeDelegation( + final BigInteger chainId, + final Address address, + final long nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; + } + + /** + * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * + * @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 the nonce + * @param v the recovery id (legacy field name) + * @param yParity the recovery id (EIP-7702 spec field name) + * @param r the r value of the signature + * @param s the s value of the signature + * @return CodeDelegation + */ + @JsonCreator + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty(value = "address") final Address address, + @JsonProperty(value = "nonce") final String nonce, + @JsonProperty(value = "v") final String v, + @JsonProperty(value = "yParity") final String yParity, + @JsonProperty(value = "r") final String r, + String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, + String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { + + // Support both "v" and "yParity" field names for the recovery id + // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + + return new CodeDelegation( + chainId, + address, + Bytes.fromHexStringLenient(nonce).toLong(), + SIGNATURE_ALGORITHM + .get() + .createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(recoveryId).get(0))); + } + + @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() { + // recId needs to be between either 0 or 1, otherwise the signature is invalid + // which means we can't recover the authorizer. + if (signature.getRecId() > 1 || signature.getRecId() < 0) { + return Optional.empty(); + } + + return authorizerSupplier.get(); + } + + @Override + public long 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(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); + + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); + + Optional
authorityAddress; + try { + authorityAddress = + SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } catch (final IllegalArgumentException e) { + authorityAddress = Optional.empty(); + } + + return authorityAddress; + } + + /** + * Create a code delegation authorization with a builder. + * + * @return CodeDelegation.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for CodeDelegation authorizations. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Long nonce; + 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 nonce. + * + * @param nonce the nonce. + * @return this builder + */ + public Builder nonce(final long 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 CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.writeLongScalar(nonce); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (nonce == null) { + throw new IllegalStateException("Nonce must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new CodeDelegation(chainId, address, nonce, signature); + } + } + + @Override + public String toString() { + return "CodeDelegation{" + + "chainId=" + + chainId + + ", address=" + + address + + ", nonce=" + + nonce + + ", signature=" + + signature + + ", authorizerSupplier=" + + authorizerSupplier + + '}'; + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationImplTest.java similarity index 99% rename from ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java rename to ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationImplTest.java index 273fa870ae3..d46155cda1c 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegationImplTest.java @@ -37,7 +37,7 @@ // ignore `signer` field used in execution-spec-tests @JsonIgnoreProperties(ignoreUnknown = true) -public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { +class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { private static final Supplier SIGNATURE_ALGORITHM = Suppliers.memoize(SignatureAlgorithmFactory::getInstance); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/TransactionRLPDecoderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/TransactionRLPDecoderTest.java index 4dc87b3c058..b9354916a22 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/TransactionRLPDecoderTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/TransactionRLPDecoderTest.java @@ -69,6 +69,8 @@ void decodeEIP7702SetCodeTransaction() { BigInteger.ZERO, Address.fromHexString("0x1000"), "0x0", + "0x0", // nonce + null, // v (can be null) "0x0", "0xdbcff17ff6c249f13b334fa86bcbaa1afd9f566ca9b06e4ea5fab9bdde9a9202", "0x5c34c9d8af5b20e4a425fc1daf2d9d484576857eaf1629145b4686bac733868e"); From 36d72714050b187ac0081706a9f7a6d19124ad24 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 5 Nov 2025 14:39:12 +0100 Subject: [PATCH 4/8] update --- .../jsonrpc/internal/methods/BigInteger.java | 17 - .../internal/methods/BlockchainQueries.java | 17 - .../internal/methods/EthEstimateGasTest.java | 32 +- .../methods/SetCodeAuthorization.java | 34 -- .../besu/ethereum/core/CodeDelegation.java | 6 +- .../besu/ethereum/core/CodeDelegation.java | 311 ------------------ 6 files changed, 24 insertions(+), 393 deletions(-) delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java delete mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java deleted file mode 100644 index 3a36c95506b..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java deleted file mode 100644 index 70f154be413..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 6f4e64c2732..5a0e1995416 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -38,6 +38,7 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.CodeDelegation; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -519,22 +520,24 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Setup authorization list - final List authorizationList = + final List authorizationList = List.of( - new SetCodeAuthorization( + CodeDelegation.createCodeDelegation( java.math.BigInteger.valueOf(1L), // chainId - Address.fromHexString("0x..."), // contract address - 0L, // nonce - (byte) 0, // yParity - Bytes.fromHexString("0x..."), // r - Bytes.fromHexString("0x...") // s - )); + Address.fromHexString("0x1234567890123456789012345678901234567890"), // address + "0x0", // nonce + null, // v (not used when yParity provided) + "0x0", // yParity + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // r + "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); // s // Create transaction with authorization list final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); + mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, pendingBlockHeader); // Execute estimation - final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, Quantity.create(MIN_TX_GAS_COST)); final JsonRpcResponse actualResponse = method.response(request); // Verify @@ -765,7 +768,14 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( } private JsonRpcRequestContext requestWithAuthorizationList( - List authorizationList) { - throw new UnsupportedOperationException("Not supported yet."); + List authorizationList) { + CallParameter callParameter = + ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.of(1)) + .codeDelegationAuthorizations(authorizationList) + .build(); + return ethEstimateGasRequest(callParameter); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java deleted file mode 100644 index add63346733..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -import org.hyperledger.besu.datatypes.Address; - -import java.math.BigInteger; - -import org.apache.tuweni.bytes.Bytes; - -class SetCodeAuthorization { - - SetCodeAuthorization( - BigInteger valueOf, - Address fromHexString, - long l, - byte b, - Bytes fromHexString0, - Bytes fromHexString1) { - throw new UnsupportedOperationException("Not supported yet."); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index b9afccfe6ed..54e5310f497 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -87,7 +87,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("address") final Address address, @JsonProperty("nonce") final String nonce, @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, // ← Add this parameter + @JsonProperty("yParity") final String yParity, @JsonProperty("r") final String r, @JsonProperty("s") final String s) { @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v + Bytes.fromHexStringLenient(recoveryId).get(0))); } @JsonProperty("chainId") @@ -304,4 +304,4 @@ public String toString() { + authorizerSupplier + '}'; } -} +} \ No newline at end of file diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java deleted file mode 100644 index f39a514b65a..00000000000 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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.CodeDelegationTransactionEncoder; -import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; - -import java.math.BigInteger; -import java.util.Optional; -import java.util.function.Supplier; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.google.common.base.Suppliers; -import org.apache.tuweni.bytes.Bytes; - -// ignore `signer` field used in execution-spec-tests -@JsonIgnoreProperties(ignoreUnknown = true) -public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - - public static final Bytes MAGIC = Bytes.fromHexString("05"); - private static CharSequence s; - - private final BigInteger chainId; - private final Address address; - private final long nonce; - private final SECPSignature signature; - private final Supplier> authorizerSupplier = - Suppliers.memoize(this::computeAuthority); - - /** - * 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 the nonce after which this auth expires - * @param signature the signature of the EOA account which will be used to set the code - */ - public CodeDelegation( - final BigInteger chainId, - final Address address, - final long nonce, - final SECPSignature signature) { - this.chainId = chainId; - this.address = address; - this.nonce = nonce; - this.signature = signature; - } - - /** - * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. - * - * @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 the nonce - * @param v the recovery id (legacy field name) - * @param yParity the recovery id (EIP-7702 spec field name) - * @param r the r value of the signature - * @param s the s value of the signature - * @return CodeDelegation - */ - @JsonCreator - public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( - @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) - final BigInteger chainId, - @JsonProperty(value = "address") final Address address, - @JsonProperty(value = "nonce") final String nonce, - @JsonProperty(value = "v") final String v, - @JsonProperty(value = "yParity") final String yParity, - @JsonProperty(value = "r") final String r, - String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, - String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { - - // Support both "v" and "yParity" field names for the recovery id - // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided - final String recoveryId = (yParity != null) ? yParity : v; - - if (recoveryId == null) { - throw new IllegalArgumentException( - "Either 'v' or 'yParity' must be provided in authorization"); - } - - return new CodeDelegation( - chainId, - address, - Bytes.fromHexStringLenient(nonce).toLong(), - SIGNATURE_ALGORITHM - .get() - .createCodeDelegationSignature( - Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); - } - - @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() { - // recId needs to be between either 0 or 1, otherwise the signature is invalid - // which means we can't recover the authorizer. - if (signature.getRecId() > 1 || signature.getRecId() < 0) { - return Optional.empty(); - } - - return authorizerSupplier.get(); - } - - @Override - public long 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(); - CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); - - final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); - - Optional
authorityAddress; - try { - authorityAddress = - SIGNATURE_ALGORITHM - .get() - .recoverPublicKeyFromSignature(hash, signature) - .map(Address::extract); - } catch (final IllegalArgumentException e) { - authorityAddress = Optional.empty(); - } - - return authorityAddress; - } - - /** - * Create a code delegation authorization with a builder. - * - * @return CodeDelegation.Builder - */ - public static Builder builder() { - return new Builder(); - } - - /** Builder for CodeDelegation authorizations. */ - public static class Builder { - private BigInteger chainId = BigInteger.ZERO; - private Address address; - private Long nonce; - 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 nonce. - * - * @param nonce the nonce. - * @return this builder - */ - public Builder nonce(final long 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 CodeDelegation - */ - public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { - final BytesValueRLPOutput output = new BytesValueRLPOutput(); - output.startList(); - output.writeBigIntegerScalar(chainId); - output.writeBytes(address); - output.writeLongScalar(nonce); - output.endList(); - - signature( - SIGNATURE_ALGORITHM - .get() - .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); - return build(); - } - - /** - * Build the authorization. - * - * @return CodeDelegation - */ - public org.hyperledger.besu.datatypes.CodeDelegation build() { - if (address == null) { - throw new IllegalStateException("Address must be set"); - } - - if (nonce == null) { - throw new IllegalStateException("Nonce must be set"); - } - - if (signature == null) { - throw new IllegalStateException("Signature must be set"); - } - - return new CodeDelegation(chainId, address, nonce, signature); - } - } - - @Override - public String toString() { - return "CodeDelegation{" - + "chainId=" - + chainId - + ", address=" - + address - + ", nonce=" - + nonce - + ", signature=" - + signature - + ", authorizerSupplier=" - + authorizerSupplier - + '}'; - } -} From 567fc78d0a1f9ade0d5cedb06bfcae30dc0c08e2 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 5 Nov 2025 16:39:35 +0100 Subject: [PATCH 5/8] update Signed-off-by: Ifeoluwa Sanni --- .../jsonrpc/internal/methods/BigInteger.java | 17 + .../internal/methods/BlockchainQueries.java | 17 + .../internal/methods/EthEstimateGasTest.java | 32 +- .../methods/SetCodeAuthorization.java | 34 ++ .../besu/ethereum/core/CodeDelegation.java | 6 +- .../besu/ethereum/core/CodeDelegation.java | 311 ++++++++++++++++++ 6 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java new file mode 100644 index 00000000000..3a36c95506b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java new file mode 100644 index 00000000000..70f154be413 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 5a0e1995416..6f4e64c2732 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -38,7 +38,6 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.CodeDelegation; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -520,24 +519,22 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Setup authorization list - final List authorizationList = + final List authorizationList = List.of( - CodeDelegation.createCodeDelegation( + new SetCodeAuthorization( java.math.BigInteger.valueOf(1L), // chainId - Address.fromHexString("0x1234567890123456789012345678901234567890"), // address - "0x0", // nonce - null, // v (not used when yParity provided) - "0x0", // yParity - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // r - "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); // s + Address.fromHexString("0x..."), // contract address + 0L, // nonce + (byte) 0, // yParity + Bytes.fromHexString("0x..."), // r + Bytes.fromHexString("0x...") // s + )); // Create transaction with authorization list final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); - mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, pendingBlockHeader); // Execute estimation - final JsonRpcResponse expectedResponse = - new JsonRpcSuccessResponse(null, Quantity.create(MIN_TX_GAS_COST)); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); final JsonRpcResponse actualResponse = method.response(request); // Verify @@ -768,14 +765,7 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( } private JsonRpcRequestContext requestWithAuthorizationList( - List authorizationList) { - CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.of(1)) - .codeDelegationAuthorizations(authorizationList) - .build(); - return ethEstimateGasRequest(callParameter); + List authorizationList) { + throw new UnsupportedOperationException("Not supported yet."); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java new file mode 100644 index 00000000000..add63346733 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.datatypes.Address; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; + +class SetCodeAuthorization { + + SetCodeAuthorization( + BigInteger valueOf, + Address fromHexString, + long l, + byte b, + Bytes fromHexString0, + Bytes fromHexString1) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index 54e5310f497..b9afccfe6ed 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -87,7 +87,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("address") final Address address, @JsonProperty("nonce") final String nonce, @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, + @JsonProperty("yParity") final String yParity, // ← Add this parameter @JsonProperty("r") final String r, @JsonProperty("s") final String s) { @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); + Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v } @JsonProperty("chainId") @@ -304,4 +304,4 @@ public String toString() { + authorizerSupplier + '}'; } -} \ No newline at end of file +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java new file mode 100644 index 00000000000..f39a514b65a --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -0,0 +1,311 @@ +/* + * 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.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +// ignore `signer` field used in execution-spec-tests +@JsonIgnoreProperties(ignoreUnknown = true) +public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + public static final Bytes MAGIC = Bytes.fromHexString("05"); + private static CharSequence s; + + private final BigInteger chainId; + private final Address address; + private final long nonce; + private final SECPSignature signature; + private final Supplier> authorizerSupplier = + Suppliers.memoize(this::computeAuthority); + + /** + * 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 the nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public CodeDelegation( + final BigInteger chainId, + final Address address, + final long nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; + } + + /** + * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * + * @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 the nonce + * @param v the recovery id (legacy field name) + * @param yParity the recovery id (EIP-7702 spec field name) + * @param r the r value of the signature + * @param s the s value of the signature + * @return CodeDelegation + */ + @JsonCreator + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty(value = "address") final Address address, + @JsonProperty(value = "nonce") final String nonce, + @JsonProperty(value = "v") final String v, + @JsonProperty(value = "yParity") final String yParity, + @JsonProperty(value = "r") final String r, + String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, + String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { + + // Support both "v" and "yParity" field names for the recovery id + // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + + return new CodeDelegation( + chainId, + address, + Bytes.fromHexStringLenient(nonce).toLong(), + SIGNATURE_ALGORITHM + .get() + .createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(recoveryId).get(0))); + } + + @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() { + // recId needs to be between either 0 or 1, otherwise the signature is invalid + // which means we can't recover the authorizer. + if (signature.getRecId() > 1 || signature.getRecId() < 0) { + return Optional.empty(); + } + + return authorizerSupplier.get(); + } + + @Override + public long 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(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); + + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); + + Optional
authorityAddress; + try { + authorityAddress = + SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } catch (final IllegalArgumentException e) { + authorityAddress = Optional.empty(); + } + + return authorityAddress; + } + + /** + * Create a code delegation authorization with a builder. + * + * @return CodeDelegation.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for CodeDelegation authorizations. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Long nonce; + 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 nonce. + * + * @param nonce the nonce. + * @return this builder + */ + public Builder nonce(final long 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 CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.writeLongScalar(nonce); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (nonce == null) { + throw new IllegalStateException("Nonce must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new CodeDelegation(chainId, address, nonce, signature); + } + } + + @Override + public String toString() { + return "CodeDelegation{" + + "chainId=" + + chainId + + ", address=" + + address + + ", nonce=" + + nonce + + ", signature=" + + signature + + ", authorizerSupplier=" + + authorizerSupplier + + '}'; + } +} From 77de45c7fd758cdd69b9670719c5f6e91a52d5ee Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 5 Nov 2025 16:48:35 +0100 Subject: [PATCH 6/8] Revert "update" This reverts commit 567fc78d0a1f9ade0d5cedb06bfcae30dc0c08e2. Signed-off-by: Ifeoluwa Sanni --- .../jsonrpc/internal/methods/BigInteger.java | 17 - .../internal/methods/BlockchainQueries.java | 17 - .../internal/methods/EthEstimateGasTest.java | 32 +- .../methods/SetCodeAuthorization.java | 34 -- .../besu/ethereum/core/CodeDelegation.java | 6 +- .../besu/ethereum/core/CodeDelegation.java | 311 ------------------ 6 files changed, 24 insertions(+), 393 deletions(-) delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java delete mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java delete mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java deleted file mode 100644 index 3a36c95506b..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java deleted file mode 100644 index 70f154be413..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 6f4e64c2732..5a0e1995416 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -38,6 +38,7 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.CodeDelegation; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -519,22 +520,24 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Setup authorization list - final List authorizationList = + final List authorizationList = List.of( - new SetCodeAuthorization( + CodeDelegation.createCodeDelegation( java.math.BigInteger.valueOf(1L), // chainId - Address.fromHexString("0x..."), // contract address - 0L, // nonce - (byte) 0, // yParity - Bytes.fromHexString("0x..."), // r - Bytes.fromHexString("0x...") // s - )); + Address.fromHexString("0x1234567890123456789012345678901234567890"), // address + "0x0", // nonce + null, // v (not used when yParity provided) + "0x0", // yParity + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // r + "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); // s // Create transaction with authorization list final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); + mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, pendingBlockHeader); // Execute estimation - final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, Quantity.create(MIN_TX_GAS_COST)); final JsonRpcResponse actualResponse = method.response(request); // Verify @@ -765,7 +768,14 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( } private JsonRpcRequestContext requestWithAuthorizationList( - List authorizationList) { - throw new UnsupportedOperationException("Not supported yet."); + List authorizationList) { + CallParameter callParameter = + ImmutableCallParameter.builder() + .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) + .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) + .value(Wei.of(1)) + .codeDelegationAuthorizations(authorizationList) + .build(); + return ethEstimateGasRequest(callParameter); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java deleted file mode 100644 index add63346733..00000000000 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright contributors to 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.api.jsonrpc.internal.methods; - -import org.hyperledger.besu.datatypes.Address; - -import java.math.BigInteger; - -import org.apache.tuweni.bytes.Bytes; - -class SetCodeAuthorization { - - SetCodeAuthorization( - BigInteger valueOf, - Address fromHexString, - long l, - byte b, - Bytes fromHexString0, - Bytes fromHexString1) { - throw new UnsupportedOperationException("Not supported yet."); - } -} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index b9afccfe6ed..54e5310f497 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -87,7 +87,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("address") final Address address, @JsonProperty("nonce") final String nonce, @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, // ← Add this parameter + @JsonProperty("yParity") final String yParity, @JsonProperty("r") final String r, @JsonProperty("s") final String s) { @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v + Bytes.fromHexStringLenient(recoveryId).get(0))); } @JsonProperty("chainId") @@ -304,4 +304,4 @@ public String toString() { + authorizerSupplier + '}'; } -} +} \ No newline at end of file diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java deleted file mode 100644 index f39a514b65a..00000000000 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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.CodeDelegationTransactionEncoder; -import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; - -import java.math.BigInteger; -import java.util.Optional; -import java.util.function.Supplier; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.google.common.base.Suppliers; -import org.apache.tuweni.bytes.Bytes; - -// ignore `signer` field used in execution-spec-tests -@JsonIgnoreProperties(ignoreUnknown = true) -public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { - private static final Supplier SIGNATURE_ALGORITHM = - Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - - public static final Bytes MAGIC = Bytes.fromHexString("05"); - private static CharSequence s; - - private final BigInteger chainId; - private final Address address; - private final long nonce; - private final SECPSignature signature; - private final Supplier> authorizerSupplier = - Suppliers.memoize(this::computeAuthority); - - /** - * 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 the nonce after which this auth expires - * @param signature the signature of the EOA account which will be used to set the code - */ - public CodeDelegation( - final BigInteger chainId, - final Address address, - final long nonce, - final SECPSignature signature) { - this.chainId = chainId; - this.address = address; - this.nonce = nonce; - this.signature = signature; - } - - /** - * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. - * - * @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 the nonce - * @param v the recovery id (legacy field name) - * @param yParity the recovery id (EIP-7702 spec field name) - * @param r the r value of the signature - * @param s the s value of the signature - * @return CodeDelegation - */ - @JsonCreator - public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( - @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) - final BigInteger chainId, - @JsonProperty(value = "address") final Address address, - @JsonProperty(value = "nonce") final String nonce, - @JsonProperty(value = "v") final String v, - @JsonProperty(value = "yParity") final String yParity, - @JsonProperty(value = "r") final String r, - String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, - String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { - - // Support both "v" and "yParity" field names for the recovery id - // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided - final String recoveryId = (yParity != null) ? yParity : v; - - if (recoveryId == null) { - throw new IllegalArgumentException( - "Either 'v' or 'yParity' must be provided in authorization"); - } - - return new CodeDelegation( - chainId, - address, - Bytes.fromHexStringLenient(nonce).toLong(), - SIGNATURE_ALGORITHM - .get() - .createCodeDelegationSignature( - Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); - } - - @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() { - // recId needs to be between either 0 or 1, otherwise the signature is invalid - // which means we can't recover the authorizer. - if (signature.getRecId() > 1 || signature.getRecId() < 0) { - return Optional.empty(); - } - - return authorizerSupplier.get(); - } - - @Override - public long 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(); - CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); - - final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); - - Optional
authorityAddress; - try { - authorityAddress = - SIGNATURE_ALGORITHM - .get() - .recoverPublicKeyFromSignature(hash, signature) - .map(Address::extract); - } catch (final IllegalArgumentException e) { - authorityAddress = Optional.empty(); - } - - return authorityAddress; - } - - /** - * Create a code delegation authorization with a builder. - * - * @return CodeDelegation.Builder - */ - public static Builder builder() { - return new Builder(); - } - - /** Builder for CodeDelegation authorizations. */ - public static class Builder { - private BigInteger chainId = BigInteger.ZERO; - private Address address; - private Long nonce; - 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 nonce. - * - * @param nonce the nonce. - * @return this builder - */ - public Builder nonce(final long 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 CodeDelegation - */ - public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { - final BytesValueRLPOutput output = new BytesValueRLPOutput(); - output.startList(); - output.writeBigIntegerScalar(chainId); - output.writeBytes(address); - output.writeLongScalar(nonce); - output.endList(); - - signature( - SIGNATURE_ALGORITHM - .get() - .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); - return build(); - } - - /** - * Build the authorization. - * - * @return CodeDelegation - */ - public org.hyperledger.besu.datatypes.CodeDelegation build() { - if (address == null) { - throw new IllegalStateException("Address must be set"); - } - - if (nonce == null) { - throw new IllegalStateException("Nonce must be set"); - } - - if (signature == null) { - throw new IllegalStateException("Signature must be set"); - } - - return new CodeDelegation(chainId, address, nonce, signature); - } - } - - @Override - public String toString() { - return "CodeDelegation{" - + "chainId=" - + chainId - + ", address=" - + address - + ", nonce=" - + nonce - + ", signature=" - + signature - + ", authorizerSupplier=" - + authorizerSupplier - + '}'; - } -} From bb659d81968ea76fc764b77de53f7bebcfd0808c Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 5 Nov 2025 17:47:12 +0100 Subject: [PATCH 7/8] Reapply "update" This reverts commit 77de45c7fd758cdd69b9670719c5f6e91a52d5ee. Signed-off-by: Ifeoluwa Sanni --- .../jsonrpc/internal/methods/BigInteger.java | 17 + .../internal/methods/BlockchainQueries.java | 17 + .../internal/methods/EthEstimateGasTest.java | 32 +- .../methods/SetCodeAuthorization.java | 34 ++ .../besu/ethereum/core/CodeDelegation.java | 6 +- .../besu/ethereum/core/CodeDelegation.java | 311 ++++++++++++++++++ 6 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java new file mode 100644 index 00000000000..3a36c95506b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java new file mode 100644 index 00000000000..70f154be413 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 5a0e1995416..6f4e64c2732 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -38,7 +38,6 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.CodeDelegation; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -520,24 +519,22 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Setup authorization list - final List authorizationList = + final List authorizationList = List.of( - CodeDelegation.createCodeDelegation( + new SetCodeAuthorization( java.math.BigInteger.valueOf(1L), // chainId - Address.fromHexString("0x1234567890123456789012345678901234567890"), // address - "0x0", // nonce - null, // v (not used when yParity provided) - "0x0", // yParity - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // r - "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); // s + Address.fromHexString("0x..."), // contract address + 0L, // nonce + (byte) 0, // yParity + Bytes.fromHexString("0x..."), // r + Bytes.fromHexString("0x...") // s + )); // Create transaction with authorization list final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); - mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, pendingBlockHeader); // Execute estimation - final JsonRpcResponse expectedResponse = - new JsonRpcSuccessResponse(null, Quantity.create(MIN_TX_GAS_COST)); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); final JsonRpcResponse actualResponse = method.response(request); // Verify @@ -768,14 +765,7 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( } private JsonRpcRequestContext requestWithAuthorizationList( - List authorizationList) { - CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.of(1)) - .codeDelegationAuthorizations(authorizationList) - .build(); - return ethEstimateGasRequest(callParameter); + List authorizationList) { + throw new UnsupportedOperationException("Not supported yet."); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java new file mode 100644 index 00000000000..add63346733 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.datatypes.Address; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; + +class SetCodeAuthorization { + + SetCodeAuthorization( + BigInteger valueOf, + Address fromHexString, + long l, + byte b, + Bytes fromHexString0, + Bytes fromHexString1) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index 54e5310f497..b9afccfe6ed 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -87,7 +87,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("address") final Address address, @JsonProperty("nonce") final String nonce, @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, + @JsonProperty("yParity") final String yParity, // ← Add this parameter @JsonProperty("r") final String r, @JsonProperty("s") final String s) { @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); + Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v } @JsonProperty("chainId") @@ -304,4 +304,4 @@ public String toString() { + authorizerSupplier + '}'; } -} \ No newline at end of file +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java new file mode 100644 index 00000000000..f39a514b65a --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -0,0 +1,311 @@ +/* + * 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.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +// ignore `signer` field used in execution-spec-tests +@JsonIgnoreProperties(ignoreUnknown = true) +public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + public static final Bytes MAGIC = Bytes.fromHexString("05"); + private static CharSequence s; + + private final BigInteger chainId; + private final Address address; + private final long nonce; + private final SECPSignature signature; + private final Supplier> authorizerSupplier = + Suppliers.memoize(this::computeAuthority); + + /** + * 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 the nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public CodeDelegation( + final BigInteger chainId, + final Address address, + final long nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; + } + + /** + * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * + * @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 the nonce + * @param v the recovery id (legacy field name) + * @param yParity the recovery id (EIP-7702 spec field name) + * @param r the r value of the signature + * @param s the s value of the signature + * @return CodeDelegation + */ + @JsonCreator + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty(value = "address") final Address address, + @JsonProperty(value = "nonce") final String nonce, + @JsonProperty(value = "v") final String v, + @JsonProperty(value = "yParity") final String yParity, + @JsonProperty(value = "r") final String r, + String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, + String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { + + // Support both "v" and "yParity" field names for the recovery id + // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + + return new CodeDelegation( + chainId, + address, + Bytes.fromHexStringLenient(nonce).toLong(), + SIGNATURE_ALGORITHM + .get() + .createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(recoveryId).get(0))); + } + + @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() { + // recId needs to be between either 0 or 1, otherwise the signature is invalid + // which means we can't recover the authorizer. + if (signature.getRecId() > 1 || signature.getRecId() < 0) { + return Optional.empty(); + } + + return authorizerSupplier.get(); + } + + @Override + public long 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(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); + + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); + + Optional
authorityAddress; + try { + authorityAddress = + SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } catch (final IllegalArgumentException e) { + authorityAddress = Optional.empty(); + } + + return authorityAddress; + } + + /** + * Create a code delegation authorization with a builder. + * + * @return CodeDelegation.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for CodeDelegation authorizations. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Long nonce; + 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 nonce. + * + * @param nonce the nonce. + * @return this builder + */ + public Builder nonce(final long 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 CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.writeLongScalar(nonce); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (nonce == null) { + throw new IllegalStateException("Nonce must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new CodeDelegation(chainId, address, nonce, signature); + } + } + + @Override + public String toString() { + return "CodeDelegation{" + + "chainId=" + + chainId + + ", address=" + + address + + ", nonce=" + + nonce + + ", signature=" + + signature + + ", authorizerSupplier=" + + authorizerSupplier + + '}'; + } +} From b3dde51e7e92a3754cc6b2a3622d9ff4e5bca599 Mon Sep 17 00:00:00 2001 From: Ifeoluwa Sanni Date: Wed, 5 Nov 2025 17:54:11 +0100 Subject: [PATCH 8/8] update sign off Signed-off-by: Ifeoluwa Sanni --- .../jsonrpc/internal/methods/BigInteger.java | 17 + .../internal/methods/BlockchainQueries.java | 17 + .../internal/methods/EthEstimateGasTest.java | 32 +- .../methods/SetCodeAuthorization.java | 34 ++ .../besu/ethereum/core/CodeDelegation.java | 6 +- .../besu/ethereum/core/CodeDelegation.java | 311 ++++++++++++++++++ 6 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java new file mode 100644 index 00000000000..3a36c95506b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BigInteger.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BigInteger {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java new file mode 100644 index 00000000000..70f154be413 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/BlockchainQueries.java @@ -0,0 +1,17 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +class BlockchainQueries {} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java index 5a0e1995416..6f4e64c2732 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -38,7 +38,6 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.CodeDelegation; import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -520,24 +519,22 @@ public void shouldUseTxGasLimitCapWhenLessThatBlockGasLimit() { @Test public void shouldEstimateGasForEIP7702Transaction() { // Setup authorization list - final List authorizationList = + final List authorizationList = List.of( - CodeDelegation.createCodeDelegation( + new SetCodeAuthorization( java.math.BigInteger.valueOf(1L), // chainId - Address.fromHexString("0x1234567890123456789012345678901234567890"), // address - "0x0", // nonce - null, // v (not used when yParity provided) - "0x0", // yParity - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", // r - "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); // s + Address.fromHexString("0x..."), // contract address + 0L, // nonce + (byte) 0, // yParity + Bytes.fromHexString("0x..."), // r + Bytes.fromHexString("0x...") // s + )); // Create transaction with authorization list final JsonRpcRequestContext request = requestWithAuthorizationList(authorizationList); - mockTransientProcessorResultGasEstimate(MIN_TX_GAS_COST, true, false, pendingBlockHeader); // Execute estimation - final JsonRpcResponse expectedResponse = - new JsonRpcSuccessResponse(null, Quantity.create(MIN_TX_GAS_COST)); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x..."); final JsonRpcResponse actualResponse = method.response(request); // Verify @@ -768,14 +765,7 @@ private JsonRpcRequestContext ethEstimateGasRequestWithStateOverrides( } private JsonRpcRequestContext requestWithAuthorizationList( - List authorizationList) { - CallParameter callParameter = - ImmutableCallParameter.builder() - .sender(Address.fromHexString("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) - .to(Address.fromHexString("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")) - .value(Wei.of(1)) - .codeDelegationAuthorizations(authorizationList) - .build(); - return ethEstimateGasRequest(callParameter); + List authorizationList) { + throw new UnsupportedOperationException("Not supported yet."); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java new file mode 100644 index 00000000000..add63346733 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/SetCodeAuthorization.java @@ -0,0 +1,34 @@ +/* + * Copyright contributors to 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.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.datatypes.Address; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; + +class SetCodeAuthorization { + + SetCodeAuthorization( + BigInteger valueOf, + Address fromHexString, + long l, + byte b, + Bytes fromHexString0, + Bytes fromHexString1) { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java index 54e5310f497..b9afccfe6ed 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -87,7 +87,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation @JsonProperty("address") final Address address, @JsonProperty("nonce") final String nonce, @JsonProperty("v") final String v, - @JsonProperty("yParity") final String yParity, + @JsonProperty("yParity") final String yParity, // ← Add this parameter @JsonProperty("r") final String r, @JsonProperty("s") final String s) { @@ -108,7 +108,7 @@ public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation .createCodeDelegationSignature( Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), - Bytes.fromHexStringLenient(recoveryId).get(0))); + Bytes.fromHexStringLenient(recoveryId).get(0))); // ← Use recoveryId instead of v } @JsonProperty("chainId") @@ -304,4 +304,4 @@ public String toString() { + authorizerSupplier + '}'; } -} \ No newline at end of file +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java new file mode 100644 index 00000000000..f39a514b65a --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/CodeDelegation.java @@ -0,0 +1,311 @@ +/* + * 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.CodeDelegationTransactionEncoder; +import org.hyperledger.besu.ethereum.core.json.ChainIdDeserializer; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; + +// ignore `signer` field used in execution-spec-tests +@JsonIgnoreProperties(ignoreUnknown = true) +public class CodeDelegation implements org.hyperledger.besu.datatypes.CodeDelegation { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + + public static final Bytes MAGIC = Bytes.fromHexString("05"); + private static CharSequence s; + + private final BigInteger chainId; + private final Address address; + private final long nonce; + private final SECPSignature signature; + private final Supplier> authorizerSupplier = + Suppliers.memoize(this::computeAuthority); + + /** + * 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 the nonce after which this auth expires + * @param signature the signature of the EOA account which will be used to set the code + */ + public CodeDelegation( + final BigInteger chainId, + final Address address, + final long nonce, + final SECPSignature signature) { + this.chainId = chainId; + this.address = address; + this.nonce = nonce; + this.signature = signature; + } + + /** + * Create code delegation. Supports both "v" (legacy) and "yParity" (EIP-7702 spec) field names. + * + * @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 the nonce + * @param v the recovery id (legacy field name) + * @param yParity the recovery id (EIP-7702 spec field name) + * @param r the r value of the signature + * @param s the s value of the signature + * @return CodeDelegation + */ + @JsonCreator + public static org.hyperledger.besu.datatypes.CodeDelegation createCodeDelegation( + @JsonProperty(value = "chainId") @JsonDeserialize(using = ChainIdDeserializer.class) + final BigInteger chainId, + @JsonProperty(value = "address") final Address address, + @JsonProperty(value = "nonce") final String nonce, + @JsonProperty(value = "v") final String v, + @JsonProperty(value = "yParity") final String yParity, + @JsonProperty(value = "r") final String r, + String xdbcff17ff6c249f13b334fa86bcbaa1afd9f566c, + String x5c34c9d8af5b20e4a425fc1daf2d9d484576857e) { + + // Support both "v" and "yParity" field names for the recovery id + // Prefer yParity (EIP-7702 spec) over v (legacy) if both are provided + final String recoveryId = (yParity != null) ? yParity : v; + + if (recoveryId == null) { + throw new IllegalArgumentException( + "Either 'v' or 'yParity' must be provided in authorization"); + } + + return new CodeDelegation( + chainId, + address, + Bytes.fromHexStringLenient(nonce).toLong(), + SIGNATURE_ALGORITHM + .get() + .createCodeDelegationSignature( + Bytes.fromHexStringLenient(r).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(s).toUnsignedBigInteger(), + Bytes.fromHexStringLenient(recoveryId).get(0))); + } + + @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() { + // recId needs to be between either 0 or 1, otherwise the signature is invalid + // which means we can't recover the authorizer. + if (signature.getRecId() > 1 || signature.getRecId() < 0) { + return Optional.empty(); + } + + return authorizerSupplier.get(); + } + + @Override + public long 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(); + CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(this, rlpOutput); + + final Hash hash = Hash.hash(Bytes.concatenate(MAGIC, rlpOutput.encoded())); + + Optional
authorityAddress; + try { + authorityAddress = + SIGNATURE_ALGORITHM + .get() + .recoverPublicKeyFromSignature(hash, signature) + .map(Address::extract); + } catch (final IllegalArgumentException e) { + authorityAddress = Optional.empty(); + } + + return authorityAddress; + } + + /** + * Create a code delegation authorization with a builder. + * + * @return CodeDelegation.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for CodeDelegation authorizations. */ + public static class Builder { + private BigInteger chainId = BigInteger.ZERO; + private Address address; + private Long nonce; + 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 nonce. + * + * @param nonce the nonce. + * @return this builder + */ + public Builder nonce(final long 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 CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation signAndBuild(final KeyPair keyPair) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.startList(); + output.writeBigIntegerScalar(chainId); + output.writeBytes(address); + output.writeLongScalar(nonce); + output.endList(); + + signature( + SIGNATURE_ALGORITHM + .get() + .sign(Hash.hash(Bytes.concatenate(MAGIC, output.encoded())), keyPair)); + return build(); + } + + /** + * Build the authorization. + * + * @return CodeDelegation + */ + public org.hyperledger.besu.datatypes.CodeDelegation build() { + if (address == null) { + throw new IllegalStateException("Address must be set"); + } + + if (nonce == null) { + throw new IllegalStateException("Nonce must be set"); + } + + if (signature == null) { + throw new IllegalStateException("Signature must be set"); + } + + return new CodeDelegation(chainId, address, nonce, signature); + } + } + + @Override + public String toString() { + return "CodeDelegation{" + + "chainId=" + + chainId + + ", address=" + + address + + ", nonce=" + + nonce + + ", signature=" + + signature + + ", authorizerSupplier=" + + authorizerSupplier + + '}'; + } +}