From 5393ea95e69b25ac285e195692dad0a331d7c393 Mon Sep 17 00:00:00 2001 From: Bilyana Gospodinova Date: Fri, 14 Feb 2025 18:03:59 +0200 Subject: [PATCH] Fix the rest `ContractCallDynamicCallsTest` tests (#10381) This PR fixes the setup for the ContractCallDynamicCallsTest tests when the modularizedServices flag is set to true. Also, the TransactionExecutionService is modified in case of a failure in an opcode trace scenario to return a failed result instead of throwing an exception directly. The reason for this is that we need to be able to return the opcode list from the context before throwing an exception. --------- Signed-off-by: Bilyana Gospodinova --- .../service/TransactionExecutionService.java | 14 +++- .../service/ContractCallDynamicCallsTest.java | 78 +++++++++++++++++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/service/TransactionExecutionService.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/service/TransactionExecutionService.java index 1fbda334b9b..44b8348f3d2 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/service/TransactionExecutionService.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/service/TransactionExecutionService.java @@ -92,7 +92,7 @@ public HederaEvmTransactionProcessingResult execute( if (transactionRecord.receiptOrThrow().status() == com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS) { result = buildSuccessResult(isContractCreate, transactionRecord, params); } else { - handleFailedResult(transactionRecord, isContractCreate, gasUsedCounter); + result = handleFailedResult(transactionRecord, isContractCreate, gasUsedCounter); } return result; } @@ -128,7 +128,7 @@ private HederaEvmTransactionProcessingResult buildSuccessResult( params.getReceiver()); } - private void handleFailedResult( + private HederaEvmTransactionProcessingResult handleFailedResult( final TransactionRecord transactionRecord, final boolean isContractCreate, final MeterProvider gasUsedCounter) @@ -144,7 +144,15 @@ private void handleFailedResult( var errorMessage = getErrorMessage(result).orElse(Bytes.EMPTY); var detail = maybeDecodeSolidityErrorStringToReadableMessage(errorMessage); updateErrorGasUsedMetric(gasUsedCounter, result.gasUsed(), 1); - throw new MirrorEvmTransactionException(status.protoName(), detail, errorMessage.toHexString()); + if (ContractCallContext.get().getOpcodeTracerOptions() == null) { + throw new MirrorEvmTransactionException(status.protoName(), detail, errorMessage.toHexString()); + } else { + // If we are in an opcode trace scenario, we need to return a failed result in order to get the + // opcode list from the ContractCallContext. If we throw an exception instead of returning a result, + // as in the regular case, we won't be able to get the opcode list. + return HederaEvmTransactionProcessingResult.failed( + result.gasUsed(), 0L, 0L, Optional.of(errorMessage), Optional.empty()); + } } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallDynamicCallsTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallDynamicCallsTest.java index 5ad341980c3..05c07812842 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallDynamicCallsTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallDynamicCallsTest.java @@ -33,14 +33,21 @@ import com.hedera.mirror.web3.common.ContractCallContext; import com.hedera.mirror.web3.evm.store.Store.OnMissing; import com.hedera.mirror.web3.exception.MirrorEvmTransactionException; +import com.hedera.mirror.web3.state.MirrorNodeState; import com.hedera.mirror.web3.utils.ContractFunctionProviderRecord; import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls; import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls.AccountAmount; import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls.NftTransfer; import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls.TokenTransferList; import com.hedera.mirror.web3.web3j.generated.DynamicEthCalls.TransferList; +import jakarta.annotation.PostConstruct; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import org.hyperledger.besu.datatypes.Address; import org.junit.jupiter.api.Test; @@ -235,6 +242,63 @@ void associateTokenTransferEthCallFail() { verifyOpcodeTracerCall(functionCall.encodeFunctionCall(), contractFunctionProvider); } + @Test + void associateTokenTransferEthCallFailModularized() throws InvocationTargetException, IllegalAccessException { + // Given + final var modularizedServicesFlag = mirrorNodeEvmProperties.isModularizedServices(); + final var backupProperties = mirrorNodeEvmProperties.getProperties(); + + try { + mirrorNodeEvmProperties.setModularizedServices(true); + // Re-init the captors, because the flag was changed. + super.setUpArgumentCaptors(); + Method postConstructMethod = Arrays.stream(MirrorNodeState.class.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(PostConstruct.class)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("@PostConstruct method not found")); + + postConstructMethod.setAccessible(true); // Make the method accessible + postConstructMethod.invoke(state); + + final Map propertiesMap = new HashMap<>(); + propertiesMap.put("contracts.maxRefundPercentOfGasLimit", "100"); + propertiesMap.put("contracts.maxGasPerSec", "15000000"); + mirrorNodeEvmProperties.setProperties(propertiesMap); + + final var treasuryAccount = accountEntityPersist(); + final var sender = accountEntityPersist(); + + final var contract = testWeb3jService.deploy(DynamicEthCalls::deploy); + + // When + final var functionCall = contract.send_associateTokenTransfer( + toAddress(EntityId.of(21496934L)).toHexString(), // Not existing address + getAddressFromEntity(treasuryAccount), + getAddressFromEntity(sender), + BigInteger.ZERO, + BigInteger.ONE); + + final var contractFunctionProvider = ContractFunctionProviderRecord.builder() + .contractAddress(Address.fromHexString(contract.getContractAddress())) + .expectedErrorMessage("Failed to associate tokens") + .build(); + + // Then + assertThatThrownBy(functionCall::send) + .isInstanceOf(MirrorEvmTransactionException.class) + .satisfies(ex -> { + MirrorEvmTransactionException exception = (MirrorEvmTransactionException) ex; + assertEquals("Failed to associate tokens", exception.getDetail()); + }); + + verifyOpcodeTracerCall(functionCall.encodeFunctionCall(), contractFunctionProvider); + } finally { + // Restore changed property values. + mirrorNodeEvmProperties.setModularizedServices(modularizedServicesFlag); + mirrorNodeEvmProperties.setProperties(backupProperties); + } + } + @ParameterizedTest @CsvSource( textBlock = @@ -340,7 +404,6 @@ void approveTokenGetAllowance(final TokenTypeEnum tokenType, final long amount, : nftPersist(treasuryEntityId, ownerEntityId, spenderEntityId); final var tokenId = token.getTokenId(); final var tokenAddress = toAddress(tokenId); - final var tokenEntityId = entityIdFromEvmAddress(toAddress(tokenId)); final var contract = testWeb3jService.deploy(DynamicEthCalls::deploy); final var contractAddress = Address.fromHexString(contract.getContractAddress()); @@ -350,7 +413,7 @@ void approveTokenGetAllowance(final TokenTypeEnum tokenType, final long amount, tokenAccountPersist(tokenId, ownerEntityId.getId()); if (tokenType == TokenTypeEnum.NON_FUNGIBLE_UNIQUE) { - nftAllowancePersist(tokenEntityId, contractEntityId, ownerEntityId); + nftAllowancePersist(tokenId, contractEntityId, ownerEntityId); } // When @@ -390,14 +453,13 @@ void approveTokenTransferFromGetAllowanceGetBalance( : nftPersist(treasuryEntityId, contractEntityId, spenderEntityId); final var tokenId = token.getTokenId(); final var tokenAddress = toAddress(tokenId); - final var tokenEntityId = entityIdFromEvmAddress(tokenAddress); tokenAccountPersist(tokenId, contractEntityId.getId()); tokenAccountPersist(tokenId, spenderEntityId.getId()); tokenAccountPersist(tokenId, ownerEntityId.getId()); if (tokenType == TokenTypeEnum.NON_FUNGIBLE_UNIQUE) { - nftAllowancePersist(tokenEntityId, contractEntityId, ownerEntityId); + nftAllowancePersist(tokenId, contractEntityId, ownerEntityId); } // When @@ -546,6 +608,7 @@ void approveForAllCryptoTransferGetAllowance() { tokenAccountPersist(tokenId, spenderEntityId.getId()); tokenAccountPersist(tokenId, contractEntityId.getId()); + nftAllowancePersist(tokenId, contractEntityId, contractEntityId); var tokenTransferList = new TokenTransferList( tokenAddress.toHexString(), @@ -587,6 +650,8 @@ void cryptoTransferFromGetAllowanceGetBalance( tokenAccountPersist(tokenId, spenderEntityId.getId()); tokenAccountPersist(tokenId, contractEntityId.getId()); + nftAllowancePersist(tokenId, spenderEntityId, contractEntityId); + nftAllowancePersist(tokenId, contractEntityId, contractEntityId); TokenTransferList tokenTransferList; if (tokenType == TokenTypeEnum.FUNGIBLE_COMMON) { @@ -633,6 +698,7 @@ void transferFromNFTGetAllowance() { tokenAccountPersist(tokenId, spenderEntityId.getId()); tokenAccountPersist(tokenId, contractEntityId.getId()); + nftAllowancePersist(tokenId, contractEntityId, spenderEntityId); // When final var functionCall = contract.send_transferFromNFTGetAllowance(tokenAddress.toHexString(), BigInteger.ONE); @@ -809,10 +875,10 @@ private EntityId spenderEntityPersistWithAlias() { } private void nftAllowancePersist( - final EntityId tokenEntityId, final EntityId spenderEntityId, final EntityId ownerEntityId) { + final long tokenEntityId, final EntityId spenderEntityId, final EntityId ownerEntityId) { domainBuilder .nftAllowance() - .customize(a -> a.tokenId(tokenEntityId.getId()) + .customize(a -> a.tokenId(tokenEntityId) .spender(spenderEntityId.getId()) .owner(ownerEntityId.getId()) .payerAccountId(ownerEntityId)