Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix contract deployment with value (0.117) #9703

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* 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.
*/

package com.hedera.mirror.web3.utils;

import static com.hedera.mirror.web3.validation.HexValidator.HEX_PREFIX;

import jakarta.annotation.Nonnull;
import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;

/**
* A utility class for extracting runtime bytecode from init bytecode of a smart contract.
* <p>
* Smart contracts have init bytecode (constructor bytecode) and runtime bytecode (the code executed when the contract
* is called). This class helps in extracting the runtime bytecode from the given init bytecode by searching for
* specific patterns.
* </p>
*/
@UtilityClass
public class BytecodeUtils {

public static final String SKIP_INIT_CODE_CHECK = "HEDERA_MIRROR_WEB3_EVM_SKIPINITCODECHECK";
private static final String CODECOPY = "39";
private static final String RETURN = "f3";
private static final long MINIMUM_INIT_CODE_SIZE = 14L;
private static final String FREE_MEMORY_POINTER = "60806040";
private static final String FREE_MEMORY_POINTER_2 = "60606040";
private static final String RUNTIME_CODE_PREFIX =
"6080"; // The pattern to find the start of the runtime code in the init bytecode

/**
* Compiled regex pattern to match the init bytecode sequence. The pattern checks for a sequence of a free memory
* pointer setup, a CODECOPY operation, and a RETURN operation, in that order. The sequence is matched
* case-insensitively to account for hexadecimal representations.
* <p>
* Pattern explanation: - (%s|%s) matches either FREE_MEMORY_POINTER or FREE_MEMORY_POINTER_2, which represent setup
* instructions for the free memory pointer, required for initialization bytecode. - [0-9a-z]+ matches one or more
* valid hexadecimal characters (0-9, a-f) after the free memory pointer setup. - %s matches the CODECOPY opcode,
* which copies code to memory and typically follows the memory pointer setup in init bytecode. - [0-9a-z]+ matches
* one or more valid hexadecimal characters (0-9, a-f) between CODECOPY and RETURN. - %s matches the RETURN opcode,
* signaling the end of the initialization bytecode.
*
* <p>
* Example pattern: (60806040|60606040)[0-9a-z]+39[0-9a-z]+f3 This example would match any sequence where either
* "60806040" or "60606040" appears, followed by "39" (CODECOPY) and then "f3" (RETURN), with valid hexadecimal
* characters in between.
*/
private static final Pattern INIT_BYTECODE_PATTERN = Pattern.compile(
String.format(
"(%s|%s)[0-9a-z]+%s[0-9a-z]+%s", FREE_MEMORY_POINTER, FREE_MEMORY_POINTER_2, CODECOPY, RETURN),
Pattern.CASE_INSENSITIVE);

public static String extractRuntimeBytecode(String initBytecode) {
// Check if the bytecode starts with "0x" and remove it if necessary
if (initBytecode.startsWith(HEX_PREFIX)) {
initBytecode = initBytecode.substring(2);

Check warning on line 70 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/utils/BytecodeUtils.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/utils/BytecodeUtils.java#L70

Added line #L70 was not covered by tests
}

String runtimeBytecode = getRuntimeBytecode(initBytecode);

return HEX_PREFIX + runtimeBytecode; // Append "0x" prefix and return
}

@Nonnull
private static String getRuntimeBytecode(final String initBytecode) {
// Find the first occurrence of "CODECOPY" (39)
int codeCopyIndex = initBytecode.indexOf(CODECOPY);

if (codeCopyIndex == -1) {
throw new IllegalArgumentException("CODECOPY instruction (39) not found in init bytecode.");
}

// Find the first occurrence of "6080" after the "CODECOPY"
int runtimeCodePrefixIndex = initBytecode.indexOf(RUNTIME_CODE_PREFIX, codeCopyIndex);

if (runtimeCodePrefixIndex == -1) {
throw new IllegalArgumentException("Runtime code prefix (6080) not found after CODECOPY.");
}

// Extract the runtime bytecode starting from the runtimeCodePrefixIndex
return initBytecode.substring(runtimeCodePrefixIndex);
}

/**
* Checks if a given data string is likely init bytecode.
*
* @param data the data string to check.
* @return true if it is init bytecode, false otherwise.
*/
public static boolean isInitBytecode(final String data) {
if (data == null || data.length() < MINIMUM_INIT_CODE_SIZE) {
return false;
}

return INIT_BYTECODE_PATTERN.matcher(data).find();
}

public static boolean isValidInitBytecode(final String data) {
return shouldSkipBytecodeCheck() || BytecodeUtils.isInitBytecode(data);
}

private static boolean shouldSkipBytecodeCheck() {
return Boolean.parseBoolean(System.getenv(SKIP_INIT_CODE_CHECK));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.hedera.mirror.web3.convert.BlockTypeDeserializer;
import com.hedera.mirror.web3.convert.BlockTypeSerializer;
import com.hedera.mirror.web3.utils.BytecodeUtils;
import com.hedera.mirror.web3.validation.Hex;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Min;
Expand Down Expand Up @@ -63,6 +64,7 @@ private boolean hasFrom() {

@AssertTrue(message = "must not be empty")
private boolean hasTo() {
return value <= 0 || from == null || StringUtils.isNotEmpty(to);
boolean isValidToField = value <= 0 || from == null || StringUtils.isNotEmpty(to);
return BytecodeUtils.isValidInitBytecode(data) || isValidToField;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
import com.hedera.mirror.web3.viewmodel.BlockType;
import com.hedera.mirror.web3.viewmodel.ContractCallRequest;
import com.hedera.mirror.web3.viewmodel.GenericErrorResponse;
import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls;
import com.hedera.mirror.web3.web3j.generated.ERCTestContractHistorical;
import com.hedera.mirror.web3.web3j.generated.EthCall;
import com.hedera.mirror.web3.web3j.generated.EvmCodes;
import com.hedera.mirror.web3.web3j.generated.EvmCodesHistorical;
import com.hedera.mirror.web3.web3j.generated.ExchangeRatePrecompileHistorical;
import com.hedera.mirror.web3.web3j.generated.NestedCallsHistorical;
import com.hedera.mirror.web3.web3j.generated.PrecompileTestContractHistorical;
import com.hedera.mirror.web3.web3j.generated.TestAddressThis;
import io.github.bucket4j.Bucket;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
Expand Down Expand Up @@ -79,6 +88,7 @@ class ContractControllerTest {
private static final String CALL_URI = "/api/v1/contracts/call";
private static final String ONE_BYTE_HEX = "80";
private static final long THROTTLE_GAS_LIMIT = 10_000_000L;
private static final String INIT_CODE = "0x6080604052348015600f57600080fd5b5060a38061001c6000396000f3";

@Resource
private MockMvc mockMvc;
Expand Down Expand Up @@ -117,9 +127,9 @@ private ResultActions contractCall(ContractCallRequest request) {
.content(convert(request)));
}

@NullAndEmptySource
@ValueSource(strings = {"0x00000000000000000000000000000000000007e7"})
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"0x00000000000000000000000000000000000007e7", "0x00000000000000000000000000000000000004e2"})
void estimateGas(String to) throws Exception {
final var request = request();
request.setEstimate(true);
Expand All @@ -128,6 +138,28 @@ void estimateGas(String to) throws Exception {
contractCall(request).andExpect(status().isOk());
}

@ParameterizedTest
@ValueSource(
strings = {
DynamicEthCalls.BINARY,
ERCTestContractHistorical.BINARY,
EthCall.BINARY,
EvmCodes.BINARY,
EvmCodesHistorical.BINARY,
ExchangeRatePrecompileHistorical.BINARY,
NestedCallsHistorical.BINARY,
PrecompileTestContractHistorical.BINARY,
TestAddressThis.BINARY
})
void estimateGasContractDeploy(final String data) throws Exception {
final var request = request();
request.setEstimate(true);
request.setValue(0);
request.setTo(null);
request.setData(data);
contractCall(request).andExpect(status().isOk());
}

@ValueSource(longs = {2000, -2000, 16_000_000L, 0})
@ParameterizedTest
void estimateGasWithInvalidGasParameter(long gas) throws Exception {
Expand Down Expand Up @@ -446,7 +478,7 @@ void callSuccessWithNullAndEmptyData(String data) throws Exception {
void callSuccessOnContractCreateWithMissingFrom() throws Exception {
final var request = request();
request.setFrom(null);
request.setTo(null);
request.setData(INIT_CODE);
request.setValue(0);
request.setEstimate(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,59 @@
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hedera.mirror.web3.utils.BytecodeUtils;
import com.hedera.mirror.web3.viewmodel.BlockType;
import com.hedera.mirror.web3.viewmodel.ContractCallRequest;
import com.hedera.mirror.web3.web3j.generated.TestAddressThis;
import com.hedera.mirror.web3.web3j.generated.TestNestedAddressThis;
import com.hedera.node.app.service.evm.contracts.execution.HederaEvmTransactionProcessingResult;
import jakarta.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.datatypes.Address;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils;

@AutoConfigureMockMvc
class ContractCallAddressThisTest extends AbstractContractCallServiceTest {

private static final String CALL_URI = "/api/v1/contracts/call";

@Resource
protected ContractExecutionService contractCallService;

@Resource
private MockMvc mockMvc;

@Resource
private ObjectMapper objectMapper;

@SpyBean
private ContractExecutionService contractExecutionService;

@SneakyThrows
private ResultActions contractCall(ContractCallRequest request) {
return mockMvc.perform(post(CALL_URI)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(convert(request)));
}

@Test
void deployAddressThisContract() {
final var contract = testWeb3jService.deploy(TestAddressThis::deploy);
final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000));
final var serviceParameters = testWeb3jService.serviceParametersForTopLevelContractCreate(
contract.getContractBinary(), ETH_ESTIMATE_GAS, Address.ZERO);
final long actualGas = 57764L;
Expand All @@ -59,15 +88,16 @@ void deployAddressThisContract() {

@Test
void addressThisFromFunction() {
final var contract = testWeb3jService.deploy(TestAddressThis::deploy);
final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000));
final var functionCall = contract.send_testAddressThisFunction();
verifyEthCallAndEstimateGas(functionCall, contract);
}

@Test
void addressThisEthCallWithoutEvmAlias() throws Exception {
// Given
final var contract = testWeb3jService.deployWithoutPersist(TestAddressThis::deploy);
final var contract =
testWeb3jService.deployWithoutPersistWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000));
addressThisContractPersist(
testWeb3jService.getContractRuntime(), Address.fromHexString(contract.getContractAddress()));
final List<Bytes> capturedOutputs = new ArrayList<>();
Expand All @@ -88,6 +118,43 @@ void addressThisEthCallWithoutEvmAlias() throws Exception {
assertThat(successfulResponse).isEqualTo(capturedOutputs.getFirst().toHexString());
}

@Test
void contractDeployWithoutValue() throws Exception {
// Given
final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000));
final var request = new ContractCallRequest();
request.setBlock(BlockType.LATEST);
request.setData(contract.getContractBinary());
request.setFrom(Address.ZERO.toHexString());
// When
contractCall(request)
// Then
.andExpect(status().isOk())
.andExpect(result -> {
final var response = result.getResponse().getContentAsString();
assertThat(response).contains(BytecodeUtils.extractRuntimeBytecode(contract.getContractBinary()));
});
}

@Test
void contractDeployWithValue() throws Exception {
// Given
final var contract = testWeb3jService.deployWithValue(TestAddressThis::deploy, BigInteger.valueOf(1000));
final var request = new ContractCallRequest();
request.setBlock(BlockType.LATEST);
request.setData(contract.getContractBinary());
request.setFrom(Address.ZERO.toHexString());
request.setValue(1000);
// When
contractCall(request)
// Then
.andExpect(status().isOk())
.andExpect(result -> {
final var response = result.getResponse().getContentAsString();
assertThat(response).contains(BytecodeUtils.extractRuntimeBytecode(contract.getContractBinary()));
});
}

@Test
void deployNestedAddressThisContract() {
final var contract = testWeb3jService.deploy(TestNestedAddressThis::deploy);
Expand Down Expand Up @@ -116,4 +183,9 @@ private void addressThisContractPersist(byte[] runtimeBytecode, Address contract
.persist();
domainBuilder.recordFile().customize(f -> f.bytes(runtimeBytecode)).persist();
}

@SneakyThrows
private String convert(Object object) {
return objectMapper.writeValueAsString(object);
}
}
Loading
Loading