From ec8a65d7d60834932a345a59c1d2954787af5527 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 17 Oct 2025 17:24:30 +0300 Subject: [PATCH 01/13] initial commit Signed-off-by: ibankov --- .../main/proto/services/response_code.proto | 6 +++++ .../app/workflows/ingest/IngestChecker.java | 22 +++++++++++++++++++ .../workflows/query/QueryWorkflowImpl.java | 1 + .../hedera/services/bdd/spec/HapiSpec.java | 20 +++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/hapi/hedera-protobuf-java-api/src/main/proto/services/response_code.proto b/hapi/hedera-protobuf-java-api/src/main/proto/services/response_code.proto index 2d3b8649676b..c705afaf7404 100644 --- a/hapi/hedera-protobuf-java-api/src/main/proto/services/response_code.proto +++ b/hapi/hedera-protobuf-java-api/src/main/proto/services/response_code.proto @@ -1888,8 +1888,14 @@ enum ResponseCodeEnum { * The HookCall set in the transaction is invalid */ INVALID_HOOK_CALL = 522; + /** * Hooks are not supported to be used in TokenAirdrop transactions */ HOOKS_ARE_NOT_SUPPORTED_IN_AIRDROPS = 523; + + /** + * The node account submitting the transaction has zero balance + */ + NODE_ACCOUNT_HAS_ZERO_BALANCE = 524; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 245753faba64..64378a59e72d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -21,6 +21,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.HOOKS_NOT_ENABLED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NODE_ACCOUNT_HAS_ZERO_BALANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; import static com.hedera.hapi.node.base.ResponseCodeEnum.PLATFORM_NOT_ACTIVE; import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED; @@ -47,6 +48,7 @@ import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.hapi.utils.EthSigsUtils; import com.hedera.node.app.info.CurrentPlatformStatus; +import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.signature.DefaultKeyVerifier; import com.hedera.node.app.signature.ExpandedSignaturePair; import com.hedera.node.app.signature.SignatureExpander; @@ -229,6 +231,19 @@ public void verifyReadyForTransactions() throws PreCheckException { } } + public void verifyNodeAccountBalance(ReadableStoreFactory storeFactory, Account payerAccount) + throws PreCheckException { + final var accountStore = storeFactory.getStore(ReadableAccountStore.class); + final var nodeAccount = + accountStore.getAccountById(networkInfo.selfNodeInfo().accountId()); + if (nodeAccount == null) { + throw new PreCheckException(INVALID_NODE_ACCOUNT); + } + if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount)) { + throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); + } + } + /** * Runs all the ingest checks on a {@link Transaction} * @@ -317,6 +332,7 @@ private void runAllChecks( logger.warn("Payer account {} has no key, indicating a problem with state", txInfo.payerID()); throw new PreCheckException(UNAUTHORIZED); } + verifyNodeAccountBalance(storeFactory, payer); // 6. Verify payer's signatures verifyPayerSignature(txInfo, payer, configuration); @@ -554,4 +570,10 @@ private void verifyPayerSignature( throw new PreCheckException(INVALID_SIGNATURE); } } + + public static boolean isSystemAccount(@NonNull Account account) { + // return false; + requireNonNull(account); + return account.accountIdOrThrow().accountNumOrThrow() <= 1000L; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java index 5e57b41687f8..3ddd90abb205 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java @@ -229,6 +229,7 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe // This should never happen, because the account is checked in the pure checks throw new PreCheckException(PAYER_ACCOUNT_NOT_FOUND); } + ingestChecker.verifyNodeAccountBalance(storeFactory, payer); // 3.iv Calculate costs final var queryFees = handler.computeFees(context).totalFee(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java index aef54ab2fbac..95324c41bbd8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java @@ -27,6 +27,8 @@ import static com.hedera.services.bdd.spec.transactions.TxnUtils.turnLoggingOff; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingHbar; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.SysFileOverrideOp.Target.FEES; import static com.hedera.services.bdd.spec.utilops.SysFileOverrideOp.Target.THROTTLES; import static com.hedera.services.bdd.spec.utilops.UtilStateChange.createEthereumAccountForSpec; @@ -39,6 +41,7 @@ import static com.hedera.services.bdd.suites.HapiSuite.ETH_SUFFIX; import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.SECP_256K1_SOURCE_KEY; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; @@ -936,9 +939,26 @@ private boolean init() { status = ERROR; return false; } + fundNodeAccounts(); return true; } + private void fundNodeAccounts() { + targetNetwork.nodes().forEach(node -> { + final var nodeAccount = node.getAccountId(); + if (nodeAccount != null) { + allRunFor( + this, + cryptoTransfer(movingHbar(ONE_HUNDRED_HBARS) + .between(setup().defaultPayerName(), asAccountString(nodeAccount)))); + } + }); + } + + private String asAccountString(final com.hedera.hapi.node.base.AccountID accountID) { + return String.format("%d.%d.%d", accountID.shardNum(), accountID.realmNum(), accountID.accountNum()); + } + private void buildRemoteNetwork() { try { targetNetwork = RemoteNetworkFactory.newWithTargetFrom(hapiSetup.remoteNodesYmlLoc()); From 53964cb653874494776ba8195defbbd5434a8136 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 17 Oct 2025 17:36:06 +0300 Subject: [PATCH 02/13] fix Signed-off-by: ibankov --- .../java/com/hedera/node/app/workflows/ingest/IngestChecker.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 64378a59e72d..e611ce3e9e78 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -572,7 +572,6 @@ private void verifyPayerSignature( } public static boolean isSystemAccount(@NonNull Account account) { - // return false; requireNonNull(account); return account.accountIdOrThrow().accountNumOrThrow() <= 1000L; } From f91404ec1085119d144748d7d283271d95af02cc Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 21 Oct 2025 12:17:24 +0300 Subject: [PATCH 03/13] fixing tests Signed-off-by: ibankov --- .../com/hedera/node/app/fixtures/AppTestBase.java | 1 + .../com/hedera/services/bdd/spec/HapiSpec.java | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java index 3f02b2c4aa40..2cd5d314a733 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java @@ -177,6 +177,7 @@ public WritableStates getWritableStates(@NonNull String serviceName) { protected Account nodeSelfAccount = Account.newBuilder() .accountId(nodeSelfAccountId) + .tinybarBalance(100_000_000) .key(FAKE_ED25519_KEY_INFOS[0].publicKey()) .declineReward(true) .build(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java index 95324c41bbd8..451047f431b4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java @@ -22,6 +22,7 @@ import static com.hedera.services.bdd.spec.HapiSpecSetup.setupFrom; import static com.hedera.services.bdd.spec.infrastructure.HapiClients.clientsFor; import static com.hedera.services.bdd.spec.keys.DefaultKeyGen.DEFAULT_KEY_GEN; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.transactions.TxnUtils.doIfNotInterrupted; import static com.hedera.services.bdd.spec.transactions.TxnUtils.resourceAsString; import static com.hedera.services.bdd.spec.transactions.TxnUtils.turnLoggingOff; @@ -129,6 +130,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.LongFunction; @@ -945,12 +947,17 @@ private boolean init() { private void fundNodeAccounts() { targetNetwork.nodes().forEach(node -> { + final AtomicLong nodeBalance = new AtomicLong(0); final var nodeAccount = node.getAccountId(); if (nodeAccount != null) { - allRunFor( - this, - cryptoTransfer(movingHbar(ONE_HUNDRED_HBARS) - .between(setup().defaultPayerName(), asAccountString(nodeAccount)))); + allRunFor(this, getAccountBalance(asAccountString(nodeAccount)).exposingBalanceTo(nodeBalance::set)); + if (nodeBalance.get() <= 0) { + allRunFor( + this, + cryptoTransfer(movingHbar(ONE_HUNDRED_HBARS) + .between(setup().defaultPayerName(), asAccountString(nodeAccount))) + .memo("Initial funding for node account " + nodeAccount.accountNum())); + } } }); } From b59cd930bff7bcd9ec59cacbffa79b657c8e40d5 Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 21 Oct 2025 14:24:24 +0300 Subject: [PATCH 04/13] unit tests Signed-off-by: ibankov --- .../workflows/ingest/IngestCheckerTest.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java index 71870fc74aef..c5abd95f06c2 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java @@ -15,6 +15,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NODE_ACCOUNT_HAS_ZERO_BALANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; import static com.hedera.hapi.node.base.ResponseCodeEnum.PLATFORM_NOT_ACTIVE; import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED; @@ -57,17 +58,20 @@ import com.hedera.node.app.fixtures.AppTestBase; import com.hedera.node.app.info.CurrentPlatformStatus; import com.hedera.node.app.info.NodeInfoImpl; +import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.signature.SignatureExpander; import com.hedera.node.app.signature.SignatureVerificationFuture; import com.hedera.node.app.signature.SignatureVerifier; import com.hedera.node.app.spi.authorization.Authorizer; import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.info.NetworkInfo; import com.hedera.node.app.spi.info.NodeInfo; import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.workflows.InsufficientBalanceException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.state.DeduplicationCache; import com.hedera.node.app.state.recordcache.DeduplicationCacheImpl; +import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.throttle.SynchronizedThrottleAccumulator; import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.SolvencyPreCheck; @@ -300,6 +304,76 @@ void testRunAllChecksSuccessfully() throws Exception { verify(opWorkflowMetrics, never()).incrementThrottled(any()); } + @Nested + class IngestCheckerNodeAccountBalanceTest { + private IngestChecker subject; + private ReadableStoreFactory storeFactory; + private ReadableAccountStore accountStore; + private Account nodeAccount; + private Account payerAccount; + + @BeforeEach + void setUp() { + storeFactory = mock(ReadableStoreFactory.class); + accountStore = mock(ReadableAccountStore.class); + nodeAccount = mock(Account.class); + payerAccount = mock(Account.class); + var networkInfo = mock(NetworkInfo.class); + + // Setup node account id + var nodeAccountId = AccountID.newBuilder().accountNum(1234L).build(); + var nodeInfo = mock(NodeInfo.class); + when(nodeInfo.accountId()).thenReturn(nodeAccountId); + when(networkInfo.selfNodeInfo()).thenReturn(nodeInfo); + + subject = new IngestChecker( + networkInfo, + mock(CurrentPlatformStatus.class), + mock(BlockStreamManager.class), + mock(TransactionChecker.class), + mock(SolvencyPreCheck.class), + mock(SignatureExpander.class), + mock(SignatureVerifier.class), + mock(DeduplicationCache.class), + mock(TransactionDispatcher.class), + mock(FeeManager.class), + mock(Authorizer.class), + mock(SynchronizedThrottleAccumulator.class), + mock(java.time.InstantSource.class), + mock(OpWorkflowMetrics.class), + null); + when(storeFactory.getStore(ReadableAccountStore.class)).thenReturn(accountStore); + } + + @Test + void throwsIfNodeAccountNotFound() { + when(accountStore.getAccountById(any())).thenReturn(null); + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + .isInstanceOf(PreCheckException.class) + .hasFieldOrPropertyWithValue("responseCode", INVALID_NODE_ACCOUNT); + } + + @Test + void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { + when(accountStore.getAccountById(any())).thenReturn(nodeAccount); + when(nodeAccount.tinybarBalance()).thenReturn(0L); + // Simulate non-system account + when(payerAccount.accountIdOrThrow()) + .thenReturn(AccountID.newBuilder().accountNum(2000L).build()); + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + .isInstanceOf(PreCheckException.class) + .hasFieldOrPropertyWithValue("responseCode", NODE_ACCOUNT_HAS_ZERO_BALANCE); + } + + @Test + void succeedsIfNodeAccountHasBalance() { + when(accountStore.getAccountById(any())).thenReturn(nodeAccount); + when(nodeAccount.tinybarBalance()).thenReturn(100L); + assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + .doesNotThrowAnyException(); + } + } + @Nested @DisplayName("1. Check the syntax") class SyntaxCheckTests { From ec1675dd6dc744f5ef58c9fd19c983dc42a31145 Mon Sep 17 00:00:00 2001 From: ibankov Date: Wed, 22 Oct 2025 11:10:46 +0300 Subject: [PATCH 05/13] addressing comments Signed-off-by: ibankov --- .../node/app/workflows/ingest/IngestChecker.java | 11 ++++++++--- .../app/workflows/prehandle/PreHandleContextImpl.java | 7 +++++++ .../node/app/workflows/query/QueryWorkflowImpl.java | 2 +- .../node/app/workflows/ingest/IngestCheckerTest.java | 6 +++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index e611ce3e9e78..d499ae58aad5 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -73,6 +73,7 @@ import com.hedera.node.config.Utils; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.HooksConfig; +import com.hedera.node.config.data.LedgerConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.state.State; @@ -231,7 +232,8 @@ public void verifyReadyForTransactions() throws PreCheckException { } } - public void verifyNodeAccountBalance(ReadableStoreFactory storeFactory, Account payerAccount) + public void verifyNodeAccountBalance( + final ReadableStoreFactory storeFactory, final Account payerAccount, final Configuration configuration) throws PreCheckException { final var accountStore = storeFactory.getStore(ReadableAccountStore.class); final var nodeAccount = @@ -239,7 +241,10 @@ public void verifyNodeAccountBalance(ReadableStoreFactory storeFactory, Account if (nodeAccount == null) { throw new PreCheckException(INVALID_NODE_ACCOUNT); } - if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount)) { + final var lastReservedSystemEntity = + configuration.getConfigData(LedgerConfig.class).numReservedSystemEntities(); + if (nodeAccount.tinybarBalance() < 1 + && payerAccount.accountIdOrThrow().accountNum() > lastReservedSystemEntity) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } @@ -332,7 +337,7 @@ private void runAllChecks( logger.warn("Payer account {} has no key, indicating a problem with state", txInfo.payerID()); throw new PreCheckException(UNAUTHORIZED); } - verifyNodeAccountBalance(storeFactory, payer); + verifyNodeAccountBalance(storeFactory, payer, configuration); // 6. Verify payer's signatures verifyPayerSignature(txInfo, payer, configuration); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java index 2a841f262dfb..87ddd7b6493a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java @@ -520,6 +520,13 @@ public TransactionKeys allKeysForTransaction(@NonNull TransactionBody body, @Non return context; } + /** + * Gets the key for the given account. This doesn't work for aliases. + * + * @param accountID The ID of the account whose key is to be fetched + * @return The key for the account + * @throws PreCheckException if the account does not exist or is deleted + */ @Override public Key getAccountKey(@NonNull final AccountID accountID) throws PreCheckException { requireNonNull(accountID); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java index 3ddd90abb205..6d0e7dbfcb26 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java @@ -229,7 +229,7 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe // This should never happen, because the account is checked in the pure checks throw new PreCheckException(PAYER_ACCOUNT_NOT_FOUND); } - ingestChecker.verifyNodeAccountBalance(storeFactory, payer); + ingestChecker.verifyNodeAccountBalance(storeFactory, payer, configuration); // 3.iv Calculate costs final var queryFees = handler.computeFees(context).totalFee(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java index c5abd95f06c2..e02b56ad5f56 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java @@ -348,7 +348,7 @@ void setUp() { @Test void throwsIfNodeAccountNotFound() { when(accountStore.getAccountById(any())).thenReturn(null); - assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) .isInstanceOf(PreCheckException.class) .hasFieldOrPropertyWithValue("responseCode", INVALID_NODE_ACCOUNT); } @@ -360,7 +360,7 @@ void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { // Simulate non-system account when(payerAccount.accountIdOrThrow()) .thenReturn(AccountID.newBuilder().accountNum(2000L).build()); - assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) .isInstanceOf(PreCheckException.class) .hasFieldOrPropertyWithValue("responseCode", NODE_ACCOUNT_HAS_ZERO_BALANCE); } @@ -369,7 +369,7 @@ void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { void succeedsIfNodeAccountHasBalance() { when(accountStore.getAccountById(any())).thenReturn(nodeAccount); when(nodeAccount.tinybarBalance()).thenReturn(100L); - assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount)) + assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) .doesNotThrowAnyException(); } } From 0ba6dd18ea6a9f9f0a7c2feec27d19de50e880ee Mon Sep 17 00:00:00 2001 From: ibankov Date: Wed, 22 Oct 2025 11:19:15 +0300 Subject: [PATCH 06/13] addressing comments Signed-off-by: ibankov --- .../hedera/node/app/workflows/ingest/IngestChecker.java | 7 +------ .../com/hedera/services/bdd/spec/HapiPropertySource.java | 4 ++++ .../main/java/com/hedera/services/bdd/spec/HapiSpec.java | 5 +---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index d499ae58aad5..6be0093d245c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -244,7 +244,7 @@ public void verifyNodeAccountBalance( final var lastReservedSystemEntity = configuration.getConfigData(LedgerConfig.class).numReservedSystemEntities(); if (nodeAccount.tinybarBalance() < 1 - && payerAccount.accountIdOrThrow().accountNum() > lastReservedSystemEntity) { + && payerAccount.accountIdOrThrow().accountNumOrThrow() > lastReservedSystemEntity) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } @@ -575,9 +575,4 @@ private void verifyPayerSignature( throw new PreCheckException(INVALID_SIGNATURE); } } - - public static boolean isSystemAccount(@NonNull Account account) { - requireNonNull(account); - return account.accountIdOrThrow().accountNumOrThrow() <= 1000L; - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java index f174f66a9c6a..068acb9fd98b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java @@ -372,6 +372,10 @@ static String asAccountString(AccountID account) { return asEntityString(account.getShardNum(), account.getRealmNum(), account.getAccountNum()); } + static String asAccountString(final com.hedera.hapi.node.base.AccountID accountID) { + return asEntityString(accountID.shardNum(), accountID.realmNum(), accountID.accountNumOrThrow()); + } + static String asAliasableAccountString(final AccountID account) { if (account.getAlias().isEmpty()) { return asAccountString(account); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java index 451047f431b4..504d3e25bc06 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java @@ -11,6 +11,7 @@ import static com.hedera.services.bdd.junit.hedera.BlockNodeNetwork.BLOCK_NODE_LOCAL_PORT; import static com.hedera.services.bdd.junit.hedera.ExternalPath.RECORD_STREAMS_DIR; import static com.hedera.services.bdd.junit.support.StreamFileAccess.STREAM_FILE_ACCESS; +import static com.hedera.services.bdd.spec.HapiPropertySource.asAccountString; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.ERROR; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.FAILED; import static com.hedera.services.bdd.spec.HapiSpec.SpecStatus.FAILED_AS_EXPECTED; @@ -962,10 +963,6 @@ private void fundNodeAccounts() { }); } - private String asAccountString(final com.hedera.hapi.node.base.AccountID accountID) { - return String.format("%d.%d.%d", accountID.shardNum(), accountID.realmNum(), accountID.accountNum()); - } - private void buildRemoteNetwork() { try { targetNetwork = RemoteNetworkFactory.newWithTargetFrom(hapiSetup.remoteNodesYmlLoc()); From 7b0c74fae15df06d6844a22828e91010e38ff752 Mon Sep 17 00:00:00 2001 From: ibankov Date: Wed, 22 Oct 2025 15:23:09 +0300 Subject: [PATCH 07/13] fix Signed-off-by: ibankov --- .../node/app/workflows/ingest/IngestChecker.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 6be0093d245c..2816a7fe2759 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -73,7 +73,6 @@ import com.hedera.node.config.Utils; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.HooksConfig; -import com.hedera.node.config.data.LedgerConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.state.State; @@ -235,16 +234,14 @@ public void verifyReadyForTransactions() throws PreCheckException { public void verifyNodeAccountBalance( final ReadableStoreFactory storeFactory, final Account payerAccount, final Configuration configuration) throws PreCheckException { + final var hederaConfig = configuration.getConfigData(HederaConfig.class); final var accountStore = storeFactory.getStore(ReadableAccountStore.class); final var nodeAccount = accountStore.getAccountById(networkInfo.selfNodeInfo().accountId()); if (nodeAccount == null) { throw new PreCheckException(INVALID_NODE_ACCOUNT); } - final var lastReservedSystemEntity = - configuration.getConfigData(LedgerConfig.class).numReservedSystemEntities(); - if (nodeAccount.tinybarBalance() < 1 - && payerAccount.accountIdOrThrow().accountNumOrThrow() > lastReservedSystemEntity) { + if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount, hederaConfig.firstUserEntity())) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } @@ -575,4 +572,8 @@ private void verifyPayerSignature( throw new PreCheckException(INVALID_SIGNATURE); } } + + private boolean isSystemAccount(final Account account, final long systemAccountThreshold) { + return account.accountIdOrThrow().accountNumOrThrow() < systemAccountThreshold; + } } From d80b4341e13016a4d313f0e40a4802c1d06615ff Mon Sep 17 00:00:00 2001 From: ibankov Date: Wed, 22 Oct 2025 18:34:05 +0300 Subject: [PATCH 08/13] fix Signed-off-by: ibankov --- .../com/hedera/node/app/workflows/ingest/IngestChecker.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 2816a7fe2759..2a8b099f9a92 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -73,6 +73,7 @@ import com.hedera.node.config.Utils; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.HooksConfig; +import com.hedera.node.config.data.LedgerConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.state.State; @@ -234,14 +235,14 @@ public void verifyReadyForTransactions() throws PreCheckException { public void verifyNodeAccountBalance( final ReadableStoreFactory storeFactory, final Account payerAccount, final Configuration configuration) throws PreCheckException { - final var hederaConfig = configuration.getConfigData(HederaConfig.class); + final var ledgerConfig = configuration.getConfigData(LedgerConfig.class); final var accountStore = storeFactory.getStore(ReadableAccountStore.class); final var nodeAccount = accountStore.getAccountById(networkInfo.selfNodeInfo().accountId()); if (nodeAccount == null) { throw new PreCheckException(INVALID_NODE_ACCOUNT); } - if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount, hederaConfig.firstUserEntity())) { + if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount, ledgerConfig.numSystemAccounts())) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } From 62f3d6b19188eba9c8d6edfc4957da62b363871e Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 23 Oct 2025 10:31:57 +0300 Subject: [PATCH 09/13] update account balance verification to use AccountsConfig Signed-off-by: ibankov --- .../node/app/workflows/ingest/IngestChecker.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 2a8b099f9a92..d5f479625dbf 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -71,9 +71,9 @@ import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; import com.hedera.node.app.workflows.purechecks.PureChecksContextImpl; import com.hedera.node.config.Utils; +import com.hedera.node.config.data.AccountsConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.HooksConfig; -import com.hedera.node.config.data.LedgerConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.state.State; @@ -235,14 +235,14 @@ public void verifyReadyForTransactions() throws PreCheckException { public void verifyNodeAccountBalance( final ReadableStoreFactory storeFactory, final Account payerAccount, final Configuration configuration) throws PreCheckException { - final var ledgerConfig = configuration.getConfigData(LedgerConfig.class); + final var accountsConfig = configuration.getConfigData(AccountsConfig.class); final var accountStore = storeFactory.getStore(ReadableAccountStore.class); final var nodeAccount = accountStore.getAccountById(networkInfo.selfNodeInfo().accountId()); if (nodeAccount == null) { throw new PreCheckException(INVALID_NODE_ACCOUNT); } - if (nodeAccount.tinybarBalance() < 1 && !isSystemAccount(payerAccount, ledgerConfig.numSystemAccounts())) { + if (nodeAccount.tinybarBalance() < 1 && !accountsConfig.isSuperuser(payerAccount.accountIdOrThrow())) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } @@ -573,8 +573,4 @@ private void verifyPayerSignature( throw new PreCheckException(INVALID_SIGNATURE); } } - - private boolean isSystemAccount(final Account account, final long systemAccountThreshold) { - return account.accountIdOrThrow().accountNumOrThrow() < systemAccountThreshold; - } } From 0f446ea4902f8d767de9f1cb3c266ee17484d071 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 24 Oct 2025 12:01:56 +0300 Subject: [PATCH 10/13] use authorizer Signed-off-by: ibankov --- .../app/workflows/ingest/IngestChecker.java | 24 ++++++++++++------- .../workflows/query/QueryWorkflowImpl.java | 2 +- .../workflows/ingest/IngestCheckerTest.java | 12 +++++----- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index d5f479625dbf..0ce6a629f6af 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -54,6 +54,7 @@ import com.hedera.node.app.signature.SignatureExpander; import com.hedera.node.app.signature.SignatureVerifier; import com.hedera.node.app.spi.authorization.Authorizer; +import com.hedera.node.app.spi.authorization.SystemPrivilege; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.info.NetworkInfo; import com.hedera.node.app.spi.signatures.SignatureVerification; @@ -71,7 +72,6 @@ import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; import com.hedera.node.app.workflows.purechecks.PureChecksContextImpl; import com.hedera.node.config.Utils; -import com.hedera.node.config.data.AccountsConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.HooksConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -232,17 +232,23 @@ public void verifyReadyForTransactions() throws PreCheckException { } } - public void verifyNodeAccountBalance( - final ReadableStoreFactory storeFactory, final Account payerAccount, final Configuration configuration) + public void verifyNodeAccountBalance(final ReadableStoreFactory storeFactory, final TransactionInfo txInfo) throws PreCheckException { - final var accountsConfig = configuration.getConfigData(AccountsConfig.class); final var accountStore = storeFactory.getStore(ReadableAccountStore.class); - final var nodeAccount = - accountStore.getAccountById(networkInfo.selfNodeInfo().accountId()); - if (nodeAccount == null) { + final var selfAccountId = networkInfo.selfNodeInfo().accountId(); + final var selfAccount = accountStore.getAccountById(selfAccountId); + + if (selfAccount == null) { throw new PreCheckException(INVALID_NODE_ACCOUNT); } - if (nodeAccount.tinybarBalance() < 1 && !accountsConfig.isSuperuser(payerAccount.accountIdOrThrow())) { + + final var nodeBalance = selfAccount.tinybarBalance(); + final var isPrivilegedAuthorized = + authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()) + == SystemPrivilege.AUTHORIZED; + + // Check node account balance and authorization + if (nodeBalance < 1 && !isPrivilegedAuthorized) { throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE); } } @@ -335,7 +341,7 @@ private void runAllChecks( logger.warn("Payer account {} has no key, indicating a problem with state", txInfo.payerID()); throw new PreCheckException(UNAUTHORIZED); } - verifyNodeAccountBalance(storeFactory, payer, configuration); + verifyNodeAccountBalance(storeFactory, txInfo); // 6. Verify payer's signatures verifyPayerSignature(txInfo, payer, configuration); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java index 6d0e7dbfcb26..262a64bf1583 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java @@ -229,7 +229,7 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe // This should never happen, because the account is checked in the pure checks throw new PreCheckException(PAYER_ACCOUNT_NOT_FOUND); } - ingestChecker.verifyNodeAccountBalance(storeFactory, payer, configuration); + ingestChecker.verifyNodeAccountBalance(storeFactory, txInfo); // 3.iv Calculate costs final var queryFees = handler.computeFees(context).totalFee(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java index e02b56ad5f56..88379e581e56 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java @@ -63,6 +63,7 @@ import com.hedera.node.app.signature.SignatureVerificationFuture; import com.hedera.node.app.signature.SignatureVerifier; import com.hedera.node.app.spi.authorization.Authorizer; +import com.hedera.node.app.spi.authorization.SystemPrivilege; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.info.NetworkInfo; import com.hedera.node.app.spi.info.NodeInfo; @@ -337,7 +338,7 @@ void setUp() { mock(DeduplicationCache.class), mock(TransactionDispatcher.class), mock(FeeManager.class), - mock(Authorizer.class), + authorizer, mock(SynchronizedThrottleAccumulator.class), mock(java.time.InstantSource.class), mock(OpWorkflowMetrics.class), @@ -348,7 +349,7 @@ void setUp() { @Test void throwsIfNodeAccountNotFound() { when(accountStore.getAccountById(any())).thenReturn(null); - assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, transactionInfo)) .isInstanceOf(PreCheckException.class) .hasFieldOrPropertyWithValue("responseCode", INVALID_NODE_ACCOUNT); } @@ -358,9 +359,8 @@ void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { when(accountStore.getAccountById(any())).thenReturn(nodeAccount); when(nodeAccount.tinybarBalance()).thenReturn(0L); // Simulate non-system account - when(payerAccount.accountIdOrThrow()) - .thenReturn(AccountID.newBuilder().accountNum(2000L).build()); - assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) + when(authorizer.hasPrivilegedAuthorization(any(), any(), any())).thenReturn(SystemPrivilege.UNAUTHORIZED); + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, transactionInfo)) .isInstanceOf(PreCheckException.class) .hasFieldOrPropertyWithValue("responseCode", NODE_ACCOUNT_HAS_ZERO_BALANCE); } @@ -369,7 +369,7 @@ void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { void succeedsIfNodeAccountHasBalance() { when(accountStore.getAccountById(any())).thenReturn(nodeAccount); when(nodeAccount.tinybarBalance()).thenReturn(100L); - assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, payerAccount, configuration)) + assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, transactionInfo)) .doesNotThrowAnyException(); } } From 161e20259ce0ddd52718ee61e19733e5ecea93cb Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 24 Oct 2025 13:40:09 +0300 Subject: [PATCH 11/13] fix Signed-off-by: ibankov --- .../com/hedera/node/app/workflows/ingest/IngestChecker.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 0ce6a629f6af..f6fc0bc1a21d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -243,9 +243,10 @@ public void verifyNodeAccountBalance(final ReadableStoreFactory storeFactory, fi } final var nodeBalance = selfAccount.tinybarBalance(); + final var privilegesResult = + authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()); final var isPrivilegedAuthorized = - authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()) - == SystemPrivilege.AUTHORIZED; + privilegesResult == SystemPrivilege.AUTHORIZED || privilegesResult == SystemPrivilege.UNNECESSARY; // Check node account balance and authorization if (nodeBalance < 1 && !isPrivilegedAuthorized) { From e7f55040fd74fb908d2d5fb848154dfc03efcb30 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 24 Oct 2025 13:48:25 +0300 Subject: [PATCH 12/13] wip Signed-off-by: ibankov --- .../hedera/node/app/authorization/PrivilegesVerifier.java | 2 +- .../com/hedera/node/app/workflows/ingest/IngestChecker.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java index de6fd8266248..20240936be34 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java @@ -85,7 +85,7 @@ public SystemPrivilege hasPrivileges( checkCryptoDelete( effectiveNumber(txBody.cryptoDeleteOrThrow().deleteAccountIDOrElse(AccountID.DEFAULT))); case NODE_CREATE -> checkNodeCreate(payerId); - default -> SystemPrivilege.UNNECESSARY; + default -> isSuperUser(payerId) ? AUTHORIZED : SystemPrivilege.UNNECESSARY; }; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index f6fc0bc1a21d..0ce6a629f6af 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -243,10 +243,9 @@ public void verifyNodeAccountBalance(final ReadableStoreFactory storeFactory, fi } final var nodeBalance = selfAccount.tinybarBalance(); - final var privilegesResult = - authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()); final var isPrivilegedAuthorized = - privilegesResult == SystemPrivilege.AUTHORIZED || privilegesResult == SystemPrivilege.UNNECESSARY; + authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()) + == SystemPrivilege.AUTHORIZED; // Check node account balance and authorization if (nodeBalance < 1 && !isPrivilegedAuthorized) { From 906b10d731b043e06af8110d6e45b08fb053ef10 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 24 Oct 2025 14:50:58 +0300 Subject: [PATCH 13/13] fix tests Signed-off-by: ibankov --- .../app/authorization/PrivilegesVerifier.java | 2 +- .../node/app/workflows/ingest/IngestChecker.java | 10 +++++++--- .../app/workflows/ingest/IngestCheckerTest.java | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java index 20240936be34..de6fd8266248 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java @@ -85,7 +85,7 @@ public SystemPrivilege hasPrivileges( checkCryptoDelete( effectiveNumber(txBody.cryptoDeleteOrThrow().deleteAccountIDOrElse(AccountID.DEFAULT))); case NODE_CREATE -> checkNodeCreate(payerId); - default -> isSuperUser(payerId) ? AUTHORIZED : SystemPrivilege.UNNECESSARY; + default -> SystemPrivilege.UNNECESSARY; }; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java index 0ce6a629f6af..9a96d6b7df87 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ingest/IngestChecker.java @@ -243,9 +243,13 @@ public void verifyNodeAccountBalance(final ReadableStoreFactory storeFactory, fi } final var nodeBalance = selfAccount.tinybarBalance(); - final var isPrivilegedAuthorized = - authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()) - == SystemPrivilege.AUTHORIZED; + final var authorization = + authorizer.hasPrivilegedAuthorization(txInfo.payerID(), txInfo.functionality(), txInfo.txBody()); + + // if privileged authorization is UNNECESSARY check for superuser else check specific admin accounts + final var isPrivilegedAuthorized = authorization == SystemPrivilege.UNNECESSARY + ? authorizer.isSuperUser(txInfo.payerID()) + : authorization == SystemPrivilege.AUTHORIZED; // Check node account balance and authorization if (nodeBalance < 1 && !isPrivilegedAuthorized) { diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java index 88379e581e56..1334fe044e35 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/ingest/IngestCheckerTest.java @@ -365,6 +365,21 @@ void throwsIfNodeAccountHasZeroBalanceAndPayerIsNotSystemAccount() { .hasFieldOrPropertyWithValue("responseCode", NODE_ACCOUNT_HAS_ZERO_BALANCE); } + @Test + void validateBehaviorWhenAuthorizerReturnsUnnecessary() { + when(accountStore.getAccountById(any())).thenReturn(nodeAccount); + when(nodeAccount.tinybarBalance()).thenReturn(0L); + // Simulate non-system account + when(authorizer.hasPrivilegedAuthorization(any(), any(), any())).thenReturn(SystemPrivilege.UNNECESSARY); + when(authorizer.isSuperUser(any())).thenReturn(true); + assertThatCode(() -> subject.verifyNodeAccountBalance(storeFactory, transactionInfo)) + .doesNotThrowAnyException(); + when(authorizer.isSuperUser(any())).thenReturn(false); + assertThatThrownBy(() -> subject.verifyNodeAccountBalance(storeFactory, transactionInfo)) + .isInstanceOf(PreCheckException.class) + .hasFieldOrPropertyWithValue("responseCode", NODE_ACCOUNT_HAS_ZERO_BALANCE); + } + @Test void succeedsIfNodeAccountHasBalance() { when(accountStore.getAccountById(any())).thenReturn(nodeAccount);