Skip to content

Commit

Permalink
Add EntityIdSingleton for modularized (#10503)
Browse files Browse the repository at this point in the history
This PR add an EntityIdSingleton implementation for getting a peek of the latest entity id.

This is needed because currently without custom implementation and db reads if we make a contract call that internally has a contract create - the contract will be assigned the first id after the last system account clashing with already existing accounts.

EntityIdSingleton - implements get method - to retrieve the first available entityId from db
EntityRepository - adds query to retrieve the first available entityId after the last system account

---------

Signed-off-by: Kristiyan Selveliev <[email protected]>
  • Loading branch information
kselveliev authored Mar 6, 2025
1 parent 5be0df2 commit 6335082
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,16 @@ order by lower(timestamp_range) desc
""",
nativeQuery = true)
Optional<Entity> 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);
}
Original file line number Diff line number Diff line change
@@ -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<EntityNumber> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,7 +115,7 @@ void verifyServicesHaveAssignedDataSources() {
verifyServiceDataSources(states, RecordCacheService.NAME, recordCacheServiceDataSources);

// EntityIdService
Map<String, Class<?>> entityIdServiceDataSources = Map.of("ENTITY_ID", DefaultSingleton.class);
Map<String, Class<?>> entityIdServiceDataSources = Map.of("ENTITY_ID", EntityIdSingleton.class);
verifyServiceDataSources(states, EntityIdService.NAME, entityIdServiceDataSources);

// TokenService
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 30 additions & 0 deletions hedera-mirror-web3/src/test/solidity/NestedCallsTestContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}

0 comments on commit 6335082

Please sign in to comment.