diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/repository/EntityRepository.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/repository/EntityRepository.java index e62610be46..5319e7e40c 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/repository/EntityRepository.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/repository/EntityRepository.java @@ -154,4 +154,16 @@ order by lower(timestamp_range) desc """, nativeQuery = true) Optional findActiveByIdAndTimestamp(long id, long blockTimestamp); + + @Query( + value = + """ + select id + from entity + where shard = ?1 and realm = ?2 + order by id desc + limit 1 + """, + nativeQuery = true) + Long findMaxId(long shard, long realm); } diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/singleton/EntityIdSingleton.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/singleton/EntityIdSingleton.java new file mode 100644 index 0000000000..5286a3b072 --- /dev/null +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/singleton/EntityIdSingleton.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +package com.hedera.mirror.web3.state.singleton; + +import static com.hedera.node.app.ids.schemas.V0490EntityIdSchema.ENTITY_ID_STATE_KEY; + +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.mirror.common.CommonProperties; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.web3.evm.properties.MirrorNodeEvmProperties; +import com.hedera.mirror.web3.repository.EntityRepository; +import com.hedera.node.config.data.HederaConfig; +import jakarta.inject.Named; +import lombok.RequiredArgsConstructor; + +@Named +@RequiredArgsConstructor +@SuppressWarnings("deprecation") +public class EntityIdSingleton implements SingletonState { + private final EntityRepository entityRepository; + private final MirrorNodeEvmProperties mirrorNodeEvmProperties; + private final CommonProperties commonProperties; + + @Override + public String getKey() { + return ENTITY_ID_STATE_KEY; + } + + @Override + public EntityNumber get() { + final long firstUserEntity = mirrorNodeEvmProperties + .getVersionedConfiguration() + .getConfigData(HederaConfig.class) + .firstUserEntity(); + + final Long maxId = entityRepository.findMaxId(commonProperties.getShard(), commonProperties.getRealm()); + + if (maxId == null) { + return new EntityNumber(EntityId.of(firstUserEntity).getNum()); + } + + final var maxEntityId = EntityId.of(maxId); + final var nextId = Math.max(maxEntityId.getNum() + 1, firstUserEntity); + return new EntityNumber(nextId); + } +} diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/repository/EntityRepositoryTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/repository/EntityRepositoryTest.java index 062df90375..2b7d4636f3 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/repository/EntityRepositoryTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/repository/EntityRepositoryTest.java @@ -463,4 +463,16 @@ void findHistoricalEntityByEvmAddressOrAliasAndTimestampRangeEqualToBlockTimesta .usingRecursiveComparison() .isEqualTo(entityHistory); } + + @Test + void findMaxIdEmptyDb() { + assertThat(entityRepository.findMaxId(0, 0)).isNull(); + } + + @Test + void findMaxId() { + final long lastId = 1111; + domainBuilder.entity().customize(e -> e.id(lastId)).persist(); + assertThat(entityRepository.findMaxId(0, 0)).isEqualTo(lastId); + } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsTest.java index d0ef7bdbde..3e531e3bf0 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsTest.java @@ -437,6 +437,24 @@ void nestedMintTokenAndHardcodedResult() throws Exception { verifyOpcodeTracerCall(function.encodeFunctionCall(), contract); } + @Test + @SuppressWarnings("deprecation") + void nestedDeployTwoContracts() throws Exception { + // Given + final var contract = testWeb3jService.deploy(NestedCalls::deploy); + final var sender = accountEntityPersist(); + testWeb3jService.setValue(100_000_000_000L); + testWeb3jService.setSender(toAddress(sender.toEntityId()).toHexString()); + // When + final var function = contract.call_deployNestedContracts(); + final var result = function.send(); + // Then + // verify that contract addresses are different + assertThat(result.getValue1()).isNotEqualTo(result.getValue2()); + // verify that contract balances are different + assertThat(result.getValue3()).isNotEqualTo(result.getValue4()); + } + private KeyValue getKeyValueForType(final KeyValueType keyValueType, String contractAddress) { return switch (keyValueType) { case INHERIT_ACCOUNT_KEY -> new KeyValue( diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServicePrecompileModificationTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServicePrecompileModificationTest.java index 1013524056..7be07d41a4 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServicePrecompileModificationTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallServicePrecompileModificationTest.java @@ -712,10 +712,7 @@ void createNonFungibleToken() throws Exception { testWeb3jService.setSender(toAddress(sender.toEntityId()).toHexString()); - final var treasuryAccount = domainBuilder - .entity() - .customize(e -> e.type(EntityType.ACCOUNT).deleted(false).evmAddress(null)) - .persist(); + final var treasuryAccount = accountEntityPersist(); final var token = populateHederaToken( contract.getContractAddress(), TokenTypeEnum.NON_FUNGIBLE_UNIQUE, treasuryAccount.toEntityId()); @@ -756,10 +753,7 @@ void createNonFungibleTokenWithCustomFees() throws Exception { testWeb3jService.setSender(toAddress(sender.toEntityId()).toHexString()); testWeb3jService.setValue(value); - final var treasuryAccount = domainBuilder - .entity() - .customize(e -> e.type(EntityType.ACCOUNT).deleted(false).evmAddress(null)) - .persist(); + final var treasuryAccount = accountEntityPersist(); final var token = populateHederaToken( contract.getContractAddress(), TokenTypeEnum.NON_FUNGIBLE_UNIQUE, treasuryAccount.toEntityId()); final var fixedFee = new FixedFee( diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateIntegrationTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateIntegrationTest.java index b2beda9498..4d8309ea49 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateIntegrationTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateIntegrationTest.java @@ -16,6 +16,7 @@ import com.hedera.mirror.web3.state.keyvalue.TokenRelationshipReadableKVState; import com.hedera.mirror.web3.state.singleton.BlockInfoSingleton; import com.hedera.mirror.web3.state.singleton.DefaultSingleton; +import com.hedera.mirror.web3.state.singleton.EntityIdSingleton; import com.hedera.mirror.web3.state.singleton.RunningHashesSingleton; import com.hedera.node.app.fees.FeeService; import com.hedera.node.app.ids.EntityIdService; @@ -114,7 +115,7 @@ void verifyServicesHaveAssignedDataSources() { verifyServiceDataSources(states, RecordCacheService.NAME, recordCacheServiceDataSources); // EntityIdService - Map> entityIdServiceDataSources = Map.of("ENTITY_ID", DefaultSingleton.class); + Map> entityIdServiceDataSources = Map.of("ENTITY_ID", EntityIdSingleton.class); verifyServiceDataSources(states, EntityIdService.NAME, entityIdServiceDataSources); // TokenService diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonIntegrationTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonIntegrationTest.java new file mode 100644 index 0000000000..c4c70a34d3 --- /dev/null +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonIntegrationTest.java @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +package com.hedera.mirror.web3.state.singleton; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hedera.mirror.common.CommonProperties; +import com.hedera.mirror.web3.Web3IntegrationTest; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; + +@RequiredArgsConstructor +class EntityIdSingletonIntegrationTest extends Web3IntegrationTest { + + private final EntityIdSingleton entityIdSingleton; + private final CommonProperties commonProperties; + + @Test + void shouldReturnNextIdWithIncrementAndRealmAndShard() { + // Create an entity with shard and realm set to (1,1) + final var entityWithShardAndRealm = + domainBuilder.entity().customize(e -> e.shard(1L).realm(1L)).persist(); + + // Get ID before setting the correct shard and realm + final var entityNumberBeforeConfig = entityIdSingleton.get(); + + // Set correct shard and realm + commonProperties.setRealm(1L); + commonProperties.setShard(1L); + final var entityNumberAfterConfig = entityIdSingleton.get(); + + // Reset to default shard and realm (0,0) + commonProperties.setRealm(0L); + commonProperties.setShard(0L); + + final var entity2 = domainBuilder.entity().persist(); + final var entityNumber2 = entityIdSingleton.get(); + + final var entity3 = domainBuilder.entity().persist(); + final var entityNumber3 = entityIdSingleton.get(); + + assertThat(entityNumberBeforeConfig.number()).isNotEqualTo(entityWithShardAndRealm.getNum() + 1); + + assertThat(entityNumberAfterConfig.number()).isEqualTo(entityWithShardAndRealm.getNum() + 1); + + assertThat(entityNumber2.number()).isEqualTo(entity2.getNum() + 1); + + assertThat(entityNumber3.number()).isEqualTo(entity3.getNum() + 1); + } +} diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonTest.java new file mode 100644 index 0000000000..cc0177393b --- /dev/null +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/singleton/EntityIdSingletonTest.java @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +package com.hedera.mirror.web3.state.singleton; + +import static com.hedera.mirror.web3.utils.ContractCallTestUtil.FIRST_USER_ENTITY_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.hedera.mirror.common.CommonProperties; +import com.hedera.mirror.web3.evm.properties.MirrorNodeEvmProperties; +import com.hedera.mirror.web3.repository.EntityRepository; +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}) +class EntityIdSingletonTest { + + private EntityIdSingleton entityIdSingleton; + + private CommonProperties commonProperties; + + @Mock + private EntityRepository entityRepository; + + @BeforeEach + void setup() { + commonProperties = new CommonProperties(); + entityIdSingleton = new EntityIdSingleton(entityRepository, new MirrorNodeEvmProperties(), commonProperties); + } + + @Test + void shouldReturnFirstUserEntityIdWhenMaxIdIsLessThanLastSystemAccount() { + when(entityRepository.findMaxId(0, 0)).thenReturn(900L); + assertThat(entityIdSingleton.get().number()).isEqualTo(FIRST_USER_ENTITY_ID); + } + + @Test + void shouldReturnFirstUserEntityIdWhenMaxIdIsNull() { + when(entityRepository.findMaxId(0, 0)).thenReturn(null); + assertThat(entityIdSingleton.get().number()).isEqualTo(FIRST_USER_ENTITY_ID); + } + + @Test + void shouldReturnNextIdWhenMaxIdIsGreaterThanLastSystemAccount() { + long maxId = 2000; + when(entityRepository.findMaxId(0, 0)).thenReturn(maxId); + assertThat(entityIdSingleton.get().number()).isEqualTo(maxId + 1); + } + + @Test + void shouldReturnNextIdWhenMaxIdIsGreaterThanLastSystemAccountNonZeroRealmShard() { + long maxId = 2000; + commonProperties.setShard(1); + commonProperties.setRealm(1); + when(entityRepository.findMaxId(1, 1)).thenReturn(maxId); + assertThat(entityIdSingleton.get().number()).isEqualTo(maxId + 1); + } + + @Test + void shouldIncrementIdMultipleTimes() { + long currentMaxId = 1001L; + long end = 1005L; + for (long expectedId = currentMaxId + 1; expectedId <= end; expectedId++) { + when(entityRepository.findMaxId(0, 0)).thenReturn(currentMaxId); + assertThat(entityIdSingleton.get().number()).isEqualTo(expectedId); + currentMaxId++; + } + } + + @Test + void shouldReturnNextIdWhenMaxIdIsGreaterThanLastSystemAccountWithIncrement() { + long maxId = 2000; + when(entityRepository.findMaxId(0, 0)).thenReturn(maxId); + assertThat(entityIdSingleton.get().number()).isEqualTo(maxId + 1); + } +} diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/ContractCallTestUtil.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/ContractCallTestUtil.java index 9a2688b074..0094c43474 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/ContractCallTestUtil.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/utils/ContractCallTestUtil.java @@ -12,6 +12,7 @@ @UtilityClass public class ContractCallTestUtil { + public static final long FIRST_USER_ENTITY_ID = 1001; public static final long TRANSACTION_GAS_LIMIT = 15_000_000L; public static final double GAS_ESTIMATE_MULTIPLIER_LOWER_RANGE = 1.05; diff --git a/hedera-mirror-web3/src/test/solidity/NestedCallsTestContract.sol b/hedera-mirror-web3/src/test/solidity/NestedCallsTestContract.sol index a5886650bc..03fc15c517 100644 --- a/hedera-mirror-web3/src/test/solidity/NestedCallsTestContract.sol +++ b/hedera-mirror-web3/src/test/solidity/NestedCallsTestContract.sol @@ -181,4 +181,34 @@ contract NestedCalls is HederaTokenService { (int responseCode, int64 newTotalSupply, int64[] memory serialNumbers) = HederaTokenService.mintToken(token, amount, metadata); return "hardcodedResult"; } + + function deployNestedContracts() public payable returns (address, address, uint256, uint256) { + require(msg.value >= 30000 wei, "Insufficient funds to deploy contracts"); + + // Deploy contracts with minimal balance + MockContract newContract1 = (new MockContract){value: 10000 wei}(); + MockContract newContract2 = (new MockContract){value: 20000 wei}(); + + // Get the balance of each contract + uint256 contract1Balance = address(newContract1).balance; + uint256 contract2Balance = address(newContract2).balance; + + // Return contract addresses and their balances + return (address(newContract1), address(newContract2), contract1Balance, contract2Balance); + } +} + +contract MockContract { + + constructor() payable {} + + function getAddress() public view returns (address) { + return address(this); + } + + function destroy() public { + selfdestruct(payable(msg.sender)); + } + + receive() external payable {} }