From f72fe739d9573a3d228a91980fe941eb74fa2ec9 Mon Sep 17 00:00:00 2001 From: Steven Sheehy <17552371+steven-sheehy@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:46:23 -0500 Subject: [PATCH] Calculate acceptance test operator balance from USD (#8957) * Add a feature flag for rejectToken so mirror node can deploy before services 0.52 * Change `operatorBalance` property to store amount in USD * Change operator initial balance to convert USD into tinybar using `transactionReceipt.ExchangeRate` or falling back to `/api/v1/network/exchangerate` * Fix assertion not printing out correct contract call response * Fix regression caused by null immutable key * Fix startup probe topic not being deleted * Fix startup probe taking forever to recover when all nodes are down Signed-off-by: Steven Sheehy --- hedera-mirror-test/README.md | 3 +- .../test/e2e/acceptance/client/SDKClient.java | 59 ++++++++++++++----- .../e2e/acceptance/client/StartupProbe.java | 26 ++++++-- .../config/AcceptanceTestProperties.java | 10 +++- .../acceptance/config/FeatureProperties.java | 2 + .../e2e/acceptance/steps/ContractFeature.java | 2 +- .../e2e/acceptance/steps/TokenFeature.java | 18 ++++++ .../util/ContractCallResponseWrapper.java | 8 ++- 8 files changed, 102 insertions(+), 26 deletions(-) diff --git a/hedera-mirror-test/README.md b/hedera-mirror-test/README.md index 47124c26695..a859f3fe902 100644 --- a/hedera-mirror-test/README.md +++ b/hedera-mirror-test/README.md @@ -44,6 +44,7 @@ include: | `hedera.mirror.test.acceptance.createOperatorAccount` | true | Whether to create a separate operator account to run the acceptance tests. | | `hedera.mirror.test.acceptance.emitBackgroundMessages` | false | Whether background topic messages should be emitted. | | `hedera.mirror.test.acceptance.feature.maxContractFunctionGas` | 3000000 | The maximum amount of gas an account is willing to pay for a contract call. | +| `hedera.mirror.test.acceptance.feature.rejectToken` | true | Whether the RejectToken functionality should be verified or not. | | `hedera.mirror.test.acceptance.feature.sidecars` | false | Whether information in sidecars should be used to verify test scenarios. | | `hedera.mirror.test.acceptance.maxNodes` | 10 | The maximum number of nodes to validate from the address book. | | `hedera.mirror.test.acceptance.maxRetries` | 2 | The number of times client should retry mirror node on supported failures. | @@ -52,7 +53,7 @@ include: | `hedera.mirror.test.acceptance.mirrorNodeAddress` | testnet.mirrornode.hedera.com:443 | The mirror node gRPC server endpoint including IP address and port. | | `hedera.mirror.test.acceptance.network` | TESTNET | Which Hedera network to use. Can be either `MAINNET`, `PREVIEWNET`, `TESTNET` or `OTHER`. | | `hedera.mirror.test.acceptance.nodes` | [] | A map of custom consensus node with the key being the account ID and the value its endpoint. | -| `hedera.mirror.test.acceptance.operatorBalance` | 80000000000 | The amount of tinybars to fund the operator. Applicable only when `createOperatorAccount` is `true`. | +| `hedera.mirror.test.acceptance.operatorBalance` | 60 | The amount of dollars to fund the operator. Applicable only when `createOperatorAccount` is `true`. | | `hedera.mirror.test.acceptance.operatorId` | 0.0.2 | Operator account ID used to pay for transactions. | | `hedera.mirror.test.acceptance.operatorKey` | Genesis key | Operator ED25519 or ECDSA private key used to sign transactions in hex encoded DER format. | | `hedera.mirror.test.acceptance.rest.baseUrl` | https://testnet.mirrornode.hedera.com/api/v1 | The URL to the mirror node REST API. | diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/SDKClient.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/SDKClient.java index 2013a4d7de0..0a69fc6b518 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/SDKClient.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/SDKClient.java @@ -24,15 +24,16 @@ import com.hedera.hashgraph.sdk.AccountDeleteTransaction; import com.hedera.hashgraph.sdk.AccountId; import com.hedera.hashgraph.sdk.Client; -import com.hedera.hashgraph.sdk.EvmAddress; import com.hedera.hashgraph.sdk.Hbar; -import com.hedera.hashgraph.sdk.PrivateKey; -import com.hedera.hashgraph.sdk.PublicKey; +import com.hedera.hashgraph.sdk.TopicDeleteTransaction; +import com.hedera.hashgraph.sdk.TopicId; +import com.hedera.hashgraph.sdk.TransactionReceipt; import com.hedera.mirror.test.e2e.acceptance.config.AcceptanceTestProperties; import com.hedera.mirror.test.e2e.acceptance.config.SdkProperties; import com.hedera.mirror.test.e2e.acceptance.props.ExpandedAccountId; import com.hedera.mirror.test.e2e.acceptance.props.NodeProperties; import jakarta.inject.Named; +import java.math.BigDecimal; import java.security.SecureRandom; import java.time.Duration; import java.util.ArrayList; @@ -61,6 +62,7 @@ public class SDKClient implements Cleanable { private final AcceptanceTestProperties acceptanceTestProperties; private final SdkProperties sdkProperties; private final MirrorNodeClient mirrorNodeClient; + private final TopicId topicId; @Getter private final ExpandedAccountId expandedOperatorAccountId; @@ -81,9 +83,10 @@ public SDKClient( .setMaxAttempts(sdkProperties.getMaxAttempts()) .setMaxNodeReadmitTime(Duration.ofSeconds(60L)) .setMaxNodesPerTransaction(sdkProperties.getMaxNodesPerTransaction()); - startupProbe.validateEnvironment(client); + var receipt = startupProbe.validateEnvironment(client); + this.topicId = receipt != null ? receipt.topicId : null; validateClient(); - expandedOperatorAccountId = getOperatorAccount(); + expandedOperatorAccountId = getOperatorAccount(receipt); this.client.setOperator(expandedOperatorAccountId.getAccountId(), expandedOperatorAccountId.getPrivateKey()); validateNetworkMap = this.client.getNetwork(); } @@ -98,6 +101,19 @@ public void clean() { var createdAccountId = expandedOperatorAccountId.getAccountId(); var operatorId = defaultOperator.getAccountId(); + if (topicId != null) { + try { + var response = new TopicDeleteTransaction() + .setTopicId(topicId) + .freezeWith(client) + .sign(defaultOperator.getPrivateKey()) + .execute(client); + log.info("Deleted startup probe topic {} via {}", topicId, response.transactionId); + } catch (Exception e) { + log.warn("Unable to delete startup probe topic {}", topicId, e); + } + } + if (!operatorId.equals(createdAccountId)) { try { var response = new AccountDeleteTransaction() @@ -152,20 +168,35 @@ private Map getNetworkMap(Set nodes) { .collect(Collectors.toMap(NodeProperties::getEndpoint, p -> AccountId.fromString(p.getAccountId()))); } - private ExpandedAccountId getOperatorAccount() { + private double getExchangeRate(TransactionReceipt receipt) { + if (receipt == null || receipt.exchangeRate == null) { + var currentRate = mirrorNodeClient.getExchangeRates().getCurrentRate(); + int cents = currentRate.getCentEquivalent(); + int hbars = currentRate.getHbarEquivalent(); + return (double) cents / (double) hbars; + } else { + return receipt.exchangeRate.exchangeRateInCents; + } + } + + private ExpandedAccountId getOperatorAccount(TransactionReceipt receipt) { try { if (acceptanceTestProperties.isCreateOperatorAccount()) { // Use the same operator key in case we need to later manually update/delete any created entities. - PrivateKey privateKey = defaultOperator.getPrivateKey(); - PublicKey publicKey = privateKey.getPublicKey(); - EvmAddress alias = null; - if (privateKey.isECDSA()) { - alias = publicKey.toEvmAddress(); - } + var privateKey = defaultOperator.getPrivateKey(); + var publicKey = privateKey.getPublicKey(); + var alias = privateKey.isECDSA() ? publicKey.toEvmAddress() : null; + + // Convert USD balance property to hbars using exchange rate from probe + double exchangeRate = getExchangeRate(receipt); + var exchangeRateUsd = BigDecimal.valueOf(exchangeRate).divide(BigDecimal.valueOf(100)); + var balance = + Hbar.from(acceptanceTestProperties.getOperatorBalance().divide(exchangeRateUsd)); + var accountId = new AccountCreateTransaction() - .setInitialBalance(Hbar.fromTinybars(acceptanceTestProperties.getOperatorBalance())) - .setKey(publicKey) .setAlias(alias) + .setInitialBalance(balance) + .setKey(publicKey) .execute(client) .getReceipt(client) .accountId; diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/StartupProbe.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/StartupProbe.java index 4bf0348ab6e..8f5cd93b88b 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/StartupProbe.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/client/StartupProbe.java @@ -26,6 +26,7 @@ import com.hedera.hashgraph.sdk.TopicMessageSubmitTransaction; import com.hedera.hashgraph.sdk.Transaction; import com.hedera.hashgraph.sdk.TransactionId; +import com.hedera.hashgraph.sdk.TransactionReceipt; import com.hedera.hashgraph.sdk.TransactionReceiptQuery; import com.hedera.hashgraph.sdk.TransactionResponse; import com.hedera.mirror.test.e2e.acceptance.config.AcceptanceTestProperties; @@ -55,22 +56,30 @@ @RequiredArgsConstructor public class StartupProbe { + private static final Duration WAIT = Duration.ofSeconds(30L); + private final AcceptanceTestProperties acceptanceTestProperties; private final RestClient.Builder restClient; - public void validateEnvironment(Client client) { + public TransactionReceipt validateEnvironment(Client client) { var startupTimeout = acceptanceTestProperties.getStartupTimeout(); var stopwatch = Stopwatch.createStarted(); if (startupTimeout.equals(Duration.ZERO)) { log.warn("Startup probe disabled"); - return; + return null; } + // Adjust these lower to recover faster since all nodes might be down during a reset. Restore at the end. + var maxNodeBackoff = client.getNodeMaxBackoff(); + client.setNodeMaxBackoff(WAIT); + client.setMaxNodeReadmitTime(WAIT); + log.info("Creating a topic to confirm node connectivity"); var transactionId = executeTransaction(client, stopwatch, () -> new TopicCreateTransaction()).transactionId; var receiptQuery = new TransactionReceiptQuery().setTransactionId(transactionId); - var topicId = executeQuery(client, stopwatch, () -> receiptQuery).topicId; + var receipt = executeQuery(client, stopwatch, () -> receiptQuery); + var topicId = receipt.topicId; log.info("Created topic {} successfully", topicId); callRestEndpoint(stopwatch, transactionId); @@ -82,7 +91,11 @@ public void validateEnvironment(Client client) { submitMessage(client, stopwatch, topicId); } while (System.currentTimeMillis() - startTime > 10_000); + client.setNodeMaxBackoff(maxNodeBackoff); + client.setMaxNodeReadmitTime(maxNodeBackoff); + log.info("Startup probe successful"); + return receipt; } @SneakyThrows @@ -130,14 +143,14 @@ private TransactionResponse executeTransaction( Client client, Stopwatch stopwatch, Supplier> transaction) { var retry = retryOperations(stopwatch); return retry.execute( - r -> transaction.get().setMaxAttempts(Integer.MAX_VALUE).execute(client, Duration.ofSeconds(30L))); + r -> transaction.get().setMaxAttempts(Integer.MAX_VALUE).execute(client, WAIT)); } @SneakyThrows private T executeQuery(Client client, Stopwatch stopwatch, Supplier> transaction) { var retry = retryOperations(stopwatch); return retry.execute( - r -> transaction.get().setMaxAttempts(Integer.MAX_VALUE).execute(client, Duration.ofSeconds(30L))); + r -> transaction.get().setMaxAttempts(Integer.MAX_VALUE).execute(client, WAIT)); } private void callRestEndpoint(Stopwatch stopwatch, TransactionId transactionId) { @@ -172,7 +185,8 @@ private RetryOperations retryOperations(Stopwatch stopwatch) { .withListener(new RetryListener() { @Override public void onError(RetryContext r, RetryCallback c, Throwable t) { - log.warn("Retry attempt #{} with error: {}", r.getRetryCount(), t.getMessage()); + log.warn( + "Retry attempt #{} with error: {} {}", r.getRetryCount(), t.getClass(), t.getMessage()); } }) .build(); diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/AcceptanceTestProperties.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/AcceptanceTestProperties.java index 030c43d4666..083fedf83cd 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/AcceptanceTestProperties.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/AcceptanceTestProperties.java @@ -20,10 +20,13 @@ import com.hedera.mirror.test.e2e.acceptance.client.ContractClient.NodeNameEnum; import com.hedera.mirror.test.e2e.acceptance.props.NodeProperties; import jakarta.inject.Named; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; import java.time.Duration; import java.util.LinkedHashSet; import java.util.Set; @@ -75,9 +78,10 @@ public class AcceptanceTestProperties { @NotNull private Set nodes = new LinkedHashSet<>(); - @Max(50_000_000_000L * 100_000_000L) - @Min(100_000_000L) - private long operatorBalance = Hbar.from(800).toTinybars(); + @NotNull + @DecimalMax("1000000") + @DecimalMin("1.0") + private BigDecimal operatorBalance = BigDecimal.valueOf(60); // Amount in USD @NotBlank private String operatorId = "0.0.2"; diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/FeatureProperties.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/FeatureProperties.java index 4db7d161f77..2cf75eb74f6 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/FeatureProperties.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/config/FeatureProperties.java @@ -36,5 +36,7 @@ public class FeatureProperties { @Max(5_000_000) private long maxContractFunctionGas = 3_000_000; + private boolean rejectToken = true; + private boolean sidecars = false; } diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/ContractFeature.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/ContractFeature.java index 34365f92652..a44c58dab42 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/ContractFeature.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/ContractFeature.java @@ -246,7 +246,7 @@ public void verifyMirrorAPIHollowAccountResponse(int amount) { assertNotNull(mirrorAccountResponse.getAccount()); assertEquals(amount, mirrorAccountResponse.getBalance().getBalance()); // Hollow account indicated by not having a public key defined. - assertEquals(ACCOUNT_EMPTY_KEYLIST, mirrorAccountResponse.getKey().getKey()); + assertThat(mirrorAccountResponse.getKey()).isNull(); } @And("the mirror node REST API should indicate not found when using evm address to retrieve as a contract") diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/TokenFeature.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/TokenFeature.java index c0d92476661..6317f5c3508 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/TokenFeature.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/steps/TokenFeature.java @@ -65,6 +65,7 @@ import com.hedera.mirror.test.e2e.acceptance.client.TokenClient; import com.hedera.mirror.test.e2e.acceptance.client.TokenClient.TokenNameEnum; import com.hedera.mirror.test.e2e.acceptance.client.TokenClient.TokenResponse; +import com.hedera.mirror.test.e2e.acceptance.config.AcceptanceTestProperties; import com.hedera.mirror.test.e2e.acceptance.props.ExpandedAccountId; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; @@ -87,6 +88,7 @@ @RequiredArgsConstructor public class TokenFeature extends AbstractFeature { + private final AcceptanceTestProperties properties; private final TokenClient tokenClient; private final AccountClient accountClient; private final MirrorNodeClient mirrorClient; @@ -457,6 +459,10 @@ public void burnToken(int amount) { @Given("{account} rejects the fungible token") public void rejectFungibleToken(AccountNameEnum ownerName) { + if (!properties.getFeatureProperties().isRejectToken()) { + return; + } + var owner = accountClient.getAccount(ownerName); networkTransactionResponse = tokenClient.rejectFungibleToken(List.of(tokenId), owner); assertThat(networkTransactionResponse.getTransactionId()).isNotNull(); @@ -467,6 +473,10 @@ public void rejectFungibleToken(AccountNameEnum ownerName) { @Then("the mirror node REST API should return the transaction {account} returns {int} fungible token to {account}") public void verifyTokenTransferForRejectedFungibleToken( AccountNameEnum senderName, long amount, AccountNameEnum treasuryName) { + if (!properties.getFeatureProperties().isRejectToken()) { + return; + } + var sender = accountClient.getAccount(senderName).getAccountId(); var treasury = accountClient.getAccount(treasuryName).getAccountId(); @@ -488,6 +498,10 @@ public void verifyTokenTransferForRejectedFungibleToken( @Given("{account} rejects serial number index {int}") public void rejectNonFungibleToken(AccountNameEnum ownerName, int index) { + if (!properties.getFeatureProperties().isRejectToken()) { + return; + } + long serialNumber = tokenNftInfoMap.get(tokenId).get(index).serialNumber(); var nftId = new NftId(tokenId, serialNumber); var owner = accountClient.getAccount(ownerName); @@ -501,6 +515,10 @@ public void rejectNonFungibleToken(AccountNameEnum ownerName, int index) { @Then( "the mirror node REST API should return the transaction {account} returns serial number index {int} to {account}") public void verifyTokenTransferForRejectedNft(AccountNameEnum senderName, int index, AccountNameEnum treasuryName) { + if (!properties.getFeatureProperties().isRejectToken()) { + return; + } + var sender = accountClient.getAccount(senderName).getAccountId(); var treasury = accountClient.getAccount(treasuryName).getAccountId(); long serialNumber = tokenNftInfoMap.get(tokenId).get(index).serialNumber(); diff --git a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/util/ContractCallResponseWrapper.java b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/util/ContractCallResponseWrapper.java index 335f1230d3a..a4973247997 100644 --- a/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/util/ContractCallResponseWrapper.java +++ b/hedera-mirror-test/src/test/java/com/hedera/mirror/test/e2e/acceptance/util/ContractCallResponseWrapper.java @@ -37,7 +37,8 @@ public class ContractCallResponseWrapper { private static final String EMPTY_RESULT = "0x0000000000000000000000000000000000000000000000000000000000000000"; - @NonNull private final ContractCallResponse response; + @NonNull + private final ContractCallResponse response; public String getResult() { return response.getResult(); @@ -120,4 +121,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(response); } + + @Override + public String toString() { + return response.getResult(); + } }