Skip to content

Commit

Permalink
Suppress recoverable error for inactive evm addresses (#8536)
Browse files Browse the repository at this point in the history
 Suppresses recoverable errors when unable to lookup an entity id for an inactive evm address.

---------

Signed-off-by: Edwin Greene <[email protected]>
  • Loading branch information
edwin-greene authored Jun 21, 2024
1 parent 16d97bc commit 2c59682
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ public interface EntityIdService {
*/
Optional<EntityId> 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<EntityId> lookup(ContractID contractId, boolean throwRecoverableError);

/**
* Specialized form of lookup(ContractID) that returns the first contract ID parameter that resolves to a non-empty
* EntityId.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ public Optional<EntityId> lookup(AccountID... accountIds) {

@Override
public Optional<EntityId> lookup(ContractID contractId) {
return lookup(contractId, true);
}

@Override
public Optional<EntityId> lookup(ContractID contractId, boolean throwRecoverableError) {
if (contractId == null || contractId.equals(ContractID.getDefaultInstance())) {
return EMPTY;
}
Expand All @@ -95,7 +100,10 @@ public Optional<EntityId> 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();
Expand Down Expand Up @@ -158,12 +166,17 @@ public void notify(Entity entity) {
}

private Optional<EntityId> findByEvmAddress(byte[] evmAddress, long shardNum, long realmNum) {
return findByEvmAddress(evmAddress, shardNum, realmNum, true);
}

private Optional<EntityId> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -91,6 +94,27 @@ private static Stream<Arguments> 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<RecordItemBuilder, RecordItem> 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<RecordItemBuilder, RecordItem> 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<RecordItemBuilder, RecordItem> contractCreate =
(RecordItemBuilder builder) -> builder.contractCreate().build();

Expand All @@ -101,7 +125,11 @@ private static Stream<Arguments> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2c59682

Please sign in to comment.