Skip to content

Commit

Permalink
fix: Cherry-Pick (0.54): Airdrop transfer list size validation (#15937)
Browse files Browse the repository at this point in the history
Signed-off-by: Zhivko Kelchev <[email protected]>
Signed-off-by: ibankov <[email protected]>
Co-authored-by: JivkoKelchev <[email protected]>
Co-authored-by: ibankov <[email protected]>
  • Loading branch information
3 people authored Oct 9, 2024
1 parent 5586626 commit 2637f74
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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

import static com.hedera.hapi.node.base.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
Expand All @@ -24,7 +25,7 @@
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable;
import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsableForAliasedId;
import static com.hedera.node.app.service.token.impl.validators.CryptoTransferValidator.validateTokenTransfers;
Expand All @@ -48,7 +49,7 @@
import com.hedera.node.app.service.token.impl.handlers.transfer.customfees.CustomFeeExemptions;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.config.data.TokensConfig;
import com.hedera.node.config.data.LedgerConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import javax.inject.Inject;
Expand Down Expand Up @@ -97,11 +98,9 @@ public void validateSemantics(
@NonNull final ReadableTokenStore tokenStore,
@NonNull final ReadableTokenRelationStore tokenRelStore,
@NonNull final ReadableNftStore nftStore) {
var tokensConfig = context.configuration().getConfigData(TokensConfig.class);
validateTrue(
op.tokenTransfers().size() <= tokensConfig.maxAllowedAirdropTransfersPerTx(),
TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED);

var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class);
var totalFungibleTransfers = 0;
var totalNftTransfers = 0;
for (final var xfers : op.tokenTransfers()) {
final var tokenId = xfers.tokenOrThrow();
final var token = getIfUsable(tokenId, tokenStore);
Expand All @@ -118,6 +117,12 @@ public void validateSemantics(
// 1. Validate token associations
validateFungibleTransfers(
context.payer(), senderAccount, tokenId, senderAccountAmount.get(), tokenRelStore);
totalFungibleTransfers += xfers.transfers().size();

// Verify that the current total number of (counted) fungible transfers does not exceed the limit
validateTrue(
totalFungibleTransfers <= ledgerConfig.tokenTransfersMaxLen(),
TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED);
}

// process non-fungible tokens transfers if any
Expand All @@ -137,14 +142,11 @@ public void validateSemantics(
final var senderId = nftTransfer.orElseThrow().senderAccountIDOrThrow();
final var senderAccount = accountStore.getAliasedAccountById(senderId);
validateTrue(senderAccount != null, INVALID_ACCOUNT_ID);
validateNftTransfers(
context.payer(),
senderAccount,
tokenId,
xfers.nftTransfers(),
tokenRelStore,
tokenStore,
nftStore);
validateNftTransfers(senderAccount, tokenId, xfers.nftTransfers(), tokenRelStore, tokenStore, nftStore);

totalNftTransfers += xfers.nftTransfers().size();
// Verify that the current total number of (counted) nft transfers does not exceed the limit
validateTrue(totalNftTransfers <= ledgerConfig.nftTransfersMaxLen(), BATCH_SIZE_LIMIT_EXCEEDED);
}
}
}
Expand Down Expand Up @@ -184,7 +186,6 @@ private static void validateFungibleTransfers(
}

private void validateNftTransfers(
@NonNull final AccountID payer,
@NonNull final Account senderAccount,
@NonNull final TokenID tokenId,
@NonNull final List<NftTransfer> nftTransfers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

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

import static com.hedera.hapi.node.base.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_ACCOUNT_AMOUNTS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_AMOUNTS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.OK;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hedera.node.app.service.token.impl.handlers.BaseTokenHandler.asToken;
import static com.hedera.node.app.service.token.impl.test.handlers.transfer.AirDropTransferType.NFT_AIRDROP;
import static com.hedera.node.app.service.token.impl.test.handlers.transfer.AirDropTransferType.TOKEN_AIRDROP;
Expand All @@ -39,6 +39,7 @@
import static org.mockito.Mockito.when;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.NftTransfer;
import com.hedera.hapi.node.base.PendingAirdropId;
import com.hedera.hapi.node.base.PendingAirdropValue;
import com.hedera.hapi.node.base.ResponseCodeEnum;
Expand Down Expand Up @@ -379,13 +380,10 @@ void tokenTransfersAboveMax() {
tokenAirdropHandler = new TokenAirdropHandler(tokenAirdropValidator, validator);
given(handleContext.savepointStack()).willReturn(stack);
given(stack.getBaseBuilder(TokenAirdropStreamBuilder.class)).willReturn(tokenAirdropRecordBuilder);
var tokenWithNoCustomFees =
fungibleToken.copyBuilder().customFees(Collections.emptyList()).build();
var nftWithNoCustomFees = nonFungibleToken
.copyBuilder()
.customFees(Collections.emptyList())
.build();
writableTokenStore.put(tokenWithNoCustomFees);
writableTokenStore.put(nftWithNoCustomFees);
given(storeFactory.writableStore(WritableTokenStore.class)).willReturn(writableTokenStore);
given(storeFactory.readableStore(ReadableTokenStore.class)).willReturn(writableTokenStore);
Expand All @@ -396,26 +394,13 @@ void tokenTransfersAboveMax() {
.build();
givenAirdropTxn(txn, payerId);

given(handleContext.dispatchRemovablePrecedingTransaction(
any(), eq(TokenAirdropStreamBuilder.class), eq(null), eq(payerId), any()))
.will((invocation) -> {
var pendingAirdropId = PendingAirdropId.newBuilder().build();
var pendingAirdropValue = PendingAirdropValue.newBuilder().build();
var pendingAirdropRecord = PendingAirdropRecord.newBuilder()
.pendingAirdropId(pendingAirdropId)
.pendingAirdropValue(pendingAirdropValue)
.build();

return tokenAirdropRecordBuilder.addPendingAirdrop(pendingAirdropRecord);
});

given(handleContext.expiryValidator()).willReturn(expiryValidator);
given(handleContext.feeCalculatorFactory()).willReturn(feeCalculatorFactory);
given(handleContext.tryToChargePayer(anyLong())).willReturn(true);

Assertions.assertThatThrownBy(() -> tokenAirdropHandler.handle(handleContext))
.isInstanceOf(HandleException.class)
.has(responseCode(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED));
.has(responseCode(BATCH_SIZE_LIMIT_EXCEEDED));
}

@Test
Expand Down Expand Up @@ -575,9 +560,12 @@ private List<TokenTransferList> transactionBodyAboveMaxTransferLimit() {

for (int i = 0; i <= MAX_TOKEN_TRANSFERS; i++) {
result.add(TokenTransferList.newBuilder()
.token(TOKEN_2468)
.transfers(ACCT_4444_PLUS_10)
.nftTransfers(SERIAL_1_FROM_3333_TO_4444, SERIAL_1_FROM_3333_TO_4444)
.token(nonFungibleTokenId)
.nftTransfers(NftTransfer.newBuilder()
.serialNumber(1)
.senderAccountID(ownerId)
.receiverAccountID(tokenReceiverId)
.build())
.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_HAS_PENDING_AIRDROPS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
Expand All @@ -101,7 +102,7 @@
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_IS_PAUSED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED;
import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON;
import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE;

Expand All @@ -113,6 +114,7 @@
import com.hedera.services.bdd.junit.HapiTestLifecycle;
import com.hedera.services.bdd.junit.LeakyHapiTest;
import com.hedera.services.bdd.junit.support.TestLifecycle;
import com.hedera.services.bdd.spec.SpecOperation;
import com.hedera.services.bdd.spec.keys.SigControl;
import com.hedera.services.bdd.spec.transactions.token.HapiTokenCreate;
import com.hedera.services.bdd.spec.transactions.token.TokenMovement;
Expand All @@ -122,10 +124,12 @@
import com.swirlds.common.utility.CommonUtils;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -478,23 +482,13 @@ final Stream<DynamicTest> tokenAirdropMultipleTokens() {
createTokenWithName("FT2"),
createTokenWithName("FT3"),
createTokenWithName("FT4"),
createTokenWithName("FT5"),
createTokenWithName("FT6"),
createTokenWithName("FT7"),
createTokenWithName("FT8"),
createTokenWithName("FT9"),
createTokenWithName("FT10"))
createTokenWithName("FT5"))
.when(tokenAirdrop(
defaultMovementOfToken("FT1"),
defaultMovementOfToken("FT2"),
defaultMovementOfToken("FT3"),
defaultMovementOfToken("FT4"),
defaultMovementOfToken("FT5"),
defaultMovementOfToken("FT6"),
defaultMovementOfToken("FT7"),
defaultMovementOfToken("FT8"),
defaultMovementOfToken("FT9"),
defaultMovementOfToken("FT10"))
defaultMovementOfToken("FT5"))
.payingWith(OWNER)
.via("fungible airdrop"))
.then(
Expand All @@ -508,17 +502,7 @@ final Stream<DynamicTest> tokenAirdropMultipleTokens() {
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT4", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT5", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT6", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT7", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT8", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT9", 10),
getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS)
.hasTokenBalance("FT10", 10));
.hasTokenBalance("FT5", 10));
}
}

Expand Down Expand Up @@ -1380,7 +1364,7 @@ final Stream<DynamicTest> aboveMaxTransfersFails() {
defaultMovementOfToken("FUNGIBLE10"),
defaultMovementOfToken("FUNGIBLE11"))
.payingWith(OWNER)
.hasKnownStatus(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED));
.hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED));
}

@HapiTest
Expand Down Expand Up @@ -1785,7 +1769,7 @@ final Stream<DynamicTest> moreThanTenTokensToMultipleAccounts() {
moving(10L, FUNGIBLE_TOKEN_J).between(ALICE, STEVE),
moving(10L, FUNGIBLE_TOKEN_K).between(ALICE, STEVE))
.signedByPayerAnd(ALICE)
.hasKnownStatus(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED));
.hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED));
}

@HapiTest
Expand Down Expand Up @@ -1967,6 +1951,70 @@ final Stream<DynamicTest> airdropTo0x0Address() {
.payingWith(OWNER)
.hasKnownStatus(INVALID_ACCOUNT_ID));
}

@HapiTest
@DisplayName("airdrop 1 fungible token to 10 accounts")
final Stream<DynamicTest> pendingAirdropOneTokenToMoreThan10Accounts() {
final var accountNames = generateAccountNames(10);
return hapiTest(flattened(
// create 10 accounts with 0 auto associations
createAccounts(accountNames, 0),
tokenAirdrop(distributeTokens(FUNGIBLE_TOKEN, OWNER, accountNames))
.payingWith(OWNER)
.hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED)));
}

@HapiTest
@DisplayName("airdrop more than 10 nft")
final Stream<DynamicTest> airdropMoreThan10Nft() {
final var nft = "nft";
var nftSupplyKey = "nftSupplyKey";
return hapiTest(flattened(
newKeyNamed(nftSupplyKey),
tokenCreate(nft)
.supplyKey(nftSupplyKey)
.tokenType(NON_FUNGIBLE_UNIQUE)
.initialSupply(0)
.treasury(OWNER),
// mint from 1 to 10 serials
mintToken(
nft,
IntStream.range(0, 10)
.mapToObj(a -> ByteString.copyFromUtf8(String.valueOf(a)))
.toList()),
// mint 11th serial
mintToken(nft, List.of(ByteString.copyFromUtf8(String.valueOf(11)))),
// try to airdrop 11 NFT
tokenAirdrop(distributeNFT(nft, OWNER, RECEIVER_WITH_0_AUTO_ASSOCIATIONS))
.payingWith(OWNER)
.hasKnownStatus(BATCH_SIZE_LIMIT_EXCEEDED)));
}

private static ArrayList<String> generateAccountNames(int count) {
final var accountNames = new ArrayList<String>(count);
for (int i = 0; i < count; i++) {
accountNames.add(String.format("account%d", i));
}
return accountNames;
}

private static ArrayList<SpecOperation> createAccounts(
ArrayList<String> accountNames, int numberOfAutoAssociations) {
final var specOps = new ArrayList<SpecOperation>(accountNames.size());
for (String accountName : accountNames) {
specOps.add(cryptoCreate(accountName).maxAutomaticTokenAssociations(numberOfAutoAssociations));
}
return specOps;
}

private static TokenMovement distributeTokens(String token, String sender, ArrayList<String> accountNames) {
return moving(accountNames.size(), token).distributing(sender, accountNames.toArray(new String[0]));
}

private static TokenMovement distributeNFT(String token, String sender, String receiver) {
final long[] serials = LongStream.rangeClosed(1, 11).toArray();
return TokenMovement.movingUnique(token, serials).between(sender, receiver);
}
}

@EmbeddedHapiTest(EmbeddedReason.NEEDS_STATE_ACCESS)
Expand Down
Loading

0 comments on commit 2637f74

Please sign in to comment.