Skip to content

Commit

Permalink
Implement ContractUpdateHandler.handle (#9379)
Browse files Browse the repository at this point in the history
Signed-off-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com>
Signed-off-by: Valentin Tronkov <valentin.tronkov@limechain.tech>
  • Loading branch information
vtronkov authored and imalygin committed Nov 13, 2023
1 parent 6913b72 commit ea231c4
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public class SingleTransactionRecordBuilderImpl
FeeRecordBuilder,
ContractDeleteRecordBuilder,
GenesisAccountRecordBuilder {

// base transaction data
private Transaction transaction;
private Bytes transactionBytes = Bytes.EMPTY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,41 @@

package com.hedera.node.app.service.contract.impl.handlers;

import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_EXPIRED_AND_PENDING_REMOVAL;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EXPIRATION_REDUCTION_NOT_ALLOWED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ADMIN_KEY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CONTRACT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MODIFYING_IMMUTABLE_CONTRACT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT;
import static com.hedera.node.app.service.token.api.AccountSummariesApi.SENTINEL_ACCOUNT_ID;
import static com.hedera.node.app.spi.HapiUtils.EMPTY_KEY_LIST;
import static com.hedera.node.app.spi.validation.ExpiryMeta.NA;
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.Key.KeyOneOfType;
import com.hedera.hapi.node.contract.ContractUpdateTransactionBody;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.api.TokenServiceApi;
import com.hedera.node.app.spi.key.KeyUtils;
import com.hedera.node.app.spi.validation.ExpiryMeta;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.app.spi.workflows.PreHandleContext;
import com.hedera.node.app.spi.workflows.TransactionHandler;
import com.hedera.node.config.data.EntitiesConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.data.TokensConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;

Expand All @@ -38,6 +59,7 @@
*/
@Singleton
public class ContractUpdateHandler implements TransactionHandler {

@Inject
public ContractUpdateHandler() {
// Exists for injection
Expand Down Expand Up @@ -74,6 +96,130 @@ private boolean hasCryptoAdminKey(final ContractUpdateTransactionBody op) {

@Override
public void handle(@NonNull final HandleContext context) throws HandleException {
throw new UnsupportedOperationException("Not implemented");
final var txn = requireNonNull(context).body();
final var op = txn.contractUpdateInstanceOrThrow();
final var target = op.contractIDOrThrow();

final var accountStore = context.readableStore(ReadableAccountStore.class);
final var toBeUpdated = accountStore.getContractById(target);
validateSemantics(toBeUpdated, context, op);
final var changed = update(toBeUpdated, context, op);

context.serviceApi(TokenServiceApi.class).updateContract(changed);
}

public Account update(
@NonNull final Account contract,
@NonNull final HandleContext context,
@NonNull final ContractUpdateTransactionBody op) {
final var builder = contract.copyBuilder();
if (op.hasAdminKey()) {
if (EMPTY_KEY_LIST.equals(op.adminKey())) {
builder.key(contract.key());
} else {
builder.key(op.adminKey());
}
}
if (op.hasExpirationTime()) {
if (contract.expiredAndPendingRemoval()) {
builder.expiredAndPendingRemoval(false);
}
builder.expirationSecond(op.expirationTime().seconds());
}
if (op.hasAutoRenewPeriod()) {
builder.autoRenewSeconds(op.autoRenewPeriod().seconds());
}
if (affectsMemo(op)) {
final var newMemo = op.hasMemoWrapper() ? op.memoWrapper() : op.memo();
context.attributeValidator().validateMemo(newMemo);
builder.memo(newMemo);
}
if (op.hasStakedAccountId()) {
if (SENTINEL_ACCOUNT_ID.equals(op.stakedAccountId())) {
builder.stakedAccountId((AccountID) null);
} else {
builder.stakedAccountId(op.stakedAccountId());
}
} else if (op.hasStakedNodeId()) {
builder.stakedNodeId(op.stakedNodeId());
}
if (op.hasDeclineReward()) {
builder.declineReward(op.declineReward());
}
if (op.hasAutoRenewAccountId()) {
builder.autoRenewAccountId(op.autoRenewAccountId());
}
if (op.hasMaxAutomaticTokenAssociations()) {
final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class);
final var entitiesConfig = context.configuration().getConfigData(EntitiesConfig.class);
final var tokensConfig = context.configuration().getConfigData(TokensConfig.class);

validateFalse(
op.maxAutomaticTokenAssociations() > ledgerConfig.maxAutoAssociations(),
REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT);

final long newMax = op.maxAutomaticTokenAssociations();
validateFalse(newMax < contract.maxAutoAssociations(), EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT);
validateFalse(
entitiesConfig.limitTokenAssociations() && newMax > tokensConfig.maxPerAccount(),
REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT);

builder.maxAutoAssociations(op.maxAutomaticTokenAssociations());
}
return builder.build();
}

private void validateSemantics(Account contract, HandleContext context, ContractUpdateTransactionBody op) {
validateTrue(contract != null, INVALID_CONTRACT_ID);

if (op.hasAdminKey()) {
boolean keyNotSentinel = !EMPTY_KEY_LIST.equals(op.adminKey());
boolean keyIsUnset = op.adminKey().key().kind() == KeyOneOfType.UNSET;
boolean keyIsNotValid = !KeyUtils.isValid(op.adminKey());
validateFalse(keyNotSentinel && (keyIsUnset || keyIsNotValid), INVALID_ADMIN_KEY);
}

if (op.hasExpirationTime()) {
try {
context.attributeValidator().validateExpiry(op.expirationTime().seconds());
} catch (HandleException e) {
validateFalse(contract.expiredAndPendingRemoval(), CONTRACT_EXPIRED_AND_PENDING_REMOVAL);
throw e;
}
}

validateFalse(!onlyAffectsExpiry(op) && !isMutable(contract), MODIFYING_IMMUTABLE_CONTRACT);
validateFalse(reducesExpiry(op, contract.expirationSecond()), EXPIRATION_REDUCTION_NOT_ALLOWED);

// validate expiry metadata
final var currentMetadata =
new ExpiryMeta(contract.expirationSecond(), contract.autoRenewSeconds(), contract.autoRenewAccountId());
final var updateMeta = new ExpiryMeta(
op.hasExpirationTime() ? op.expirationTime().seconds() : NA,
op.hasAutoRenewPeriod() ? op.autoRenewPeriod().seconds() : NA,
null);
context.expiryValidator().resolveUpdateAttempt(currentMetadata, updateMeta);
}

boolean onlyAffectsExpiry(ContractUpdateTransactionBody op) {
return !(op.hasProxyAccountID()
|| op.hasFileID()
|| affectsMemo(op)
|| op.hasAutoRenewPeriod()
|| op.hasAdminKey());
}

boolean affectsMemo(ContractUpdateTransactionBody op) {
return op.hasMemoWrapper() || (op.memo() != null && op.memo().length() > 0);
}

boolean isMutable(final Account contract) {
return Optional.ofNullable(contract.key())
.map(key -> !key.hasContractID())
.orElse(false);
}

private boolean reducesExpiry(ContractUpdateTransactionBody op, long curExpiry) {
return op.hasExpirationTime() && op.expirationTime().seconds() < curExpiry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ public long originalKvUsageFor(@NonNull final AccountID id) {
return oldAccount == null ? 0 : oldAccount.contractKvPairsNumber();
}

@Override
public void updateContract(Account contract) {
accountStore.put(contract);
}

/**
* A utility method that charges (debits) the payer up to the given total fee. If the payer account doesn't exist,
* then an exception is thrown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.token.CryptoTransferTransactionBody;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.spi.fees.Fees;
Expand Down Expand Up @@ -185,4 +186,10 @@ void chargeFees(
* @return the number of storage slots used by the given account before any changes were made
*/
long originalKvUsageFor(@NonNull AccountID id);

/**
* Updates the passed contract
* @param contract the contract that is updated
*/
void updateContract(Account contract);
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public List<HapiSpec> getSpecsInSuite() {
updateStakingFieldsWorks());
}

@HapiTest
private HapiSpec updateStakingFieldsWorks() {
return defaultHapiSpec("updateStakingFieldsWorks")
.given(
Expand Down Expand Up @@ -207,6 +208,7 @@ private HapiSpec eip1014AddressAlwaysHasPriority() {
}))))));
}

@HapiTest
private HapiSpec updateWithBothMemoSettersWorks() {
final var firstMemo = "First";
final var secondMemo = "Second";
Expand All @@ -226,6 +228,7 @@ private HapiSpec updateWithBothMemoSettersWorks() {
getContractInfo(CONTRACT).has(contractWith().memo(thirdMemo)));
}

@HapiTest
private HapiSpec updatingExpiryWorks() {
final var newExpiry = Instant.now().getEpochSecond() + 5 * ONE_MONTH;
return defaultHapiSpec("UpdatingExpiryWorks")
Expand All @@ -234,6 +237,7 @@ private HapiSpec updatingExpiryWorks() {
.then(getContractInfo(CONTRACT).has(contractWith().expiry(newExpiry)));
}

@HapiTest
private HapiSpec rejectsExpiryTooFarInTheFuture() {
final var smallBuffer = 12_345L;
final var excessiveExpiry = defaultMaxLifetime + Instant.now().getEpochSecond() + smallBuffer;
Expand All @@ -244,6 +248,7 @@ private HapiSpec rejectsExpiryTooFarInTheFuture() {
.then(contractUpdate(CONTRACT).newExpirySecs(excessiveExpiry).hasKnownStatus(INVALID_EXPIRATION_TIME));
}

@HapiTest
private HapiSpec updateAutoRenewWorks() {
return defaultHapiSpec("UpdateAutoRenewWorks")
.given(
Expand All @@ -254,6 +259,7 @@ private HapiSpec updateAutoRenewWorks() {
.then(getContractInfo(CONTRACT).has(contractWith().autoRenew(THREE_MONTHS_IN_SECONDS + ONE_DAY)));
}

@HapiTest
private HapiSpec updateAutoRenewAccountWorks() {
final var autoRenewAccount = "autoRenewAccount";
final var newAutoRenewAccount = "newAutoRenewAccount";
Expand All @@ -280,6 +286,7 @@ private HapiSpec updateAutoRenewAccountWorks() {
.logged());
}

@HapiTest
private HapiSpec updateAdminKeyWorks() {
return defaultHapiSpec("UpdateAdminKeyWorks")
.given(
Expand Down Expand Up @@ -419,6 +426,7 @@ HapiSpec fridayThe13thSpec() {
contractDelete(contract + suffix).payingWith(payer).hasKnownStatus(SUCCESS));
}

@HapiTest
private HapiSpec updateDoesNotChangeBytecode() {
// HSCS-DCPR-001
final var simpleStorageContract = "SimpleStorage";
Expand Down

0 comments on commit ea231c4

Please sign in to comment.