Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement pending state for airdrop purposes #13566

2 changes: 1 addition & 1 deletion hapi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ tasks.cloneHederaProtobufs {
// uncomment below to use a specific tag
// tag = "v0.51.0"
// uncomment below to use a specific branch
branch = "main"
branch = "hip-904-proto-updates"
}

sourceSets {
Expand Down
1 change: 1 addition & 0 deletions hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ public static HederaFunctionality functionOf(final TransactionBody txn) throws U
case NODE_CREATE -> HederaFunctionality.NODE_CREATE;
case NODE_UPDATE -> HederaFunctionality.NODE_UPDATE;
case NODE_DELETE -> HederaFunctionality.NODE_DELETE;
case TOKEN_REJECT -> HederaFunctionality.TOKEN_REJECT;
case UNSET -> throw new UnknownHederaFunctionality();
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public interface StoreMetricsService {
enum StoreType {
TOPIC,
ACCOUNT,
AIRDROPS,
NFT,
TOKEN,
TOKEN_RELATION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public StoreMetricsServiceImpl(@NonNull final Metrics metrics) {
this.storeMetricsMap = new EnumMap<>(StoreType.class);
storeMetricsMap.put(StoreType.TOPIC, new StoreMetricsImpl(metrics, "topics"));
storeMetricsMap.put(StoreType.ACCOUNT, new StoreMetricsImpl(metrics, "accounts"));
storeMetricsMap.put(StoreType.AIRDROPS, new StoreMetricsImpl(metrics, "airdrops"));
storeMetricsMap.put(StoreType.NFT, new StoreMetricsImpl(metrics, "nfts"));
storeMetricsMap.put(StoreType.TOKEN, new StoreMetricsImpl(metrics, "tokens"));
storeMetricsMap.put(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@
import com.hedera.node.app.service.schedule.ScheduleService;
import com.hedera.node.app.service.schedule.impl.ReadableScheduleStoreImpl;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableAirdropStore;
import com.hedera.node.app.service.token.ReadableNetworkStakingRewardsStore;
import com.hedera.node.app.service.token.ReadableNftStore;
import com.hedera.node.app.service.token.ReadableStakingInfoStore;
import com.hedera.node.app.service.token.ReadableTokenRelationStore;
import com.hedera.node.app.service.token.ReadableTokenStore;
import com.hedera.node.app.service.token.TokenService;
import com.hedera.node.app.service.token.impl.ReadableAccountStoreImpl;
import com.hedera.node.app.service.token.impl.ReadableAirdropStoreImpl;
import com.hedera.node.app.service.token.impl.ReadableNetworkStakingRewardsStoreImpl;
import com.hedera.node.app.service.token.impl.ReadableNftStoreImpl;
import com.hedera.node.app.service.token.impl.ReadableStakingInfoStoreImpl;
Expand Down Expand Up @@ -77,6 +79,7 @@ private static Map<Class<?>, StoreEntry> createFactoryMap() {
Map<Class<?>, StoreEntry> newMap = new HashMap<>();
// Tokens and accounts
newMap.put(ReadableAccountStore.class, new StoreEntry(TokenService.NAME, ReadableAccountStoreImpl::new));
newMap.put(ReadableAirdropStore.class, new StoreEntry(TokenService.NAME, ReadableAirdropStoreImpl::new));
newMap.put(ReadableNftStore.class, new StoreEntry(TokenService.NAME, ReadableNftStoreImpl::new));
newMap.put(
ReadableStakingInfoStore.class, new StoreEntry(TokenService.NAME, ReadableStakingInfoStoreImpl::new));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.hedera.node.app.service.schedule.impl.WritableScheduleStoreImpl;
import com.hedera.node.app.service.token.TokenService;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.WritableAirdropStore;
import com.hedera.node.app.service.token.impl.WritableNetworkStakingRewardsStore;
import com.hedera.node.app.service.token.impl.WritableNftStore;
import com.hedera.node.app.service.token.impl.WritableStakingInfoStore;
Expand Down Expand Up @@ -65,6 +66,7 @@ private static Map<Class<?>, StoreEntry> createFactoryMap() {
newMap.put(WritableTopicStore.class, new StoreEntry(ConsensusService.NAME, WritableTopicStore::new));
// TokenService
newMap.put(WritableAccountStore.class, new StoreEntry(TokenService.NAME, WritableAccountStore::new));
newMap.put(WritableAirdropStore.class, new StoreEntry(TokenService.NAME, WritableAirdropStore::new));
newMap.put(WritableNftStore.class, new StoreEntry(TokenService.NAME, WritableNftStore::new));
newMap.put(WritableTokenStore.class, new StoreEntry(TokenService.NAME, WritableTokenStore::new));
newMap.put(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_GRANT_KYC_TO_ACCOUNT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_MINT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_PAUSE;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_REJECT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_REVOKE_KYC_FROM_ACCOUNT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_UNFREEZE_ACCOUNT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_UNPAUSE;
Expand Down Expand Up @@ -176,6 +177,7 @@
* @param freeze the permission for {@link HederaFunctionality#FREEZE} functionality
* @param getAccountDetails the permission for {@link HederaFunctionality#GET_ACCOUNT_DETAILS} functionality
* @param tokenUpdateNfts the permission for {@link HederaFunctionality#TOKEN_UPDATE_NFTS} functionality
* @param tokenReject the permission for {@link HederaFunctionality#TOKEN_REJECT} functionality
*
* @param createNode the permission for {@link HederaFunctionality#NODE_CREATE} functionality
* @param updateNode the permission for {@link HederaFunctionality#NODE_UPDATE} functionality
Expand Down Expand Up @@ -247,6 +249,7 @@ public record ApiPermissionConfig(
@ConfigProperty(defaultValue = "2-58") PermissionedAccountsRange freeze,
@ConfigProperty(defaultValue = "2-50") PermissionedAccountsRange getAccountDetails,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenUpdateNfts,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenReject,
ibankov marked this conversation as resolved.
Show resolved Hide resolved
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange createNode,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange updateNode,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange deleteNode,
Expand Down Expand Up @@ -297,6 +300,7 @@ public record ApiPermissionConfig(
permissionKeys.put(SCHEDULE_DELETE, c -> c.scheduleDelete);
permissionKeys.put(SCHEDULE_SIGN, c -> c.scheduleSign);
permissionKeys.put(TOKEN_UPDATE_NFTS, c -> c.tokenUpdateNfts);
permissionKeys.put(TOKEN_REJECT, c -> c.tokenReject);
/* Queries */
permissionKeys.put(CONSENSUS_GET_TOPIC_INFO, c -> c.getTopicInfo);
permissionKeys.put(CONTRACT_CALL_LOCAL, c -> c.contractCallLocalMethod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public record TokensConfig(
ScaleFactor nftsMintThrottleScaleFactor,
@ConfigProperty(value = "nfts.useVirtualMerkle", defaultValue = "true") @NetworkProperty
boolean nftsUseVirtualMerkle,
@ConfigProperty(value = "maxAllowedAirdrops", defaultValue = "1000000") @NetworkProperty
long maxAllowedAirdorps,
Comment on lines +51 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting for @netopyr to give us the exact value here.

@ConfigProperty(value = "autoCreations.isEnabled", defaultValue = "true") @NetworkProperty
boolean autoCreationsIsEnabled,
@ConfigProperty(value = "maxMetadataBytes", defaultValue = "100") @NetworkProperty int tokensMaxMetadataBytes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ public final class PbjConverter {
case TransactionGetFastRecord -> HederaFunctionality.TRANSACTION_GET_FAST_RECORD;
case UncheckedSubmit -> HederaFunctionality.UNCHECKED_SUBMIT;
case UtilPrng -> HederaFunctionality.UTIL_PRNG;
case TokenReject -> HederaFunctionality.TOKEN_REJECT;
case UNRECOGNIZED -> throw new RuntimeException("Unknown function UNRECOGNIZED");
};
}
Expand Down Expand Up @@ -458,6 +459,7 @@ public final class PbjConverter {
.TransactionGetFastRecord;
case UNCHECKED_SUBMIT -> com.hederahashgraph.api.proto.java.HederaFunctionality.UncheckedSubmit;
case UTIL_PRNG -> com.hederahashgraph.api.proto.java.HederaFunctionality.UtilPrng;
case TOKEN_REJECT -> com.hederahashgraph.api.proto.java.HederaFunctionality.TokenReject;
};
}

Expand Down Expand Up @@ -798,6 +800,8 @@ public final class PbjConverter {
case GOSSIP_ENDPOINTS_EXCEEDED_LIMIT -> ResponseCodeEnum.GOSSIP_ENDPOINTS_EXCEEDED_LIMIT;
case SERVICE_ENDPOINTS_EXCEEDED_LIMIT -> ResponseCodeEnum.SERVICE_ENDPOINTS_EXCEEDED_LIMIT;
case INVALID_IPV4_ADDRESS -> ResponseCodeEnum.INVALID_IPV4_ADDRESS;
case TOKEN_REFERENCE_REPEATED -> ResponseCodeEnum.TOKEN_REFERENCE_REPEATED;
case INVALID_OWNER_ID -> ResponseCodeEnum.INVALID_OWNER_ID;
case UNRECOGNIZED -> throw new RuntimeException("UNRECOGNIZED Response code!");
};
}
Expand Down Expand Up @@ -1362,6 +1366,9 @@ public static com.hederahashgraph.api.proto.java.ResponseCodeEnum fromPbj(@NonNu
case INVALID_IPV4_ADDRESS -> com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_IPV4_ADDRESS;

// case UNRECOGNIZED -> throw new RuntimeException("UNRECOGNIZED Response code!");
case TOKEN_REFERENCE_REPEATED -> com.hederahashgraph.api.proto.java.ResponseCodeEnum
.TOKEN_REFERENCE_REPEATED;
case INVALID_OWNER_ID -> com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_OWNER_ID;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ static HederaFunctionality functionalityForType(final DataOneOfType transactionT
case NODE_UPDATE -> HederaFunctionality.NODE_UPDATE;
case NODE_DELETE -> HederaFunctionality.NODE_DELETE;
case UNSET -> HederaFunctionality.NONE;
case TOKEN_REJECT -> HederaFunctionality.TOKEN_REJECT;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hedera.node.app.service.token.impl;

import static com.hedera.node.app.service.token.impl.schemas.V0500TokenSchema.AIRDROPS_KEY;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.PendingAirdropId;
import com.hedera.hapi.node.base.PendingAirdropValue;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableAirdropStore;
import com.swirlds.state.spi.ReadableKVState;
import com.swirlds.state.spi.ReadableStates;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
* Default implementation of {@link ReadableAccountStore}
*/
public class ReadableAirdropStoreImpl implements ReadableAirdropStore {
/** The underlying data storage class that holds the airdrop data. */
private final ReadableKVState<PendingAirdropId, PendingAirdropValue> readableAirdropState;

/**
* Create a new {@link ReadableAirdropStoreImpl} instance.
*
* @param states The state to use.
*/
public ReadableAirdropStoreImpl(@NonNull final ReadableStates states) {
requireNonNull(states);
this.readableAirdropState = states.get(AIRDROPS_KEY);
}

/** {@inheritDoc} */
@Override
public boolean exists(@NonNull final PendingAirdropId airdropId) {
return readableAirdropState.contains(requireNonNull(airdropId));
}

/** {@inheritDoc} */
@Override
@Nullable
public PendingAirdropValue getFungibleAirdropAmount(@NonNull final PendingAirdropId airdropId) {
ibankov marked this conversation as resolved.
Show resolved Hide resolved
requireNonNull(airdropId);
if (airdropId.hasNonFungibleToken()) {
return null;
}
return readableAirdropState.get(airdropId);
}

/** {@inheritDoc} */
@Override
public long sizeOfState() {
return readableAirdropState.size();
}
Neeharika-Sompalli marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hedera.node.app.service.token.impl;

import static com.hedera.node.app.service.token.impl.schemas.V0500TokenSchema.AIRDROPS_KEY;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.PendingAirdropId;
import com.hedera.hapi.node.base.PendingAirdropValue;
import com.hedera.node.app.spi.metrics.StoreMetricsService;
import com.hedera.node.config.data.TokensConfig;
import com.swirlds.config.api.Configuration;
import com.swirlds.state.spi.WritableKVState;
import com.swirlds.state.spi.WritableStates;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
* Provides write methods for modifying underlying data storage mechanisms for
* working with Pending Airdrops.
*
* <p>This class is not exported from the module. It is an internal implementation detail.
* This class is not complete, it will be extended with other methods like remove, update etc.,
*/
public class WritableAirdropStore extends ReadableAirdropStoreImpl {
/**
* The underlying data storage class that holds the Pending Airdrops data.
*/
private final WritableKVState<PendingAirdropId, PendingAirdropValue> airdropState;

/**
* Create a new {@link WritableAirdropStore} instance.
*
* @param states The state to use.
*/
public WritableAirdropStore(
@NonNull final WritableStates states,
@NonNull final Configuration configuration,
@NonNull final StoreMetricsService storeMetricsService) {
super(states);
airdropState = states.get(AIRDROPS_KEY);

final long maxCapacity = configuration.getConfigData(TokensConfig.class).maxAllowedAirdorps();
final var storeMetrics = storeMetricsService.get(StoreMetricsService.StoreType.AIRDROPS, maxCapacity);
airdropState.setMetrics(storeMetrics);
}

/**
* Persists a new {@link PendingAirdropId} with given {@link PendingAirdropValue} into the state,
* as well as exporting its ID to the transaction receipt. If there is existing
* airdrop with the same id we add the value to the existing drop.
*
* @param airdropId - the airdropId to be persisted.
* @param airdropValue - the value for the given airdropId to be persisted.
*/
public void put(@NonNull final PendingAirdropId airdropId, @NonNull final PendingAirdropValue airdropValue) {
requireNonNull(airdropId);
requireNonNull(airdropValue);

if (!airdropState.contains(airdropId)) {
airdropState.put(airdropId, airdropValue);
return;
}

if (airdropId.hasFungibleTokenType()) {
var existingAirdropValue =
requireNonNull(airdropState.get(airdropId)).amount();
var newValue = airdropValue.amount() + existingAirdropValue;
var newAirdropValue =
PendingAirdropValue.newBuilder().amount(newValue).build();
airdropState.put(airdropId, newAirdropValue);
}
}

/**
* Removes a {@link PendingAirdropId} from the state
*
* @param airdropId the {@code PendingAirdropId} to be removed
*/
public void remove(@NonNull final PendingAirdropId airdropId) {
airdropState.remove(requireNonNull(airdropId));
vtronkov marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Returns the {@link PendingAirdropValue} with the given airdrop id.
* If no such airdrop exists, returns {@code null}
*
* @param airdropId - the id of the airdrop, which value should be retrieved
* @return the fungible airdrop value, or {@code null} if no such
* airdrop exists
*/
@Nullable
public PendingAirdropValue getForModify(@NonNull final PendingAirdropId airdropId) {
requireNonNull(airdropId);
return airdropState.getForModify(airdropId);
ibankov marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.base.PendingAirdropId;
import com.hedera.hapi.node.base.PendingAirdropValue;
import com.hedera.hapi.node.base.SemanticVersion;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.state.spi.MigrationContext;
import com.swirlds.state.spi.StateDefinition;
import com.swirlds.state.spi.WritableKVState;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Set;
import java.util.SortedMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -38,6 +42,8 @@
public class V0500TokenSchema extends StakingInfoManagementSchema {
private static final Logger log = LogManager.getLogger(V0500TokenSchema.class);
private static final String SHARED_VALUES_KEY = "V0500_FIRST_STORAGE_KEYS";
private static final long MAX_PENDING_AIRDROPS = 1_000_000_000L;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question for the PR reviewer: do you think this is accurate?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what is the decided value for this. @Nana-EC @netopyr Do you know if this is correct ?

public static final String AIRDROPS_KEY = "PENDING_AIRDROPS";

private static final SemanticVersion VERSION =
SemanticVersion.newBuilder().major(0).minor(50).patch(0).build();
Expand All @@ -46,6 +52,14 @@ public V0500TokenSchema() {
super(VERSION);
}

@SuppressWarnings("rawtypes")
@NonNull
@Override
public Set<StateDefinition> statesToCreate() {
return Set.of(StateDefinition.onDisk(
AIRDROPS_KEY, PendingAirdropId.PROTOBUF, PendingAirdropValue.PROTOBUF, MAX_PENDING_AIRDROPS));
}

@Override
public void migrate(@NonNull final MigrationContext ctx) {
requireNonNull(ctx);
Expand Down
Loading
Loading