Skip to content

Commit

Permalink
Use getOriginalValue instead of using from readableStore (#7707)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Hess <[email protected]>
Signed-off-by: Neeharika-Sompalli <[email protected]>
Signed-off-by: Michael Heinrichs <[email protected]>
Signed-off-by: neeharika.sompalli <[email protected]>
Co-authored-by: Matt Hess <[email protected]>
Co-authored-by: Michael Heinrichs <[email protected]>
  • Loading branch information
3 people authored Aug 2, 2023
1 parent f1e0bed commit 55bef50
Show file tree
Hide file tree
Showing 21 changed files with 1,457 additions and 292 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* Copyright (C) 2023 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.hapi.node.base.ResponseCodeEnum.FAIL_INVALID;
import static com.hedera.node.app.service.token.impl.comparator.TokenComparators.ACCOUNT_AMOUNT_COMPARATOR;
import static com.hedera.node.app.service.token.impl.handlers.staking.StakingRewardsHelper.asAccountAmounts;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.NftID;
import com.hedera.hapi.node.base.NftTransfer;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenTransferList;
import com.hedera.hapi.node.state.common.EntityIDPair;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Base class for both {@link com.hedera.node.app.service.token.records.ParentRecordFinalizer} and {@link
* com.hedera.node.app.service.token.records.ChildRecordFinalizer}. This contains methods that are common to both
* classes.
*/
public class RecordFinalizerBase {
private static final AccountID ZERO_ACCOUNT_ID =
AccountID.newBuilder().accountNum(0).build();

/**
* Gets all hbar changes for all modified accounts from the given {@link WritableAccountStore}.
* @param writableAccountStore the {@link WritableAccountStore} to get the hbar changes from
* @return a {@link Map} of {@link AccountID} to {@link Long} representing the hbar changes for all modified
*/
@NonNull
protected Map<AccountID, Long> hbarChangesFrom(@NonNull final WritableAccountStore writableAccountStore) {
final var hbarChanges = new HashMap<AccountID, Long>();
var netHbarBalance = 0;
for (final AccountID modifiedAcctId : writableAccountStore.modifiedAccountsInState()) {
final var modifiedAcct = writableAccountStore.getAccountById(modifiedAcctId);
final var persistedAcct = writableAccountStore.getOriginalValue(modifiedAcctId);
// It's possible the modified account was created in this transaction, in which case the non-existent
// persisted account effectively has no balance (i.e. its prior balance is 0)
final var persistedBalance = persistedAcct != null ? persistedAcct.tinybarBalance() : 0;

// Never allow an account's net hbar balance to be negative
validateTrue(modifiedAcct.tinybarBalance() >= 0, FAIL_INVALID);

final var netHbarChange = modifiedAcct.tinybarBalance() - persistedBalance;
if (netHbarChange != 0) {
netHbarBalance += netHbarChange;
hbarChanges.put(modifiedAcctId, netHbarChange);
}
}
// Since this is a finalization handler, we should have already succeeded in handling the transaction in a
// handler before getting here. Therefore, if the sum is non-zero, something went wrong, and we'll respond with
// FAIL_INVALID
validateTrue(netHbarBalance == 0, FAIL_INVALID);

return hbarChanges;
}

/**
* Gets all fungible tokenRelation balances for all modified token relations from the given {@link WritableTokenRelationStore}.
* @param writableTokenRelStore the {@link WritableTokenRelationStore} to get the token relation balances from
* @return a {@link Map} of {@link EntityIDPair} to {@link Long} representing the token relation balances for all
* modified token relations
*/
@NonNull
protected Map<EntityIDPair, Long> fungibleChangesFrom(
@NonNull final WritableTokenRelationStore writableTokenRelStore) {
final var fungibleChanges = new HashMap<EntityIDPair, Long>();
for (final EntityIDPair modifiedRel : writableTokenRelStore.modifiedTokens()) {
final var relAcctId = modifiedRel.accountId();
final var relTokenId = modifiedRel.tokenId();
final var modifiedTokenRel = writableTokenRelStore.get(relAcctId, relTokenId);
final var persistedTokenRel = writableTokenRelStore.getOriginalValue(relAcctId, relTokenId);

// It's possible the modified token rel was created in this transaction. If so, use a persisted balance of 0
// for the token rel that didn't exist
final var persistedBalance = persistedTokenRel != null ? persistedTokenRel.balance() : 0;
// It is possible that the account is dissociated with the token in this transaction. If so, use a
// balance of 0 for the token rel that didn't exist
final var modifiedTokenRelBalance = modifiedTokenRel != null ? modifiedTokenRel.balance() : 0;
// Never allow a fungible token's balance to be negative
validateTrue(modifiedTokenRelBalance >= 0, FAIL_INVALID);

// If the token rel's balance has changed, add it to the list of changes
final var netFungibleChange = modifiedTokenRelBalance - persistedBalance;
if (netFungibleChange != 0) {
fungibleChanges.put(modifiedRel, netFungibleChange);
}
}

return fungibleChanges;
}

/**
* Given a map of {@link EntityIDPair} to {@link Long} representing the changes to the balances of the token
* relations, returns a list of {@link TokenTransferList} representing the changes to the token relations.
* @param fungibleChanges the map of {@link EntityIDPair} to {@link Long} representing the changes to the balances
* @return a list of {@link TokenTransferList} representing the changes to the token relations
*/
@NonNull
protected List<TokenTransferList> asTokenTransferListFrom(@NonNull final Map<EntityIDPair, Long> fungibleChanges) {
final var fungibleTokenTransferLists = new ArrayList<TokenTransferList>();
final var acctAmountsByTokenId = new HashMap<TokenID, HashMap<AccountID, Long>>();
for (final var fungibleChange : fungibleChanges.entrySet()) {
final var tokenIdOfAcctAmountChange = fungibleChange.getKey().tokenId();
final var accountIdOfAcctAmountChange = fungibleChange.getKey().accountId();
if (!acctAmountsByTokenId.containsKey(tokenIdOfAcctAmountChange)) {
acctAmountsByTokenId.put(tokenIdOfAcctAmountChange, new HashMap<>());
}
if (fungibleChange.getValue() != 0) {
final var tokenIdMap = acctAmountsByTokenId.get(tokenIdOfAcctAmountChange);
tokenIdMap.merge(accountIdOfAcctAmountChange, fungibleChange.getValue(), Long::sum);
}
}
// Mold the fungible changes into a transfer ordered by (token ID, account ID). The fungible pairs are ordered
// by (accountId, tokenId), so we need to group by each token ID
for (final var acctAmountsForToken : acctAmountsByTokenId.entrySet()) {
final var singleTokenTransfers = acctAmountsForToken.getValue();
if (!singleTokenTransfers.isEmpty()) {
final var aaList = asAccountAmounts(singleTokenTransfers);
aaList.sort(ACCOUNT_AMOUNT_COMPARATOR);
fungibleTokenTransferLists.add(TokenTransferList.newBuilder()
.token(acctAmountsForToken.getKey())
.transfers(aaList)
.build());
}
}

return fungibleTokenTransferLists;
}

/**
* Gets all nft ownership changes for all modified nfts from the given {@link WritableNftStore}.
* @param writableNftStore the {@link WritableNftStore} to get the nft ownership changes from
* @return a {@link Map} of {@link TokenID} to {@link List} of {@link NftTransfer} representing the nft ownership
*/
@NonNull
protected Map<TokenID, List<NftTransfer>> nftChangesFrom(@NonNull final WritableNftStore writableNftStore) {
final var nftChanges = new HashMap<TokenID, List<NftTransfer>>();
for (final NftID nftId : writableNftStore.modifiedNfts()) {
final var modifiedNft = writableNftStore.get(nftId);
final var persistedNft = writableNftStore.getOriginalValue(nftId);

// The NFT may not have existed before, in which case we'll use a null sender account ID
final var senderAccountId = persistedNft != null ? persistedNft.ownerId() : null;
// If the NFT has been burned or wiped, modifiedNft will be null. In that case the receiverId
// will be explicitly set as 0.0.0
final var builder = NftTransfer.newBuilder();
if (modifiedNft != null) {
builder.receiverAccountID(modifiedNft.ownerId());
} else {
builder.receiverAccountID(ZERO_ACCOUNT_ID);
}
final var nftTransfer = builder.serialNumber(nftId.serialNumber())
.senderAccountID(senderAccountId)
.build();

if (!nftChanges.containsKey(nftId.tokenId())) {
nftChanges.put(nftId.tokenId(), new ArrayList<>());
}

final var currentNftChanges = nftChanges.get(nftId.tokenId());
currentNftChanges.add(nftTransfer);
nftChanges.put(nftId.tokenId(), currentNftChanges);
}
return nftChanges;
}

/**
* Given a {@link Map} of {@link TokenID} to {@link List} of {@link NftTransfer} representing the nft ownership
* changes, returns a list of {@link TokenTransferList} representing the changes to the nft ownership.
* @param nftChanges the {@link Map} of {@link TokenID} to {@link List} of {@link NftTransfer} representing the nft
* @return a list of {@link TokenTransferList} representing the changes to the nft ownership
*/
protected List<TokenTransferList> asTokenTransferListFromNftChanges(
final Map<TokenID, List<NftTransfer>> nftChanges) {

// Create a new transfer list for each token ID
final var nftTokenTransferLists = new ArrayList<TokenTransferList>();
for (final var nftsForTokenId : nftChanges.entrySet()) {
if (!nftsForTokenId.getValue().isEmpty()) {
// This var is the collection of all NFT transfers _for a single token ID_
// NFT serial numbers will not be sorted, instead will be displayed in the order they were added in
// transaction
final var nftTransfersForTokenId = nftsForTokenId.getValue();
nftTokenTransferLists.add(TokenTransferList.newBuilder()
.token(nftsForTokenId.getKey())
.nftTransfers(nftTransfersForTokenId)
.build());
}
}

return nftTokenTransferLists;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.hedera.node.app.service.token.impl;

import static com.hedera.node.app.service.evm.accounts.HederaEvmContractAliases.EVM_ADDRESS_LEN;
import static com.hedera.node.app.service.mono.utils.EntityIdUtils.isOfEvmAddressSize;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
Expand Down Expand Up @@ -213,4 +214,39 @@ public Set<AccountID> modifiedAccountsInState() {
public Set<Bytes> modifiedAliasesInState() {
return aliases().modifiedKeys();
}

/**
* Gets the original value associated with the given accountId before any modifications were made to
* it. The returned value will be {@code null} if the accountId does not exist.
*
* @param id The accountId. Cannot be null, otherwise an exception is thrown.
* @return The original value, or null if there is no such accountId in the state
* @throws NullPointerException if the accountId is null.
*/
@Nullable
public Account getOriginalValue(@NonNull final AccountID id) {
requireNonNull(id);
// Get the account number based on the account identifier. It may be null.
final var accountOneOf = id.account();
final Long accountNum =
switch (accountOneOf.kind()) {
case ACCOUNT_NUM -> accountOneOf.as();
case ALIAS -> {
final Bytes alias = accountOneOf.as();
if (isOfEvmAddressSize(alias) && isMirror(alias)) {
yield fromMirror(alias);
} else {
final var entityNum = aliases().getOriginalValue(alias);
yield entityNum == null ? 0L : entityNum.accountNum();
}
}
case UNSET -> 0L;
};

return accountNum == null
? null
: accountState()
.getOriginalValue(
AccountID.newBuilder().accountNum(accountNum).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,18 @@ public void remove(final @NonNull NftID serialNum) {
public void remove(final @NonNull TokenID tokenId, final long serialNum) {
remove(NftID.newBuilder().tokenId(tokenId).serialNumber(serialNum).build());
}

/**
* Gets the original value associated with the given nftId before any modifications were made to
* it. The returned value will be {@code null} if the nftId does not exist.
*
* @param nftId The nftId. Cannot be null, otherwise an exception is thrown.
* @return The original value, or null if there is no such nftId in the state
* @throws NullPointerException if the accountId is null.
*/
@Nullable
public Nft getOriginalValue(@NonNull final NftID nftId) {
requireNonNull(nftId);
return nftState.getOriginalValue(nftId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@ public void put(final long nodeId, @NonNull final StakingNodeInfo stakingNodeInf
requireNonNull(stakingNodeInfo);
stakingInfoState.put(nodeId, stakingNodeInfo);
}

/**
* Gets the original value associated with the given nodeId before any modifications were made to
* it. The returned value will be {@code null} if the nodeId does not exist.
*
* @param nodeId The nftId.
* @return The original value, or null if there is no such nftId in the state
*/
@Nullable
public StakingNodeInfo getOriginalValue(final long nodeId) {
return stakingInfoState.getOriginalValue(nodeId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ public TokenRelation getForModify(@NonNull final AccountID accountId, @NonNull f
EntityIDPair.newBuilder().accountId(accountId).tokenId(tokenId).build());
}

/**
* Gets the original value associated with the given tokenRelation before any modifications were made to
* it. The returned value will be {@code null} if the tokenRelation does not exist.
*
* @param accountId The accountId of tokenRelation.
* @param tokenId The tokenId of tokenRelation.
* @return The original value, or null if there is no such tokenRelation in the state
*/
@Nullable
public TokenRelation getOriginalValue(@NonNull final AccountID accountId, @NonNull final TokenID tokenId) {
requireNonNull(accountId);
requireNonNull(tokenId);
return tokenRelState.getOriginalValue(
EntityIDPair.newBuilder().accountId(accountId).tokenId(tokenId).build());
}

/**
* @return the set of token relations modified in existing state
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.hedera.node.app.spi.state.WritableKVState;
import com.hedera.node.app.spi.state.WritableStates;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -88,4 +89,17 @@ public long sizeOfState() {
public Set<TokenID> modifiedTokens() {
return tokenState.modifiedKeys();
}

/**
* Gets the original value associated with the given tokenId before any modifications were made to
* it. The returned value will be {@code null} if the tokenId does not exist.
*
* @param tokenId The tokenId.
* @return The original value, or null if there is no such tokenId in the state
*/
@Nullable
public Token getOriginalValue(@NonNull final TokenID tokenId) {
requireNonNull(tokenId);
return tokenState.getOriginalValue(tokenId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import static com.hedera.node.app.spi.HapiUtils.ACCOUNT_ID_COMPARATOR;

import com.hedera.hapi.node.base.AccountAmount;
import com.hedera.hapi.node.base.NftTransfer;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenTransferList;
import java.util.Comparator;
Expand All @@ -32,8 +31,6 @@ private TokenComparators() {

public static final Comparator<AccountAmount> ACCOUNT_AMOUNT_COMPARATOR =
Comparator.comparing(AccountAmount::accountID, ACCOUNT_ID_COMPARATOR);
public static final Comparator<NftTransfer> NFT_TRANSFER_COMPARATOR =
Comparator.comparingLong(NftTransfer::serialNumber);
public static final Comparator<TokenID> TOKEN_ID_COMPARATOR = Comparator.comparingLong(TokenID::tokenNum);
public static final Comparator<TokenTransferList> TOKEN_TRANSFER_LIST_COMPARATOR =
(o1, o2) -> Objects.compare(o1.token(), o2.token(), TOKEN_ID_COMPARATOR);
Expand Down
Loading

0 comments on commit 55bef50

Please sign in to comment.