Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -71,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;
Expand Down Expand Up @@ -229,6 +232,21 @@ 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 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())) {
throw new PreCheckException(NODE_ACCOUNT_HAS_ZERO_BALANCE);
}
}

/**
* Runs all the ingest checks on a {@link Transaction}
*
Expand Down Expand Up @@ -317,6 +335,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);

// 6. Verify payer's signatures
verifyPayerSignature(txInfo, payer, configuration);
Expand Down Expand Up @@ -554,4 +573,8 @@ private void verifyPayerSignature(
throw new PreCheckException(INVALID_SIGNATURE);
}
}

private boolean isSystemAccount(final Account account, final long systemAccountThreshold) {
return account.accountIdOrThrow().accountNumOrThrow() < systemAccountThreshold;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, configuration);

// 3.iv Calculate costs
final var queryFees = handler.computeFees(context).totalFee();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, configuration))
.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, configuration))
.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, configuration))
.doesNotThrowAnyException();
}
}

@Nested
@DisplayName("1. Check the syntax")
class SyntaxCheckTests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,11 +23,14 @@
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;
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;
Expand All @@ -39,6 +43,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;
Expand Down Expand Up @@ -126,6 +131,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;
Expand Down Expand Up @@ -936,9 +942,27 @@ private boolean init() {
status = ERROR;
return false;
}
fundNodeAccounts();
return true;
}

private void fundNodeAccounts() {
targetNetwork.nodes().forEach(node -> {
final AtomicLong nodeBalance = new AtomicLong(0);
final var nodeAccount = node.getAccountId();
if (nodeAccount != null) {
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()));
}
}
});
}

private void buildRemoteNetwork() {
try {
targetNetwork = RemoteNetworkFactory.newWithTargetFrom(hapiSetup.remoteNodesYmlLoc());
Expand Down
Loading