3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

For Balance Recorder, reward recordings only, that is the default.

This commit is contained in:
kennycud 2024-12-25 13:24:24 -08:00
parent c71f5fa8bf
commit 4f0aabfb36
7 changed files with 767 additions and 57 deletions

View File

@ -283,7 +283,7 @@ public class AssetsResource {
Optional<HSQLDBBalanceRecorder> recorder = HSQLDBBalanceRecorder.getInstance();
if( recorder.isPresent()) {
Optional<BlockHeightRangeAddressAmounts> addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end));
Optional<BlockHeightRangeAddressAmounts> addressAmounts = recorder.get().getAddressAmounts(new BlockHeightRange(begin, end, false));
if( addressAmounts.isPresent() ) {
return addressAmounts.get().getAmounts().stream()

View File

@ -12,12 +12,15 @@ public class BlockHeightRange {
private int end;
private boolean isRewardDistribution;
public BlockHeightRange() {
}
public BlockHeightRange(int begin, int end) {
public BlockHeightRange(int begin, int end, boolean isRewardDistribution) {
this.begin = begin;
this.end = end;
this.isRewardDistribution = isRewardDistribution;
}
public int getBegin() {
@ -28,6 +31,10 @@ public class BlockHeightRange {
return end;
}
public boolean isRewardDistribution() {
return isRewardDistribution;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -46,6 +53,7 @@ public class BlockHeightRange {
return "BlockHeightRange{" +
"begin=" + begin +
", end=" + end +
", isRewardDistribution=" + isRewardDistribution +
'}';
}
}

View File

@ -3,6 +3,7 @@ package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.api.SearchMode;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.Controller;
@ -14,7 +15,10 @@ import org.qortal.data.arbitrary.ArbitraryResourceCache;
import org.qortal.data.arbitrary.ArbitraryResourceData;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.BalanceRecorderUtils;
@ -434,37 +438,11 @@ public class HSQLDBCacheUtils {
// if there is a prior height
if(priorHeight.isPresent()) {
BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight);
boolean isRewardDistribution = BalanceRecorderUtils.isRewardDistributionRange(priorHeight.get(), currentHeight);
LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange);
List<AccountBalanceData> currentBalances = balancesByHeight.get(currentHeight);
List<AddressAmountData> currentDynamics
= BalanceRecorderUtils.buildBalanceDynamics(
currentBalances,
balancesByHeight.get(priorHeight.get()),
Settings.getInstance().getMinimumBalanceRecording());
LOGGER.debug("dynamics built: count = " + currentDynamics.size());
if(LOGGER.isDebugEnabled())
currentDynamics.stream()
.sorted(Comparator.comparingLong(AddressAmountData::getAmount).reversed())
.limit(Settings.getInstance().getTopBalanceLoggingLimit())
.forEach(top5Dynamic -> LOGGER.debug("Top Dynamics = " + top5Dynamic));
BlockHeightRangeAddressAmounts amounts
= new BlockHeightRangeAddressAmounts( blockHeightRange, currentDynamics );
balanceDynamics.add(amounts);
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight - Settings.getInstance().getBalanceRecorderRollbackAllowance(), balancesByHeight);
while(balanceDynamics.size() > capacity) {
BlockHeightRangeAddressAmounts oldestDynamics = BalanceRecorderUtils.removeOldestDynamics(balanceDynamics);
LOGGER.debug("removing oldest dynamics: range " + oldestDynamics.getRange());
// if this range has a reward recording block or if other blocks are enabled for recording
if( isRewardDistribution || !Settings.getInstance().isRewardRecordingOnly() ) {
produceBalanceDynamics(currentHeight, priorHeight, isRewardDistribution, balancesByHeight, balanceDynamics, capacity);
}
}
else {
@ -482,6 +460,69 @@ public class HSQLDBCacheUtils {
timer.scheduleAtFixedRate(task, 300_000, frequency * 60_000);
}
private static void produceBalanceDynamics(int currentHeight, Optional<Integer> priorHeight, boolean isRewardDistribution, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight, CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> balanceDynamics, int capacity) {
BlockHeightRange blockHeightRange = new BlockHeightRange(priorHeight.get(), currentHeight, isRewardDistribution);
LOGGER.debug("building dynamics for block heights: range = " + blockHeightRange);
List<AccountBalanceData> currentBalances = balancesByHeight.get(currentHeight);
ArrayList<TransactionData> transactions = getTransactionDataForBlocks(blockHeightRange);
LOGGER.info("transactions counted for balance adjustments: count = " + transactions.size());
List<AddressAmountData> currentDynamics
= BalanceRecorderUtils.buildBalanceDynamics(
currentBalances,
balancesByHeight.get(priorHeight.get()),
Settings.getInstance().getMinimumBalanceRecording(),
transactions);
LOGGER.debug("dynamics built: count = " + currentDynamics.size());
if(LOGGER.isDebugEnabled())
currentDynamics.stream()
.sorted(Comparator.comparingLong(AddressAmountData::getAmount).reversed())
.limit(Settings.getInstance().getTopBalanceLoggingLimit())
.forEach(top5Dynamic -> LOGGER.debug("Top Dynamics = " + top5Dynamic));
BlockHeightRangeAddressAmounts amounts
= new BlockHeightRangeAddressAmounts( blockHeightRange, currentDynamics );
balanceDynamics.add(amounts);
BalanceRecorderUtils.removeRecordingsBelowHeight(currentHeight - Settings.getInstance().getBalanceRecorderRollbackAllowance(), balancesByHeight);
while(balanceDynamics.size() > capacity) {
BlockHeightRangeAddressAmounts oldestDynamics = BalanceRecorderUtils.removeOldestDynamics(balanceDynamics);
LOGGER.debug("removing oldest dynamics: range " + oldestDynamics.getRange());
}
}
private static ArrayList<TransactionData> getTransactionDataForBlocks(BlockHeightRange blockHeightRange) {
ArrayList<TransactionData> transactions;
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures
= repository.getTransactionRepository().getSignaturesMatchingCriteria(
blockHeightRange.getBegin() + 1, blockHeightRange.getEnd() - blockHeightRange.getBegin(),
null, null,null, null, null,
TransactionsResource.ConfirmationStatus.CONFIRMED,
null, null, null);
transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
LOGGER.debug(String.format("Found %s transactions for " + blockHeightRange, transactions.size()));
} catch (Exception e) {
transactions = new ArrayList<>(0);
LOGGER.warn("Problems getting transactions for balance recording: " + e.getMessage());
}
return transactions;
}
private static int recordCurrentBalances(ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
int currentHeight;

View File

@ -494,7 +494,14 @@ public class Settings {
*/
private int balanceRecorderRollbackAllowance = 100;
// Domain mapping
/**
* Is Reward Recording Only
*
* Set true to only retain the recordings that cover reward distributions, otherwise set false.
*/
private boolean rewardRecordingOnly = true;
// Domain mapping
public static class ThreadLimit {
private String messageType;
private Integer limit;
@ -1311,4 +1318,8 @@ public class Settings {
public int getBalanceRecorderRollbackAllowance() {
return balanceRecorderRollbackAllowance;
}
public boolean isRewardRecordingOnly() {
return rewardRecordingOnly;
}
}

View File

@ -1,13 +1,26 @@
package org.qortal.utils;
import org.qortal.block.Block;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.CreateAssetOrderTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MultiPaymentTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@ -67,22 +80,190 @@ public class BalanceRecorderUtils {
}
}
public static List<AddressAmountData> buildBalanceDynamics(final List<AccountBalanceData> balances, final List<AccountBalanceData> priorBalances, long minimum) {
public static List<AddressAmountData> buildBalanceDynamics(
final List<AccountBalanceData> balances,
final List<AccountBalanceData> priorBalances,
long minimum,
List<TransactionData> transactions) {
List<AddressAmountData> addressAmounts = new ArrayList<>(balances.size());
Map<String, Long> amountsByAddress = new HashMap<>(transactions.size());
// prior balance
addressAmounts.addAll(
balances.stream()
for( TransactionData transactionData : transactions ) {
mapBalanceModificationsForTransaction(amountsByAddress, transactionData);
}
List<AddressAmountData> addressAmounts
= balances.stream()
.map(balance -> buildBalanceDynamicsForAccount(priorBalances, balance))
.map( data -> adjustAddressAmount(amountsByAddress.getOrDefault(data.getAddress(), 0L), data))
.filter(ADDRESS_AMOUNT_DATA_NOT_ZERO)
.filter( data -> data.getAmount() >= minimum)
.collect(Collectors.toList())
);
.filter(data -> data.getAmount() >= minimum)
.collect(Collectors.toList());
return addressAmounts;
}
public static AddressAmountData adjustAddressAmount(long adjustment, AddressAmountData data) {
return new AddressAmountData(data.getAddress(), data.getAmount() - adjustment);
}
public static void mapBalanceModificationsForTransaction(Map<String, Long> amountsByAddress, TransactionData transactionData) {
String creatorAddress;
// AT Transaction
if( transactionData instanceof ATTransactionData) {
creatorAddress = mapBalanceModificationsForAtTransaction(amountsByAddress, (ATTransactionData) transactionData);
}
// Buy Name Transaction
else if( transactionData instanceof BuyNameTransactionData) {
creatorAddress = mapBalanceModificationsForBuyNameTransaction(amountsByAddress, (BuyNameTransactionData) transactionData);
}
// Create Asset Order Transaction
else if( transactionData instanceof CreateAssetOrderTransactionData) {
//TODO I'm not sure how to handle this one. This hasn't been used at this point in the blockchain.
creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey());
}
// Deploy AT Transaction
else if( transactionData instanceof DeployAtTransactionData ) {
creatorAddress = mapBalanceModificationsForDeployAtTransaction(amountsByAddress, (DeployAtTransactionData) transactionData);
}
// Multi Payment Transaction
else if( transactionData instanceof MultiPaymentTransactionData) {
creatorAddress = mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress, (MultiPaymentTransactionData) transactionData);
}
// Payment Transaction
else if( transactionData instanceof PaymentTransactionData ) {
creatorAddress = mapBalanceModicationsForPaymentTransaction(amountsByAddress, (PaymentTransactionData) transactionData);
}
// Transfer Asset Transaction
else if( transactionData instanceof TransferAssetTransactionData) {
creatorAddress = mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, (TransferAssetTransactionData) transactionData);
}
// Other Transactions
else {
creatorAddress = Crypto.toAddress(transactionData.getCreatorPublicKey());
}
// all transactions modify the balance for fees
mapBalanceModifications(amountsByAddress, transactionData.getFee(), creatorAddress, Optional.empty());
}
public static String mapBalanceModificationsForTransferAssetTransaction(Map<String, Long> amountsByAddress, TransferAssetTransactionData transferAssetData) {
String creatorAddress = Crypto.toAddress(transferAssetData.getSenderPublicKey());
if( transferAssetData.getAssetId() == 0) {
mapBalanceModifications(
amountsByAddress,
transferAssetData.getAmount(),
creatorAddress,
Optional.of(transferAssetData.getRecipient())
);
}
return creatorAddress;
}
public static String mapBalanceModicationsForPaymentTransaction(Map<String, Long> amountsByAddress, PaymentTransactionData paymentData) {
String creatorAddress = Crypto.toAddress(paymentData.getCreatorPublicKey());
mapBalanceModifications(amountsByAddress,
paymentData.getAmount(),
creatorAddress,
Optional.of(paymentData.getRecipient())
);
return creatorAddress;
}
public static String mapBalanceModificationsForMultiPaymentTransaction(Map<String, Long> amountsByAddress, MultiPaymentTransactionData multiPaymentData) {
String creatorAddress = Crypto.toAddress(multiPaymentData.getCreatorPublicKey());
for(PaymentData payment : multiPaymentData.getPayments() ) {
mapBalanceModificationsForTransaction(
amountsByAddress,
getPaymentTransactionData(multiPaymentData, payment)
);
}
return creatorAddress;
}
public static String mapBalanceModificationsForDeployAtTransaction(Map<String, Long> amountsByAddress, DeployAtTransactionData transactionData) {
String creatorAddress;
DeployAtTransactionData deployAtData = transactionData;
creatorAddress = Crypto.toAddress(deployAtData.getCreatorPublicKey());
if( deployAtData.getAssetId() == 0 ) {
mapBalanceModifications(
amountsByAddress,
deployAtData.getAmount(),
creatorAddress,
Optional.of(deployAtData.getAtAddress())
);
}
return creatorAddress;
}
public static String mapBalanceModificationsForBuyNameTransaction(Map<String, Long> amountsByAddress, BuyNameTransactionData transactionData) {
String creatorAddress;
BuyNameTransactionData buyNameData = transactionData;
creatorAddress = Crypto.toAddress(buyNameData.getCreatorPublicKey());
mapBalanceModifications(
amountsByAddress,
buyNameData.getAmount(),
creatorAddress,
Optional.of(buyNameData.getSeller())
);
return creatorAddress;
}
public static String mapBalanceModificationsForAtTransaction(Map<String, Long> amountsByAddress, ATTransactionData transactionData) {
String creatorAddress;
ATTransactionData atData = transactionData;
creatorAddress = atData.getATAddress();
if( atData.getAssetId() != null && atData.getAssetId() == 0) {
mapBalanceModifications(
amountsByAddress,
atData.getAmount(),
creatorAddress,
Optional.of(atData.getRecipient())
);
}
return creatorAddress;
}
public static PaymentTransactionData getPaymentTransactionData(MultiPaymentTransactionData multiPaymentData, PaymentData payment) {
return new PaymentTransactionData(
new BaseTransactionData(
multiPaymentData.getTimestamp(),
multiPaymentData.getTxGroupId(),
multiPaymentData.getReference(),
multiPaymentData.getCreatorPublicKey(),
0L,
multiPaymentData.getSignature()
),
payment.getRecipient(),
payment.getAmount()
);
}
public static void mapBalanceModifications(Map<String, Long> amountsByAddress, Long amount, String sender, Optional<String> recipient) {
amountsByAddress.put(
sender,
amountsByAddress.getOrDefault(sender, 0L) - amount
);
if( recipient.isPresent() )
amountsByAddress.put(
recipient.get(),
amountsByAddress.getOrDefault(recipient.get(), 0L) + amount
);
}
public static void removeRecordingsAboveHeight(int currentHeight, ConcurrentHashMap<Integer, List<AccountBalanceData>> balancesByHeight) {
balancesByHeight.entrySet().stream()
.filter(heightWithBalances -> heightWithBalances.getKey() > currentHeight)
@ -116,4 +297,23 @@ public class BalanceRecorderUtils {
.sorted(Comparator.reverseOrder()).findFirst();
return priorHeight;
}
/**
* Is Reward Distribution Range?
*
* @param start start height, exclusive
* @param end end height, inclusive
*
* @return true there is a reward distribution block within this block range
*/
public static boolean isRewardDistributionRange(int start, int end) {
// iterate through the block height until a reward distribution block or the end of the range
for( int i = start + 1; i <= end; i++) {
if( Block.isRewardDistributionBlock(i) ) return true;
}
// no reward distribution blocks found within range
return false;
}
}

View File

@ -2,13 +2,27 @@ package org.qortal.test.utils;
import org.junit.Assert;
import org.junit.Test;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AddressAmountData;
import org.qortal.data.account.BlockHeightRange;
import org.qortal.data.account.BlockHeightRangeAddressAmounts;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.BuyNameTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MultiPaymentTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.utils.BalanceRecorderUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -18,6 +32,10 @@ import java.util.stream.Collectors;
public class BalanceRecorderUtilsTests {
public static final String RECIPIENT_ADDRESS = "recipient";
public static final String AT_ADDRESS = "atAddress";
public static final String OTHER = "Other";
@Test
public void testNotZeroForZero() {
boolean test = BalanceRecorderUtils.ADDRESS_AMOUNT_DATA_NOT_ZERO.test( new AddressAmountData("", 0));
@ -42,8 +60,8 @@ public class BalanceRecorderUtilsTests {
@Test
public void testAddressAmountComparatorReverseOrder() {
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0));
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
@ -53,8 +71,8 @@ public class BalanceRecorderUtilsTests {
@Test
public void testAddressAmountComparatorForwardOrder() {
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts1 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 2, false), new ArrayList<>(0));
BlockHeightRangeAddressAmounts addressAmounts2 = new BlockHeightRangeAddressAmounts(new BlockHeightRange(2, 3, false), new ArrayList<>(0));
int compare = BalanceRecorderUtils.BLOCK_HEIGHT_RANGE_ADDRESS_AMOUNTS_COMPARATOR.compare(addressAmounts1, addressAmounts2);
@ -124,7 +142,7 @@ public class BalanceRecorderUtilsTests {
List<AccountBalanceData> priorBalances = new ArrayList<>(1);
priorBalances.add(new AccountBalanceData(address, 0, 1));
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0);
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
@ -145,7 +163,7 @@ public class BalanceRecorderUtilsTests {
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0);
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, 0, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
@ -156,6 +174,55 @@ public class BalanceRecorderUtilsTests {
Assert.assertEquals(2, addressAmountData.getAmount());
}
@Test
public void testBuildBalanceDynamicOneAccountAdjustment() {
List<AccountBalanceData> balances = new ArrayList<>(1);
balances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 20));
List<AccountBalanceData> priorBalances = new ArrayList<>(0);
priorBalances.add(new AccountBalanceData(RECIPIENT_ADDRESS, 0, 12));
List<TransactionData> transactions = new ArrayList<>();
final long amount = 5L;
final long fee = 1L;
boolean exceptionThrown = false;
try {
byte[] creatorPublicKey = TestUtils.generatePublicKey();
PaymentTransactionData paymentData
= new PaymentTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount
);
transactions.add(paymentData);
List<AddressAmountData> dynamics
= BalanceRecorderUtils.buildBalanceDynamics(
balances,
priorBalances,
0,
transactions
);
Assert.assertNotNull(dynamics);
Assert.assertEquals(1, dynamics.size());
AddressAmountData addressAmountData = dynamics.get(0);
Assert.assertNotNull(addressAmountData);
Assert.assertEquals(RECIPIENT_ADDRESS, addressAmountData.getAddress());
Assert.assertEquals(3, addressAmountData.getAmount());
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testBuildBalanceDynamicsTwoAccountsNegativeValues() {
@ -170,7 +237,7 @@ public class BalanceRecorderUtilsTests {
priorBalances.add(new AccountBalanceData(address2, 0, 200));
priorBalances.add(new AccountBalanceData(address1, 0, 5000));
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L);
List<AddressAmountData> dynamics = BalanceRecorderUtils.buildBalanceDynamics(balances, priorBalances, -100L, new ArrayList<>(0));
Assert.assertNotNull(dynamics);
Assert.assertEquals(2, dynamics.size());
@ -304,10 +371,10 @@ public class BalanceRecorderUtilsTests {
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
BlockHeightRange range1 = new BlockHeightRange(10, 20);
BlockHeightRange range1 = new BlockHeightRange(10, 20, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
BlockHeightRange range2 = new BlockHeightRange(1, 4);
BlockHeightRange range2 = new BlockHeightRange(1, 4, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range2, new ArrayList<>()));
Assert.assertEquals(2, dynamics.size());
@ -323,13 +390,13 @@ public class BalanceRecorderUtilsTests {
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
BlockHeightRange range1 = new BlockHeightRange(1,5);
BlockHeightRange range1 = new BlockHeightRange(1,5, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range1, new ArrayList<>()));
BlockHeightRange range2 = new BlockHeightRange(6, 11);
BlockHeightRange range2 = new BlockHeightRange(6, 11, false);
dynamics.add((new BlockHeightRangeAddressAmounts(range2, new ArrayList<>())));
BlockHeightRange range3 = new BlockHeightRange(22, 16);
BlockHeightRange range3 = new BlockHeightRange(22, 16, false);
dynamics.add(new BlockHeightRangeAddressAmounts(range3, new ArrayList<>()));
Assert.assertEquals(3, dynamics.size());
@ -344,18 +411,353 @@ public class BalanceRecorderUtilsTests {
public void testRemoveOldestDynamicsTwice() {
CopyOnWriteArrayList<BlockHeightRangeAddressAmounts> dynamics = new CopyOnWriteArrayList<>();
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5), new ArrayList<>()));
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9), new ArrayList<>()));
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(1, 5, false), new ArrayList<>()));
dynamics.add(new BlockHeightRangeAddressAmounts(new BlockHeightRange(5, 9, false), new ArrayList<>()));
Assert.assertEquals(2, dynamics.size());
BalanceRecorderUtils.removeOldestDynamics(dynamics);
Assert.assertEquals(1, dynamics.size());
Assert.assertTrue(dynamics.get(0).getRange().equals(new BlockHeightRange(5, 9)));
Assert.assertTrue(dynamics.get(0).getRange().equals(new BlockHeightRange(5, 9, false)));
BalanceRecorderUtils.removeOldestDynamics(dynamics);
Assert.assertEquals(0, dynamics.size());
}
@Test
public void testMapBalanceModificationsForPaymentTransaction() {
boolean exceptionThrown = false;
try {
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
PaymentTransactionData paymentData
= new PaymentTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount
);
// map balance modifications for addresses in the transaction
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModicationsForPaymentTransaction(amountsByAddress, paymentData);
// this will not add the fee, that is done in a different place
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForAssetOrderTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
TransferAssetTransactionData transferAssetData
= new TransferAssetTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
RECIPIENT_ADDRESS,
amount,
0
);
// map balance modifications for addresses in the transaction
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModificationsForTransferAssetTransaction(amountsByAddress, transferAssetData);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForATTransactionMessageType() {
boolean exceptionThrown = false;
try {
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
ATTransactionData atTransactionData = new ATTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS,
RECIPIENT_ADDRESS,
new byte[0]);
BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData);
// no balance changes for AT message
Assert.assertTrue(amountsByAddress.size() == 0);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForATTransactionPaymentType() {
boolean exceptionThrown = false;
try{
final long amount = 1L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
ATTransactionData atTransactionData
= new ATTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS,
RECIPIENT_ADDRESS,
amount,
0
);
BalanceRecorderUtils.mapBalanceModificationsForAtTransaction( amountsByAddress, atTransactionData);
assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS);
assertAmountByAddress(amountsByAddress, -amount, AT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForBuyNameTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
BuyNameTransactionData buyNameData
= new BuyNameTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
"null",
amount,
RECIPIENT_ADDRESS
);
BalanceRecorderUtils.mapBalanceModificationsForBuyNameTransaction(amountsByAddress, buyNameData);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsOneAddress() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountsByAddress(amountsByAddress, 2*amount, creatorPublicKey, RECIPIENT_ADDRESS);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForMultiPaymentTransaction2PaymentsTwoAddresses() {
boolean exceptionThrown = false;
try{
final long amount = 100L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
List<PaymentData> payments = new ArrayList<>();
payments.add(new PaymentData(RECIPIENT_ADDRESS, 0, amount));
payments.add(new PaymentData(OTHER, 0, amount));
MultiPaymentTransactionData multiPayment
= new MultiPaymentTransactionData(new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
payments);
BalanceRecorderUtils.mapBalanceModificationsForMultiPaymentTransaction(amountsByAddress,multiPayment);
assertAmountByAddress(amountsByAddress, amount, RECIPIENT_ADDRESS);
assertAmountByAddress(amountsByAddress, amount, OTHER);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, 2*-amount, creatorAddress);
} catch( Exception e ) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForDeployAtTransaction() {
boolean exceptionThrown = false;
try{
final long amount = 3L;
final long fee = 1L;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
DeployAtTransactionData deployAt
= new DeployAtTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
AT_ADDRESS, "name", "description", "type", "tags", new byte[0], amount, Asset.QORT
);
BalanceRecorderUtils.mapBalanceModificationsForDeployAtTransaction(amountsByAddress,deployAt);
assertAmountsByAddress(amountsByAddress, amount, creatorPublicKey, AT_ADDRESS);
} catch( Exception e) {
exceptionThrown = true;
e.printStackTrace();
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testMapBalanceModificationsForTransaction() {
boolean exceptionThrown = false;
try {
final long fee = 2;
byte[] creatorPublicKey = TestUtils.generatePublicKey();
Map<String, Long> amountsByAddress = new HashMap<>();
BalanceRecorderUtils.mapBalanceModificationsForTransaction(
amountsByAddress,
new RegisterNameTransactionData(
new BaseTransactionData(0L, 0, null, creatorPublicKey, fee, null),
"aaa", "data", "aaa")
);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, -fee, creatorAddress);
} catch(Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testBlockHeightRangeEqualityTrue() {
BlockHeightRange range1 = new BlockHeightRange(2, 4, false);
BlockHeightRange range2 = new BlockHeightRange(2, 4, true);
Assert.assertTrue(range1.equals(range2));
Assert.assertEquals(range1, range2);
}
@Test
public void testBloHeightRangeEqualityFalse() {
BlockHeightRange range1 = new BlockHeightRange(2, 3, true);
BlockHeightRange range2 = new BlockHeightRange(2, 4, true);
Assert.assertFalse(range1.equals(range2));
}
private static void assertAmountsByAddress(Map<String, Long> amountsByAddress, long amount, byte[] creatorPublicKey, String recipientAddress) {
assertAmountByAddress(amountsByAddress, amount, recipientAddress);
String creatorAddress = Crypto.toAddress(creatorPublicKey);
assertAmountByAddress(amountsByAddress, -amount, creatorAddress);
}
private static void assertAmountByAddress(Map<String, Long> amountsByAddress, long amount, String address) {
Long amountForAddress = amountsByAddress.get(address);
Assert.assertTrue(amountsByAddress.containsKey(address));
Assert.assertNotNull(amountForAddress);
Assert.assertEquals(amount, amountForAddress.longValue());
}
}

View File

@ -0,0 +1,48 @@
package org.qortal.test.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Security;
public class TestUtils {
public static byte[] generatePublicKey() throws Exception {
// Add the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
// Generate a key pair
KeyPair keyPair = generateKeyPair();
// Get the public key
PublicKey publicKey = keyPair.getPublic();
// Get the public key as a byte array
byte[] publicKeyBytes = publicKey.getEncoded();
// Generate a RIPEMD160 message digest from the public key
byte[] ripeMd160Digest = generateRipeMd160Digest(publicKeyBytes);
return ripeMd160Digest;
}
public static KeyPair generateKeyPair() throws Exception {
// Generate a key pair using the RSA algorithm
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // Key size (bits)
return keyGen.generateKeyPair();
}
public static byte[] generateRipeMd160Digest(byte[] input) throws Exception {
// Create a RIPEMD160 message digest instance
MessageDigest ripeMd160 = MessageDigest.getInstance("RIPEMD160", new BouncyCastleProvider());
// Update the message digest with the input bytes
ripeMd160.update(input);
// Get the message digest bytes
return ripeMd160.digest();
}
}