diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileCreateHandler.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileCreateHandler.java index d0968b8ee93f..cd6a727ca660 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileCreateHandler.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/handlers/FileCreateHandler.java @@ -3,10 +3,13 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_FILE_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static com.hedera.node.app.service.file.impl.utils.FileServiceUtils.validateAndAddRequiredKeys; import static com.hedera.node.app.service.file.impl.utils.FileServiceUtils.validateContent; import static com.hedera.node.app.spi.validation.ExpiryMeta.NA; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.FileID; @@ -51,6 +54,13 @@ public FileCreateHandler() {} public void pureChecks(@NonNull final PureChecksContext context) throws PreCheckException { final FileCreateTransactionBody transactionBody = context.body().fileCreateOrThrow(); + if (transactionBody.hasShardID()) { + validateTruePreCheck(transactionBody.shardIDOrThrow().shardNum() >= 0, INVALID_FILE_ID); + } + if (transactionBody.hasRealmID()) { + validateTruePreCheck(transactionBody.realmIDOrThrow().realmNum() >= 0, INVALID_FILE_ID); + } + if (!transactionBody.hasExpirationTime()) { throw new PreCheckException(INVALID_EXPIRATION_TIME); } @@ -80,6 +90,17 @@ public void handle(@NonNull final HandleContext handleContext) throws HandleExce final var fileServiceConfig = handleContext.configuration().getConfigData(FilesConfig.class); final var fileCreateTransactionBody = handleContext.body().fileCreateOrThrow(); + final var hederaConfig = handleContext.configuration().getConfigData(HederaConfig.class); + + // Don't allow creation of files that don't match the configured shard and realm + if (fileCreateTransactionBody.hasShardID()) { + validateTrue( + fileCreateTransactionBody.shardIDOrThrow().shardNum() == hederaConfig.shard(), INVALID_FILE_ID); + } + if (fileCreateTransactionBody.hasRealmID()) { + validateTrue( + fileCreateTransactionBody.realmIDOrThrow().realmNum() == hederaConfig.realm(), INVALID_FILE_ID); + } if (fileCreateTransactionBody.hasKeys()) { KeyList transactionKeyList = fileCreateTransactionBody.keys(); @@ -110,18 +131,11 @@ public void handle(@NonNull final HandleContext handleContext) throws HandleExce handleContext.attributeValidator().validateMemo(fileCreateTransactionBody.memo()); builder.memo(fileCreateTransactionBody.memo()); - final var hederaConfig = handleContext.configuration().getConfigData(HederaConfig.class); builder.keys(fileCreateTransactionBody.keys()); final var fileId = FileID.newBuilder() .fileNum(handleContext.entityNumGenerator().newEntityNum()) - .shardNum( - fileCreateTransactionBody.hasShardID() - ? fileCreateTransactionBody.shardIDOrThrow().shardNum() - : hederaConfig.shard()) - .realmNum( - fileCreateTransactionBody.hasRealmID() - ? fileCreateTransactionBody.realmIDOrThrow().realmNum() - : hederaConfig.realm()) + .shardNum(hederaConfig.shard()) + .realmNum(hederaConfig.realm()) .build(); builder.fileId(fileId); validateContent(CommonPbjConverters.asBytes(fileCreateTransactionBody.contents()), fileServiceConfig); diff --git a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileCreateTest.java b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileCreateTest.java index 2f923ec6e5cc..c19b0ec7e6eb 100644 --- a/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileCreateTest.java +++ b/hedera-node/hedera-file-service-impl/src/test/java/com/hedera/node/app/service/file/impl/test/handlers/FileCreateTest.java @@ -125,6 +125,9 @@ void setUp() { config = HederaTestConfigBuilder.createConfig().getConfigData(FilesConfig.class); lenient().when(handleContext.configuration()).thenReturn(configuration); lenient().when(configuration.getConfigData(FilesConfig.class)).thenReturn(config); + lenient() + .when(configuration.getConfigData(HederaConfig.class)) + .thenReturn(DEFAULT_CONFIG.getConfigData(HederaConfig.class)); lenient().when(storeFactory.writableStore(WritableFileStore.class)).thenReturn(fileStore); lenient().when(handleContext.entityNumGenerator()).thenReturn(entityNumGenerator); } @@ -310,13 +313,63 @@ void failsWhenMaxRegimeExceeds() { assertEquals(2, fileStore.sizeOfState()); config = new FilesConfig(1L, 1L, 1L, 1L, 1L, 1L, 1L, new LongPair(150L, 159L), 1L, 1L, 1); - given(configuration.getConfigData(any())).willReturn(config); + given(configuration.getConfigData(FilesConfig.class)).willReturn(config); final var msg = assertThrows(HandleException.class, () -> subject.handle(handleContext)); assertEquals(ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED, msg.getStatus()); assertEquals(0, this.fileStore.modifiedFiles().size()); } + @Test + @DisplayName("Handle rejects a shard that doesn't match the network configuration") + void handleRejectsMismatchedShard() { + final var txBody = newCreateTxn(anotherKeys, expirationTime, SHARD + 1, REALM); + given(handleContext.body()).willReturn(txBody); + + final var failure = assertThrows(HandleException.class, () -> subject.handle(handleContext)); + assertEquals(ResponseCodeEnum.INVALID_FILE_ID, failure.getStatus()); + assertEquals(0, fileStore.modifiedFiles().size()); + } + + @Test + @DisplayName("Handle rejects a realm that doesn't match the network configuration") + void handleRejectsMismatchedRealm() { + final var txBody = newCreateTxn(anotherKeys, expirationTime, SHARD, REALM + 1); + given(handleContext.body()).willReturn(txBody); + + final var failure = assertThrows(HandleException.class, () -> subject.handle(handleContext)); + assertEquals(ResponseCodeEnum.INVALID_FILE_ID, failure.getStatus()); + assertEquals(0, fileStore.modifiedFiles().size()); + } + + @Test + @DisplayName("Handle uses the configured shard and realm when the body omits them") + void handleUsesConfiguredShardAndRealmWhenAbsent() { + final var txBody = TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder().accountID(ACCOUNT_ID_3)) + .fileCreate(FileCreateTransactionBody.newBuilder() + .keys(anotherKeys) + .memo("memo") + .contents(Bytes.wrap(contents)) + .expirationTime(Timestamp.newBuilder().seconds(expirationTime))) + .build(); + + given(handleContext.body()).willReturn(txBody); + given(handleContext.attributeValidator()).willReturn(validator); + given(storeFactory.writableStore(WritableFileStore.class)).willReturn(writableStore); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + given(expiryValidator.resolveCreationAttempt(anyBoolean(), any(), any())) + .willReturn(new ExpiryMeta(expirationTime, NA, null)); + given(entityNumGenerator.newEntityNum()).willReturn(1_234L); + given(handleContext.savepointStack()).willReturn(stack); + given(stack.getBaseBuilder(CreateFileStreamBuilder.class)).willReturn(recordBuilder); + + subject.handle(handleContext); + + assertEquals(fileId, fileStore.get(fileId).orElseThrow().fileId()); + verify(recordBuilder).fileID(fileId); + } + public static void assertFailsWith(final ResponseCodeEnum status, final Runnable something) { final var ex = assertThrows(PreCheckException.class, something::run); assertEquals(status, ex.responseCode()); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/DispatchingEvmFrameState.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/DispatchingEvmFrameState.java index cb5afb41fb5d..554958c0fc4d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/DispatchingEvmFrameState.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/DispatchingEvmFrameState.java @@ -363,7 +363,7 @@ public boolean isHollowAccount(@NonNull final Address address) { if (number == MISSING_ENTITY_NUMBER) { return false; } - final AccountID accountID = AccountID.newBuilder().accountNum(number).build(); + final AccountID accountID = entityIdFactory().newAccountId(number); final var account = nativeOperations.getAccount(accountID); if (account == null) { return false; @@ -470,15 +470,14 @@ private Optional validateAccountCreation(@NonNull final A } final var number = maybeMissingNumberOf(address, nativeOperations); if (number != MISSING_ENTITY_NUMBER) { - final AccountID accountID = - AccountID.newBuilder().accountNum(number).build(); + final AccountID accountID = entityIdFactory().newAccountId(number); final var account = nativeOperations.getAccount(accountID); if (account != null) { if (account.expiredAndPendingRemoval()) { return Optional.of(FAILURE_DURING_LAZY_ACCOUNT_CREATION); } else { throw new IllegalArgumentException( - "Unexpired account 0.0." + number + " already exists at address " + address); + "Unexpired account " + accountID + " already exists at address " + address); } } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/DispatchingEvmFrameStateTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/DispatchingEvmFrameStateTest.java index cc0752bfbcc6..250a4e99a687 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/DispatchingEvmFrameStateTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/DispatchingEvmFrameStateTest.java @@ -62,6 +62,7 @@ import com.hedera.node.app.service.contract.impl.state.TokenEvmAccount; import com.hedera.node.app.service.contract.impl.state.TxStorageUsage; import com.hedera.node.app.service.contract.impl.test.TestHelpers; +import com.hedera.node.app.spi.fixtures.ids.FakeEntityIdFactoryImpl; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; @@ -496,6 +497,7 @@ void cannotTransferToScheduleAccount() { @Test void cannotLazyCreateOverExpiredAccount() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); givenWellKnownAccount(contractWith(A_ACCOUNT_ID).expiredAndPendingRemoval(true)); given(nativeOperations.resolveAlias( DEFAULT_HEDERA_CONFIG.shard(), @@ -512,6 +514,7 @@ void cannotLazyCreateOverExpiredAccount() { @Test void noHaltIfLazyCreationOk() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); given(nativeOperations.createHollowAccount(tuweniToPbjBytes(EVM_ADDRESS))) .willReturn(ResponseCodeEnum.SUCCESS); given(nativeOperations.configuration()).willReturn(configuration); @@ -522,6 +525,7 @@ void noHaltIfLazyCreationOk() { @Test void translatesMaxAccountsCreated() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); given(nativeOperations.createHollowAccount(tuweniToPbjBytes(EVM_ADDRESS))) .willReturn(ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); given(nativeOperations.configuration()).willReturn(configuration); @@ -541,6 +545,7 @@ void throwsOnLazyCreateOfLongZeroAddress() { @Test void throwsOnLazyCreateOfNonExpiredAccount() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); givenWellKnownAccount(contractWith(A_ACCOUNT_ID)); given(nativeOperations.configuration()).willReturn(configuration); given(nativeOperations.resolveAlias(anyLong(), anyLong(), eq(Bytes.wrap(EVM_ADDRESS.toArrayUnsafe())))) @@ -695,12 +700,14 @@ void returnsNullForMissingAlias() { @Test void missingAliasIsNotHollow() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); given(nativeOperations.configuration()).willReturn(configuration); assertFalse(subject.isHollowAccount(EVM_ADDRESS)); } @Test void missingAccountIsNotHollow() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); given(nativeOperations.resolveAlias( DEFAULT_HEDERA_CONFIG.shard(), DEFAULT_HEDERA_CONFIG.realm(), @@ -712,6 +719,7 @@ void missingAccountIsNotHollow() { @Test void extantAccountIsHollowOnlyIfHasAnEmptyKey() { + given(nativeOperations.entityIdFactory()).willReturn(entityIdFactory); given(nativeOperations.resolveAlias( DEFAULT_HEDERA_CONFIG.shard(), DEFAULT_HEDERA_CONFIG.realm(), @@ -723,6 +731,51 @@ void extantAccountIsHollowOnlyIfHasAnEmptyKey() { assertTrue(subject.isHollowAccount(EVM_ADDRESS)); } + @Test + void isHollowAccountLooksUpTheConfiguredShardAndRealm() { + final var nonZeroShard = 1L; + final var nonZeroRealm = 2L; + final var nonZeroConfig = HederaTestConfigBuilder.create() + .withValue("hedera.shard", nonZeroShard) + .withValue("hedera.realm", nonZeroRealm) + .getOrCreateConfig(); + final var shardedIdFactory = new FakeEntityIdFactoryImpl(nonZeroShard, nonZeroRealm); + final var shardedAccountId = shardedIdFactory.newAccountId(ACCOUNT_NUM); + + given(nativeOperations.entityIdFactory()).willReturn(shardedIdFactory); + given(nativeOperations.configuration()).willReturn(nonZeroConfig); + given(nativeOperations.resolveAlias(nonZeroShard, nonZeroRealm, Bytes.wrap(EVM_ADDRESS.toArrayUnsafe()))) + .willReturn(ACCOUNT_NUM); + // The hollow account lives at 1.2.; a lookup that assumed shard/realm 0 would miss it + givenWellKnownAccount( + shardedAccountId, + contractWith(shardedAccountId) + .key(Key.newBuilder().keyList(KeyList.DEFAULT).build())); + + assertTrue(subject.isHollowAccount(EVM_ADDRESS)); + } + + @Test + void lazyCreationDetectsExistingAccountInTheConfiguredShardAndRealm() { + final var nonZeroShard = 1L; + final var nonZeroRealm = 2L; + final var nonZeroConfig = HederaTestConfigBuilder.create() + .withValue("hedera.shard", nonZeroShard) + .withValue("hedera.realm", nonZeroRealm) + .getOrCreateConfig(); + final var shardedIdFactory = new FakeEntityIdFactoryImpl(nonZeroShard, nonZeroRealm); + final var shardedAccountId = shardedIdFactory.newAccountId(ACCOUNT_NUM); + + given(nativeOperations.entityIdFactory()).willReturn(shardedIdFactory); + given(nativeOperations.configuration()).willReturn(nonZeroConfig); + given(nativeOperations.resolveAlias(nonZeroShard, nonZeroRealm, Bytes.wrap(EVM_ADDRESS.toArrayUnsafe()))) + .willReturn(ACCOUNT_NUM); + // An unexpired account already occupies 1.2., so lazy creation must be refused + givenWellKnownAccount(shardedAccountId, contractWith(shardedAccountId)); + + assertThrows(IllegalArgumentException.class, () -> subject.tryLazyCreation(EVM_ADDRESS)); + } + @Test void usesResolvedNumberFromDispatch() { given(nativeOperations.resolveAlias( diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/StakingValidator.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/StakingValidator.java index 628feb53a072..8da08bcc502b 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/StakingValidator.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/StakingValidator.java @@ -2,6 +2,7 @@ package com.hedera.node.app.service.token.impl.validators; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_STAKING_ID; +import static com.hedera.node.app.service.token.api.AccountSummariesApi.SENTINEL_ACCOUNT_ID; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -124,8 +125,7 @@ private static void validateStakedId( private static boolean isValidStakingSentinel( @NonNull String stakedIdKind, @Nullable AccountID stakedAccountId, @Nullable Long stakedNodeId) { if (stakedIdKind.equals("STAKED_ACCOUNT_ID")) { - // current checking only account num since shard and realm are 0.0 - return requireNonNull(stakedAccountId).accountNumOrThrow() == 0; + return SENTINEL_ACCOUNT_ID.equals(requireNonNull(stakedAccountId)); } else if (stakedIdKind.equals("STAKED_NODE_ID")) { return requireNonNull(stakedNodeId) == -1; } else { diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java index e23d17adf1be..79316e1d2cd2 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoUpdateHandlerTest.java @@ -413,6 +413,30 @@ void sentinelValuesForStakedAccountNumberWorks() { assertNull(writableStore.get(updateAccountId).stakedAccountId()); } + @Test + void stakedAccountIdWithWrongShardRealmIsNotTreatedAsSentinel() { + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(configuration, 1)); + + // Only 0.0.0 is the reset-staking sentinel; a num-0 id in another shard/realm names a + // nonexistent account and must be rejected rather than persisted as the staking target + final var base = new CryptoUpdateBuilder().withStakedAccountId(0).build(); + final var txn = base.copyBuilder() + .cryptoUpdateAccount(base.cryptoUpdateAccountOrThrow() + .copyBuilder() + .stakedAccountId(AccountID.newBuilder() + .shardNum(9) + .realmNum(9) + .accountNum(0) + .build())) + .build(); + givenTxnWith(txn); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(INVALID_STAKING_ID)); + assertNull(writableStore.get(updateAccountId).stakedAccountId()); + } + @Test void sentinelValuesForStakedNodeNumberWorks() { given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(configuration, 1));