Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -470,15 +470,14 @@ private Optional<ExceptionalHaltReason> 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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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()))))
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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.<num>; 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.<num>, so lazy creation must be refused
givenWellKnownAccount(shardedAccountId, contractWith(shardedAccountId));

assertThrows(IllegalArgumentException.class, () -> subject.tryLazyCreation(EVM_ADDRESS));
}

@Test
void usesResolvedNumberFromDispatch() {
given(nativeOperations.resolveAlias(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading