From 2c59682b512f84b98de443a1b6d65d19aa806173 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 21 Jun 2024 18:36:16 -0400 Subject: [PATCH] Suppress recoverable error for inactive evm addresses (#8536) Suppresses recoverable errors when unable to lookup an entity id for an inactive evm address. --------- Signed-off-by: Edwin Greene --- .../domain/ContractResultServiceImpl.java | 11 +++++-- .../importer/domain/EntityIdService.java | 9 ++++++ .../importer/domain/EntityIdServiceImpl.java | 17 +++++++++-- .../domain/ContractResultServiceImplTest.java | 30 ++++++++++++++++++- .../domain/EntityIdServiceImplTest.java | 27 ++++++++++++++++- 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/ContractResultServiceImpl.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/ContractResultServiceImpl.java index 2e245077fe8..35db0587896 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/ContractResultServiceImpl.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/ContractResultServiceImpl.java @@ -89,12 +89,19 @@ public void process(@NonNull RecordItem recordItem, Transaction transaction) { // contractResult var transactionHandler = transactionHandlerFactory.get(TransactionType.of(transaction.getType())); + // Whether a recoverable error should be thrown on entity id lookup + // Inactive evm addresses which cannot be looked up should not throw recoverable errors + boolean lookupRecoverable = + !(recordItem.isSuccessful() && (functionResult.getContractID().hasEvmAddress())); + // in pre-compile case transaction is not a contract type and entityId will be of a different type var contractId = (contractCallOrCreate ? Optional.ofNullable(transaction.getEntityId()) - : entityIdService.lookup(functionResult.getContractID())) + : entityIdService.lookup(functionResult.getContractID(), lookupRecoverable)) .orElse(EntityId.EMPTY); - var isRecoverableError = EntityId.isEmpty(contractId) + var isRecoverableError = !(recordItem.isSuccessful() + && transactionRecord.getReceipt().getContractID().hasEvmAddress()) + && EntityId.isEmpty(contractId) && !contractCallOrCreate && !ContractID.getDefaultInstance().equals(functionResult.getContractID()); if (isRecoverableError) { diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java index 3dea400ad7c..9c7d697624d 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java @@ -53,6 +53,15 @@ public interface EntityIdService { */ Optional lookup(ContractID contractId); + /** + * Converts a protobuf ContractID to an EntityID, resolving any EVM addresses that may be present. + * + * @param contractId The protobuf contract ID + * @param throwRecoverableError If true, will throw a recoverable error if an EVM address cannot be found. + * @return An optional of the converted EntityId if it can be resolved, or EntityId.EMPTY if none can be resolved. + */ + Optional lookup(ContractID contractId, boolean throwRecoverableError); + /** * Specialized form of lookup(ContractID) that returns the first contract ID parameter that resolves to a non-empty * EntityId. diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java index fb5992f505b..de693bffdc8 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java @@ -86,6 +86,11 @@ public Optional lookup(AccountID... accountIds) { @Override public Optional lookup(ContractID contractId) { + return lookup(contractId, true); + } + + @Override + public Optional lookup(ContractID contractId, boolean throwRecoverableError) { if (contractId == null || contractId.equals(ContractID.getDefaultInstance())) { return EMPTY; } @@ -95,7 +100,10 @@ public Optional lookup(ContractID contractId) { case EVM_ADDRESS -> cacheLookup( contractId.getEvmAddress(), () -> findByEvmAddress( - toBytes(contractId.getEvmAddress()), contractId.getShardNum(), contractId.getRealmNum())); + toBytes(contractId.getEvmAddress()), + contractId.getShardNum(), + contractId.getRealmNum(), + throwRecoverableError)); default -> { Utility.handleRecoverableError("Invalid ContractID: {}", contractId); yield Optional.empty(); @@ -158,12 +166,17 @@ public void notify(Entity entity) { } private Optional findByEvmAddress(byte[] evmAddress, long shardNum, long realmNum) { + return findByEvmAddress(evmAddress, shardNum, realmNum, true); + } + + private Optional findByEvmAddress( + byte[] evmAddress, long shardNum, long realmNum, boolean throwRecoverableError) { var id = Optional.ofNullable(DomainUtils.fromEvmAddress(evmAddress)) // Verify shard and realm match when assuming evmAddress is in the 'shard.realm.num' form .filter(e -> e.getShard() == shardNum && e.getRealm() == realmNum) .or(() -> entityRepository.findByEvmAddress(evmAddress).map(EntityId::of)); - if (id.isEmpty()) { + if (id.isEmpty() && throwRecoverableError) { Utility.handleRecoverableError("Entity not found for EVM address {}", Hex.encodeHexString(evmAddress)); } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/ContractResultServiceImplTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/ContractResultServiceImplTest.java index f0dc25f57d5..b20a2693916 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/ContractResultServiceImplTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/ContractResultServiceImplTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.protobuf.ByteString; import com.hedera.mirror.common.domain.DomainBuilder; import com.hedera.mirror.common.domain.contract.ContractTransaction; import com.hedera.mirror.common.domain.entity.EntityId; @@ -37,7 +38,9 @@ import com.hedera.mirror.importer.parser.record.transactionhandler.TransactionHandler; import com.hedera.mirror.importer.parser.record.transactionhandler.TransactionHandlerFactory; import com.hederahashgraph.api.proto.java.ContractID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenType; +import com.hederahashgraph.api.proto.java.TransactionReceipt; import java.util.ArrayList; import java.util.HashSet; import java.util.Objects; @@ -91,6 +94,27 @@ private static Stream provideEntities() { .record(x -> x.setContractCallResult(builder.contractFunctionResult())) .build(); + var contractIdWithEvm = ContractID.newBuilder() + .setEvmAddress(ByteString.copyFromUtf8("1234")) + .build(); + + // The transaction receipt does not have an evm address, so a recoverable error is expected. + Function withInactiveEvmFunctionOnly = + (RecordItemBuilder builder) -> builder.tokenMint(TokenType.FUNGIBLE_COMMON) + .record(x -> x.setReceipt( + TransactionReceipt.newBuilder().setStatus(ResponseCodeEnum.SUCCESS)) + .setContractCallResult(builder.contractFunctionResult(contractIdWithEvm))) + .build(); + + // The transaction receipt has an evm address, so no recoverable error is expected. + Function withInactiveEvmReceipt = + (RecordItemBuilder builder) -> builder.tokenMint(TokenType.FUNGIBLE_COMMON) + .record(x -> x.setReceipt(TransactionReceipt.newBuilder() + .setStatus(ResponseCodeEnum.SUCCESS) + .setContractID(contractIdWithEvm)) + .setContractCallResult(builder.contractFunctionResult(contractIdWithEvm))) + .build(); + Function contractCreate = (RecordItemBuilder builder) -> builder.contractCreate().build(); @@ -101,7 +125,11 @@ private static Stream provideEntities() { Arguments.of(withDefaultContractId, EntityId.EMPTY, false), Arguments.of(contractCreate, EntityId.EMPTY, false), Arguments.of(contractCreate, null, false), - Arguments.of(contractCreate, EntityId.of(0, 0, 5), false)); + Arguments.of(contractCreate, EntityId.of(0, 0, 5), false), + Arguments.of(withInactiveEvmFunctionOnly, null, true), + Arguments.of(withInactiveEvmFunctionOnly, EntityId.EMPTY, true), + Arguments.of(withInactiveEvmReceipt, null, false), + Arguments.of(withInactiveEvmReceipt, EntityId.EMPTY, false)); } @BeforeEach diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java index 5792bb5b87b..09c1f600a1b 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java @@ -32,10 +32,15 @@ import com.hederahashgraph.api.proto.java.ContractID; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; @RequiredArgsConstructor +@ExtendWith(OutputCaptureExtension.class) class EntityIdServiceImplTest extends ImporterIntegrationTest { // in the form 'shard.realm.num' @@ -45,6 +50,8 @@ class EntityIdServiceImplTest extends ImporterIntegrationTest { 0, 0, 0, 0, 0, 0, 0, 100, // num }; + private static final String RECOVERABLE_ERROR_LOG_PREFIX = "Recoverable error. "; + private final EntityRepository entityRepository; private final EntityIdService entityIdService; @@ -213,13 +220,31 @@ void lookupContractEvmAddressSpecific() { } @Test - void lookupContractEvmAddressNoMatch() { + void lookupContractEvmAddressNoMatch(CapturedOutput output) { Entity contract = domainBuilder .entity() .customize(e -> e.alias(null).type(CONTRACT)) .get(); var contractId = getProtoContractId(contract); assertThat(entityIdService.lookup(contractId)).isEmpty(); + assertThat(output.getAll()).containsIgnoringCase(RECOVERABLE_ERROR_LOG_PREFIX); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void lookupContractEvmAddressRecoverableError(boolean throwRecoverableError, CapturedOutput output) { + Entity contract = domainBuilder + .entity() + .customize(e -> e.alias(null).type(CONTRACT)) + .get(); + var contractId = getProtoContractId(contract); + + assertThat(entityIdService.lookup(contractId, throwRecoverableError)).isEmpty(); + if (throwRecoverableError) { + assertThat(output.getAll()).containsIgnoringCase(RECOVERABLE_ERROR_LOG_PREFIX); + } else { + assertThat(output.getAll()).doesNotContainIgnoringCase(RECOVERABLE_ERROR_LOG_PREFIX); + } } @Test