Skip to content

Commit

Permalink
Calculate acceptance test operator balance from USD (#8957)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
steven-sheehy authored Aug 6, 2024
1 parent beb49fa commit f72fe73
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 26 deletions.
3 changes: 2 additions & 1 deletion hedera-mirror-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
Expand All @@ -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()
Expand Down Expand Up @@ -152,20 +168,35 @@ private Map<String, AccountId> getNetworkMap(Set<NodeProperties> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -130,14 +143,14 @@ private TransactionResponse executeTransaction(
Client client, Stopwatch stopwatch, Supplier<Transaction<?>> 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> T executeQuery(Client client, Stopwatch stopwatch, Supplier<Query<T, ?>> 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) {
Expand Down Expand Up @@ -172,7 +185,8 @@ private RetryOperations retryOperations(Stopwatch stopwatch) {
.withListener(new RetryListener() {
@Override
public <T, E extends Throwable> void onError(RetryContext r, RetryCallback<T, E> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,9 +78,10 @@ public class AcceptanceTestProperties {
@NotNull
private Set<NodeProperties> 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand All @@ -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);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -120,4 +121,9 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(response);
}

@Override
public String toString() {
return response.getResult();
}
}

0 comments on commit f72fe73

Please sign in to comment.