Skip to content

Commit

Permalink
feat: Implement pending state for airdrop purposes (#13566)
Browse files Browse the repository at this point in the history
Signed-off-by: ibankov <[email protected]>
Co-authored-by: Valentin Tronkov <[email protected]>
  • Loading branch information
2 people authored and JivkoKelchev committed Jul 17, 2024
1 parent 222940d commit 12f0f1d
Show file tree
Hide file tree
Showing 17 changed files with 700 additions and 6 deletions.
3 changes: 2 additions & 1 deletion hapi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ tasks.cloneHederaProtobufs {
// uncomment below to use a specific tag
// tag = "v0.51.0"
// uncomment below to use a specific branch
branch = "main"
// branch = "main"
branch = "hip-904-TokenClaimAirdrop"
}

sourceSets {
Expand Down
3 changes: 2 additions & 1 deletion hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,13 @@ public static HederaFunctionality functionOf(final TransactionBody txn) throws U
case TOKEN_UPDATE -> HederaFunctionality.TOKEN_UPDATE;
case TOKEN_UPDATE_NFTS -> HederaFunctionality.TOKEN_UPDATE_NFTS;
case TOKEN_WIPE -> HederaFunctionality.TOKEN_ACCOUNT_WIPE;
case TOKEN_REJECT -> HederaFunctionality.TOKEN_REJECT;
case UTIL_PRNG -> HederaFunctionality.UTIL_PRNG;
case UNCHECKED_SUBMIT -> HederaFunctionality.UNCHECKED_SUBMIT;
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 TOKEN_AIRDROP -> HederaFunctionality.TOKEN_AIRDROP;
case UNSET -> throw new UnknownHederaFunctionality();
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,13 @@ private static <T extends Record, R extends GeneratedMessageV3> R explicitPbjToP
case TokenUnpause -> HederaFunctionality.TOKEN_UNPAUSE;
case TokenUpdate -> HederaFunctionality.TOKEN_UPDATE;
case TokenUpdateNfts -> HederaFunctionality.TOKEN_UPDATE_NFTS;
case TokenReject -> HederaFunctionality.TOKEN_REJECT;
case TransactionGetReceipt -> HederaFunctionality.TRANSACTION_GET_RECEIPT;
case TransactionGetRecord -> HederaFunctionality.TRANSACTION_GET_RECORD;
case TransactionGetFastRecord -> HederaFunctionality.TRANSACTION_GET_FAST_RECORD;
case UncheckedSubmit -> HederaFunctionality.UNCHECKED_SUBMIT;
case UtilPrng -> HederaFunctionality.UTIL_PRNG;
case TokenReject -> HederaFunctionality.TOKEN_REJECT;
case TokenAirdrop -> HederaFunctionality.TOKEN_AIRDROP;
case UNRECOGNIZED -> throw new RuntimeException("Unknown function UNRECOGNIZED");
};
}
Expand Down Expand Up @@ -656,9 +657,6 @@ private static <T extends Record, R extends GeneratedMessageV3> R explicitPbjToP
case MAX_CHILD_RECORDS_EXCEEDED -> ResponseCodeEnum.MAX_CHILD_RECORDS_EXCEEDED;
case INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES -> ResponseCodeEnum.INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES;
case TRANSACTION_HAS_UNKNOWN_FIELDS -> ResponseCodeEnum.TRANSACTION_HAS_UNKNOWN_FIELDS;
case TOKEN_REFERENCE_REPEATED -> ResponseCodeEnum.TOKEN_REFERENCE_REPEATED;
case TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED -> ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED;
case INVALID_OWNER_ID -> ResponseCodeEnum.INVALID_OWNER_ID;
case ACCOUNT_IS_IMMUTABLE -> ResponseCodeEnum.ACCOUNT_IS_IMMUTABLE;
case ALIAS_ALREADY_ASSIGNED -> ResponseCodeEnum.ALIAS_ALREADY_ASSIGNED;
case INVALID_METADATA_KEY -> ResponseCodeEnum.INVALID_METADATA_KEY;
Expand All @@ -681,6 +679,9 @@ private static <T extends Record, R extends GeneratedMessageV3> R explicitPbjToP
case FQDN_SIZE_TOO_LARGE -> ResponseCodeEnum.FQDN_SIZE_TOO_LARGE;
case INVALID_ENDPOINT -> ResponseCodeEnum.INVALID_ENDPOINT;
case GOSSIP_ENDPOINTS_EXCEEDED_LIMIT -> ResponseCodeEnum.GOSSIP_ENDPOINTS_EXCEEDED_LIMIT;
case TOKEN_REFERENCE_REPEATED -> ResponseCodeEnum.TOKEN_REFERENCE_REPEATED;
case INVALID_OWNER_ID -> ResponseCodeEnum.INVALID_OWNER_ID;
case TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED -> ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED;
case SERVICE_ENDPOINTS_EXCEEDED_LIMIT -> ResponseCodeEnum.SERVICE_ENDPOINTS_EXCEEDED_LIMIT;
case INVALID_IPV4_ADDRESS -> ResponseCodeEnum.INVALID_IPV4_ADDRESS;
case EMPTY_TOKEN_REFERENCE_LIST -> ResponseCodeEnum.EMPTY_TOKEN_REFERENCE_LIST;
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 @@ -41,13 +41,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 @@ -80,6 +82,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 @@ -36,6 +36,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 @@ -70,6 +71,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 @@ -59,6 +59,7 @@
import static com.hedera.hapi.node.base.HederaFunctionality.SYSTEM_DELETE;
import static com.hedera.hapi.node.base.HederaFunctionality.SYSTEM_UNDELETE;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_ACCOUNT_WIPE;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_AIRDROP;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_ASSOCIATE_TO_ACCOUNT;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_BURN;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_CREATE;
Expand Down Expand Up @@ -177,6 +178,7 @@
* @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 tokenAirdrop the permission for {@link HederaFunctionality#TOKEN_AIRDROP} 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 @@ -248,6 +250,7 @@ public record ApiPermissionConfig(
@ConfigProperty(defaultValue = "2-50") PermissionedAccountsRange getAccountDetails,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenUpdateNfts,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenReject,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenAirdrop,
@ConfigProperty(defaultValue = "2-55") PermissionedAccountsRange createNode,
@ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange updateNode,
@ConfigProperty(defaultValue = "2-55") PermissionedAccountsRange deleteNode) {
Expand Down Expand Up @@ -298,6 +301,7 @@ public record ApiPermissionConfig(
permissionKeys.put(SCHEDULE_SIGN, c -> c.scheduleSign);
permissionKeys.put(TOKEN_UPDATE_NFTS, c -> c.tokenUpdateNfts);
permissionKeys.put(TOKEN_REJECT, c -> c.tokenReject);
permissionKeys.put(TOKEN_AIRDROP, c -> c.tokenAirdrop);
/* 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 @@ -48,6 +48,8 @@ public record TokensConfig(
ScaleFactor nftsMintThrottleScaleFactor,
@ConfigProperty(value = "nfts.useVirtualMerkle", defaultValue = "true") @NetworkProperty
boolean nftsUseVirtualMerkle,
@ConfigProperty(value = "maxAllowedAirdrops", defaultValue = "1000000") @NetworkProperty
long maxAllowedAirdorps,
@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
@@ -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 get(@NonNull final PendingAirdropId airdropId) {
requireNonNull(airdropId);
if (airdropId.hasNonFungibleToken()) {
return null;
}
return readableAirdropState.get(airdropId);
}

/** {@inheritDoc} */
@Override
public long sizeOfState() {
return readableAirdropState.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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));
}

/**
* Returns the {@link PendingAirdropValue} with the given airdrop id. If the airdrop contains only NFT return {@code null}.
* 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);
if (airdropId.hasNonFungibleToken()) {
return null;
}
return airdropState.getForModify(airdropId);
}
}
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;
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

0 comments on commit 12f0f1d

Please sign in to comment.