Merge pull request #143 from AlphaX-Projects/merge-batch-payout

Merge batch payout
This commit is contained in:
QuickMythril 2023-11-15 21:48:44 -05:00 committed by GitHub
commit dbbe78263d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1207 additions and 85 deletions

View File

@ -370,83 +370,107 @@ public class Block {
int height = parentBlockData.getHeight() + 1;
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
// Fetch our list of online accounts, removing any that are missing a nonce
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
Long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
byte[] encodedOnlineAccounts = new byte[0];
int onlineAccountsCount = 0;
byte[] onlineAccountsSignatures = null;
if (isBatchRewardDistributionBlock(height)) {
// Batch reward distribution block - copy online accounts from recent block with highest online accounts count
// After feature trigger, remove any online accounts that are level 0
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
onlineAccounts.removeIf(a -> {
try {
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
} catch (DataException e) {
// Something went wrong, so remove the account
return true;
}
});
int firstBlock = height - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
int lastBlock = height - 1;
BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock);
encodedOnlineAccounts = highOnlineAccountsBlock.getEncodedOnlineAccounts();
onlineAccountsCount = highOnlineAccountsBlock.getOnlineAccountsCount();
// No point in copying signatures since these aren't revalidated, and because of this onlineAccountsTimestamp must be null too
onlineAccountsSignatures = null;
onlineAccountsTimestamp = null;
}
else if (isOnlineAccountsBlock(height)) {
// Standard online accounts block - add online accounts in regular way
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
}
// Fetch our list of online accounts, removing any that are missing a nonce
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
// was taking around 5000ms to query individually, vs 50ms using this approach.
List<byte[]> allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys();
// Map using index into sorted list of reward-shares as key
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
if (accountIndex == null)
// Online account (reward-share) with current timestamp but reward-share cancelled
continue;
indexedOnlineAccounts.put(accountIndex, onlineAccountData);
}
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
accountIndexes.sort(null);
// Convert to compressed integer set
ConciseSet onlineAccountsSet = new ConciseSet();
onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
int onlineAccountsCount = onlineAccountsSet.size();
// Collate all signatures
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
.stream()
.map(OnlineAccountData::getSignature)
.collect(Collectors.toList());
// Aggregated, single signature
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
// Add nonces to the end of the online accounts signatures
try {
// Create ordered list of nonce values
List<Integer> nonces = new ArrayList<>();
for (int i = 0; i < onlineAccountsCount; ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
nonces.add(onlineAccountData.getNonce());
// After feature trigger, remove any online accounts that are level 0
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
onlineAccounts.removeIf(a -> {
try {
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
} catch (DataException e) {
// Something went wrong, so remove the account
return true;
}
});
}
// Encode the nonces to a byte array
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
}
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
// was taking around 5000ms to query individually, vs 50ms using this approach.
List<byte[]> allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys();
// Map using index into sorted list of reward-shares as key
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
if (accountIndex == null)
// Online account (reward-share) with current timestamp but reward-share cancelled
continue;
indexedOnlineAccounts.put(accountIndex, onlineAccountData);
}
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
accountIndexes.sort(null);
// Convert to compressed integer set
ConciseSet onlineAccountsSet = new ConciseSet();
onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
onlineAccountsCount = onlineAccountsSet.size();
// Collate all signatures
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
.stream()
.map(OnlineAccountData::getSignature)
.collect(Collectors.toList());
// Aggregated, single signature
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
// Add nonces to the end of the online accounts signatures
try {
// Create ordered list of nonce values
List<Integer> nonces = new ArrayList<>();
for (int i = 0; i < onlineAccountsCount; ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
nonces.add(onlineAccountData.getNonce());
}
// Encode the nonces to a byte array
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
// Append the encoded nonces to the encoded online account signatures
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(onlineAccountsSignatures);
outputStream.write(encodedNonces);
onlineAccountsSignatures = outputStream.toByteArray();
} catch (TransformationException | IOException e) {
return null;
}
// Append the encoded nonces to the encoded online account signatures
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(onlineAccountsSignatures);
outputStream.write(encodedNonces);
onlineAccountsSignatures = outputStream.toByteArray();
}
catch (TransformationException | IOException e) {
return null;
else {
// No online accounts should be included in this block
onlineAccountsTimestamp = null;
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
@ -1058,6 +1082,40 @@ public class Block {
if (accountIndexes.size() != this.blockData.getOnlineAccountsCount())
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
// Online accounts should only be included in designated blocks; all others must be empty
if (!this.isOnlineAccountsBlock()) {
if (this.blockData.getOnlineAccountsCount() != 0 || accountIndexes.size() != 0) {
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
// Not a designated online accounts block and account count is 0. Everything is correct so no need to validate further.
return ValidationResult.OK;
}
// If this is a batch reward distribution block, ensure that online accounts have been copied from the correct previous block
if (this.isBatchRewardDistributionBlock()) {
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
int lastBlock = this.getBlockData().getHeight() - 1;
BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock);
if (this.blockData.getOnlineAccountsCount() != highOnlineAccountsBlock.getOnlineAccountsCount()) {
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
if (!Arrays.equals(this.blockData.getEncodedOnlineAccounts(), highOnlineAccountsBlock.getEncodedOnlineAccounts())) {
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
if (this.blockData.getOnlineAccountsSignatures() != null) {
// Signatures are excluded to reduce block size
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
if (this.blockData.getOnlineAccountsTimestamp() != null) {
// Online accounts timestamp must be null, because no signatures are included
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
// Online accounts have been correctly copied, and were already validated in earlier block, so consider them valid
return ValidationResult.OK;
}
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
if (onlineRewardShares == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
@ -1477,11 +1535,15 @@ public class Block {
LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight()));
if (this.blockData.getHeight() > 1) {
// Increase account levels
increaseAccountLevels();
// Distribute block rewards, including transaction fees, before transactions processed
processBlockRewards();
// Account levels and block rewards are only processed on block reward distribution blocks
if (this.isRewardDistributionBlock()) {
// Increase account levels
increaseAccountLevels();
// Distribute block rewards, including transaction fees, before transactions processed
processBlockRewards();
}
if (this.blockData.getHeight() == 212937)
// Apply fix for block 212937
@ -1541,9 +1603,13 @@ public class Block {
}
// Increase blocks minted count for all accounts
int delta = 1;
if (this.isBatchRewardDistributionActive()) {
delta = BlockChain.getInstance().getBlockRewardBatchSize();
}
// Batch update in repository
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +delta);
// Keep track of level bumps in case we need to apply to other entries
Map<String, Integer> bumpedAccounts = new HashMap<>();
@ -1593,8 +1659,32 @@ public class Block {
protected void processBlockRewards() throws DataException {
// General block reward
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
// Add transaction fees
if (this.isBatchRewardDistributionActive()) {
// Batch distribution is active - so multiply the reward by the batch size
reward *= BlockChain.getInstance().getBlockRewardBatchSize();
if (!this.isRewardDistributionBlock()) {
// Shouldn't ever happen, but checking here for safety
throw new DataException("Attempted to distribute a batch reward in a non-reward-distribution block");
}
// Add transaction fees since last distribution block
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1;
int lastBlock = this.blockData.getHeight() - 1;
Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock);
if (totalFees == null) {
throw new DataException("Unable to calculate total fees for block range");
}
reward += totalFees;
LOGGER.debug("Total fees for range {} - {} when processing: {}", firstBlock, lastBlock, totalFees);
}
// Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet)
reward += this.blockData.getTotalFees();
LOGGER.debug("Total fees when processing block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
LOGGER.debug("Block reward when processing block {}: {}", this.blockData.getHeight(), reward);
// Nothing to reward?
if (reward <= 0)
@ -1752,11 +1842,14 @@ public class Block {
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();
// Account levels and block rewards are only processed/orphaned on block reward distribution blocks
if (this.isRewardDistributionBlock()) {
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();
// Decrease account levels
decreaseAccountLevels();
// Decrease account levels
decreaseAccountLevels();
}
}
// Delete block from blockchain
@ -1832,8 +1925,32 @@ public class Block {
protected void orphanBlockRewards() throws DataException {
// General block reward
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
// Add transaction fees
if (this.isBatchRewardDistributionActive()) {
// Batch distribution is active - so multiply the reward by the batch size
reward *= BlockChain.getInstance().getBlockRewardBatchSize();
if (!this.isRewardDistributionBlock()) {
// Shouldn't ever happen, but checking here for safety
throw new DataException("Attempted to orphan batched rewards in a non-reward-distribution block");
}
// Add transaction fees since last distribution block
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1;
int lastBlock = this.blockData.getHeight() - 1;
Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock);
if (totalFees == null) {
throw new DataException("Unable to calculate total fees for block range");
}
reward += totalFees;
LOGGER.debug("Total fees for range {} - {} when orphaning: {}", firstBlock, lastBlock, totalFees);
}
// Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet)
reward += this.blockData.getTotalFees();
LOGGER.debug("Total fees when orphaning block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
LOGGER.debug("Block reward when orphaning block {}: {}", this.blockData.getHeight(), reward);
// Nothing to reward?
if (reward <= 0)
@ -1874,9 +1991,13 @@ public class Block {
}
// Decrease blocks minted count for all accounts
int delta = 1;
if (this.isBatchRewardDistributionActive()) {
delta = BlockChain.getInstance().getBlockRewardBatchSize();
}
// Batch update in repository
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -1);
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -delta);
for (AccountData accountData : allUniqueExpandedAccounts) {
// Adjust count locally (in Java)
@ -1899,6 +2020,105 @@ public class Block {
}
}
/**
* Specifies whether the batch reward feature trigger has activated yet.
* Note that the exact block of the feature trigger activation will return false,
* because this is actually the very last block with non-batched reward distributions.
*
* @return true if active, false if batch rewards feature trigger height not reached yet.
*/
public boolean isBatchRewardDistributionActive() {
return Block.isBatchRewardDistributionActive(this.blockData.getHeight());
}
public static boolean isBatchRewardDistributionActive(int height) {
// Once the getBlockRewardBatchStartHeight is reached, reward distributions per block must stop.
// Note the > instead of >= below, as the first batch distribution isn't until 1000 blocks *after* the
// start height. The block exactly matching the start height is not batched.
return height > BlockChain.getInstance().getBlockRewardBatchStartHeight();
}
/**
* Specifies whether rewards are distributed in this block, via ANY method (batch or single).
*
* @return true if rewards are to be distributed in this block.
*/
public boolean isRewardDistributionBlock() {
return Block.isRewardDistributionBlock(this.blockData.getHeight());
}
public static boolean isRewardDistributionBlock(int height) {
// Up to and *including* the start height (feature trigger), the rewards are distributed in every block
if (!Block.isBatchRewardDistributionActive(height)) {
return true;
}
// After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size
return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0;
}
/**
* Specifies whether BATCH rewards are distributed in this block.
*
* @return true if a batch distribution will occur, false if a single no distribution will occur.
*/
public boolean isBatchRewardDistributionBlock() {
return Block.isBatchRewardDistributionBlock(this.blockData.getHeight());
}
public static boolean isBatchRewardDistributionBlock(int height) {
// Up to and *including* the start height (feature trigger), batch reward distribution isn't active yet
if (!Block.isBatchRewardDistributionActive(height)) {
return false;
}
// After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size
return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0;
}
/**
* Specifies whether online accounts are to be included in this block.
*
* @return true if online accounts should be included, false if they should be excluded.
*/
public boolean isOnlineAccountsBlock() {
return Block.isOnlineAccountsBlock(this.getBlockData().getHeight());
}
private static boolean isOnlineAccountsBlock(int height) {
// After feature trigger, only certain blocks contain online accounts
if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) {
final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
return height >= (getNextBatchDistributionBlockHeight(height) - leadingBlockCount);
}
// Before feature trigger, all blocks contain online accounts
return true;
}
/**
*
* @param currentHeight
*
* @return the next height of a batch reward distribution. Must only be called after the
* batch reward feature trigger has activated. It is not useful prior to this.
*/
private static int getNextBatchDistributionBlockHeight(int currentHeight) {
final int batchSize = BlockChain.getInstance().getBlockRewardBatchSize();
if (currentHeight % batchSize == 0) {
// Already a reward distribution block
return currentHeight;
} else {
// Calculate the difference needed to reach the next distribution block
final int difference = batchSize - (currentHeight % batchSize);
return currentHeight + difference;
}
}
private static class BlockRewardCandidate {
public final String description;
public long share;

View File

@ -211,6 +211,19 @@ public class BlockChain {
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
private long mempowTransactionUpdatesTimestamp;
/** Feature trigger block height for batch block reward payouts.
* This MUST be a multiple of blockRewardBatchSize. Can't use
* featureTriggers because unit tests need to set this value via Reflection. */
private int blockRewardBatchStartHeight;
/** Block reward batch size. Must be (significantly) less than block prune size,
* as all blocks in the range need to be present in the repository when processing/orphaning */
private int blockRewardBatchSize;
/** Number of blocks prior to the batch reward distribution blocks to include online accounts
* data and to base online accounts decisions on. */
private int blockRewardBatchAccountsBlockCount;
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@ -367,6 +380,21 @@ public class BlockChain {
return this.onlineAccountsModulusV2Timestamp;
}
/* Block reward batching */
public long getBlockRewardBatchStartHeight() {
return this.blockRewardBatchStartHeight;
}
public int getBlockRewardBatchSize() {
return this.blockRewardBatchSize;
}
public int getBlockRewardBatchAccountsBlockCount() {
return this.blockRewardBatchAccountsBlockCount;
}
// Self sponsorship algo
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
@ -652,6 +680,22 @@ public class BlockChain {
if (totalShareV2 < 0 || totalShareV2 > 1_00000000L)
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
// Check that blockRewardBatchSize isn't zero
if (this.blockRewardBatchSize <= 0)
Settings.throwValidationError("\"blockRewardBatchSize\" must be greater than 0");
// Check that blockRewardBatchStartHeight is a multiple of blockRewardBatchSize
if (this.blockRewardBatchStartHeight % this.blockRewardBatchSize != 0)
Settings.throwValidationError("\"blockRewardBatchStartHeight\" must be a multiple of \"blockRewardBatchSize\"");
// Check that blockRewardBatchAccountsBlockCount isn't zero
if (this.blockRewardBatchAccountsBlockCount <= 0)
Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be greater than 0");
// Check that blockRewardBatchSize isn't zero
if (this.blockRewardBatchAccountsBlockCount > this.blockRewardBatchSize)
Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be less than or equal to \"blockRewardBatchSize\"");
}
/** Minor normalization, cached value generation, etc. */

View File

@ -12,6 +12,7 @@ import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
@ -36,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
// Minting new blocks
@ -126,10 +128,6 @@ public class BlockMinter extends Thread {
if (minLatestBlockTimestamp == null)
continue;
// No online accounts for current timestamp? (e.g. during startup)
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
continue;
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
// No minting accounts?
if (mintingAccountsData.isEmpty())
@ -545,6 +543,18 @@ public class BlockMinter extends Thread {
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
}
public static Block mintTestingBlockUnvalidatedWithoutOnlineAccounts(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
if (!BlockChain.getInstance().isTestChain())
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
// Make sure there are no online accounts
OnlineAccountsManager.getInstance().removeAllOnlineAccounts();
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
assertTrue(onlineAccounts.isEmpty());
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
}
public static Block mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();

View File

@ -780,6 +780,13 @@ public class OnlineAccountsManager {
}
// Utils
public void removeAllOnlineAccounts() {
this.currentOnlineAccounts.clear();
}
// Network handlers
public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {

View File

@ -189,10 +189,18 @@ public class BlockData implements Serializable {
return this.encodedOnlineAccounts;
}
public void setEncodedOnlineAccounts(byte[] encodedOnlineAccounts) {
this.encodedOnlineAccounts = encodedOnlineAccounts;
}
public int getOnlineAccountsCount() {
return this.onlineAccountsCount;
}
public void setOnlineAccountsCount(int onlineAccountsCount) {
this.onlineAccountsCount = onlineAccountsCount;
}
public Long getOnlineAccountsTimestamp() {
return this.onlineAccountsTimestamp;
}

View File

@ -132,6 +132,17 @@ public interface BlockRepository {
*/
public List<BlockData> getBlocks(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns blocks within height range.
*/
public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns block with highest online accounts count in specified range. If more than one block
* has the same high count, the oldest one is returned.
*/
public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns block summaries for the passed height range.
*/

View File

@ -356,6 +356,36 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException {
String sql = "SELECT SUM(total_fees) AS sum_total_fees FROM Blocks WHERE height BETWEEN ? AND ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) {
if (resultSet == null)
return null;
long totalFees = resultSet.getLong(1);
return totalFees;
} catch (SQLException e) {
throw new DataException("Error fetching total fees in block range from repository", e);
}
}
@Override
public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException {
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height BETWEEN ? AND ? "
+ "ORDER BY online_accounts_count DESC, height ASC "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error fetching highest online accounts block in range from repository", e);
}
}
@Override
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "

View File

@ -1017,7 +1017,9 @@ public class Settings {
}
public int getPruneBlockLimit() {
return this.pruneBlockLimit;
// Never prune more than twice the block reward batch size, as the data is needed when processing/orphaning
int minPruneBlockLimit = BlockChain.getInstance().getBlockRewardBatchSize() * 2;
return Math.max(this.pruneBlockLimit, minPruneBlockLimit);
}
public long getAtStatesPruneInterval() {

View File

@ -458,6 +458,10 @@ public class BlockTransformer extends Transformer {
}
public static ConciseSet decodeOnlineAccounts(byte[] encodedOnlineAccounts) {
if (encodedOnlineAccounts.length == 0) {
return new ConciseSet();
}
int[] words = new int[encodedOnlineAccounts.length / 4];
ByteBuffer.wrap(encodedOnlineAccounts).asIntBuffer().get(words);
return new ConciseSet(words, false);

View File

@ -31,6 +31,9 @@
"onlineAccountsModulusV2Timestamp": 1659801600000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
"mempowTransactionUpdatesTimestamp": 1693558800000,
"blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000,
"blockRewardBatchAccountsBlockCount": 25,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },

View File

@ -1,7 +1,9 @@
package org.qortal.test.common;
import com.google.common.primitives.Longs;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.Qortal25519Extras;
import org.qortal.data.network.OnlineAccountData;
@ -20,6 +22,7 @@ import java.security.SecureRandom;
import java.util.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.qortal.crypto.Qortal25519Extras.signForAggregation;
public class AccountUtils {
@ -107,6 +110,12 @@ public class AccountUtils {
return sponsees;
}
public static Account createRandomAccount(Repository repository) {
byte[] randomPublicKey = new byte[32];
new Random().nextBytes(randomPublicKey);
return new PublicKeyAccount(repository, randomPublicKey);
}
public static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException {
// Bob attempts to create a reward share transaction
byte[] randomPrivateKey = new byte[32];
@ -172,6 +181,24 @@ public class AccountUtils {
assertEquals(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), expectedBalance, actualBalance);
}
public static void assertBalanceGreaterThan(Repository repository, String accountName, long assetId, long minimumBalance) throws DataException {
long actualBalance = getBalance(repository, accountName, assetId);
String assetName = repository.getAssetRepository().fromAssetId(assetId).getName();
assertTrue(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), actualBalance > minimumBalance);
}
public static int getBlocksMinted(Repository repository, String accountName) throws DataException {
return Common.getTestAccount(repository, accountName).getBlocksMinted();
}
public static void assertBlocksMinted(Repository repository, String accountName, int expectedBlocksMinted) throws DataException {
int actualBlocksMinted = getBlocksMinted(repository, accountName);
assertEquals(String.format("%s's blocks minted incorrect", accountName), expectedBlocksMinted, actualBlocksMinted);
}
public static List<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
List<OnlineAccountData> onlineAccounts = new ArrayList<>();

View File

@ -10,6 +10,8 @@ import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import static org.junit.Assert.*;
public class BlockUtils {
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
@ -29,6 +31,20 @@ public class BlockUtils {
return block;
}
/** Mints a new block using "alice-reward-share" test account, via multiple re-orgs. */
public static Block mintBlockWithReorgs(Repository repository, int reorgCount) throws DataException {
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
Block block;
for (int i=0; i<reorgCount; i++) {
block = BlockMinter.mintTestingBlock(repository, mintingAccount);
assertNotNull(block);
BlockUtils.orphanLastBlock(repository);
}
return BlockMinter.mintTestingBlock(repository, mintingAccount);
}
public static Long getNextBlockReward(Repository repository) throws DataException {
int currentHeight = repository.getBlockRepository().getBlockchainHeight();
@ -70,4 +86,23 @@ public class BlockUtils {
} while (true);
}
public static void assertEqual(BlockData block1, BlockData block2) {
assertArrayEquals(block1.getSignature(), block2.getSignature());
assertEquals(block1.getVersion(), block2.getVersion());
assertArrayEquals(block1.getReference(), block2.getReference());
assertEquals(block1.getTransactionCount(), block2.getTransactionCount());
assertEquals(block1.getTotalFees(), block2.getTotalFees());
assertArrayEquals(block1.getTransactionsSignature(), block2.getTransactionsSignature());
// assertEquals(block1.getHeight(), block2.getHeight()); // Height not automatically included after deserialization
assertEquals(block1.getTimestamp(), block2.getTimestamp());
assertArrayEquals(block1.getMinterPublicKey(), block2.getMinterPublicKey());
assertArrayEquals(block1.getMinterSignature(), block2.getMinterSignature());
assertEquals(block1.getATCount(), block2.getATCount());
assertEquals(block1.getATFees(), block2.getATFees());
assertArrayEquals(block1.getEncodedOnlineAccounts(), block2.getEncodedOnlineAccounts());
assertEquals(block1.getOnlineAccountsCount(), block2.getOnlineAccountsCount());
assertEquals(block1.getOnlineAccountsTimestamp(), block2.getOnlineAccountsTimestamp());
assertArrayEquals(block1.getOnlineAccountsSignatures(), block2.getOnlineAccountsSignatures());
}
}

View File

@ -0,0 +1,682 @@
package org.qortal.test.minting;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.controller.BlockMinter;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.PaymentTransactionData;
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.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.NTP;
import java.util.*;
import static org.junit.Assert.*;
public class BatchRewardTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testBatchReward() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 20, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 20, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
Long blockReward = BlockUtils.getNextBlockReward(repository);
// Deploy an AT so we have transaction fees in each block
// This also mints block 2
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, Common.getTestAccount(repository, "bob"), AtUtils.buildSimpleAT(), 1_00000000L);
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2);
long expectedBalance = initialBalances.get("alice").get(Asset.QORT) + blockReward + deployAtTransaction.getTransactionData().getFee();
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
long aliceCurrentBalance = expectedBalance;
AccountUtils.assertBlocksMinted(repository, "alice", 1);
// Mint blocks 3-20
Block block;
for (int i=3; i<=20; i++) {
expectedBalance = aliceCurrentBalance + BlockUtils.getNextBlockReward(repository);
block = BlockUtils.mintBlockWithReorgs(repository, 10);
expectedBalance += block.getBlockData().getTotalFees();
assertFalse(block.isBatchRewardDistributionActive());
assertTrue(block.isRewardDistributionBlock());
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
aliceCurrentBalance = expectedBalance;
}
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 20);
AccountUtils.assertBlocksMinted(repository, "alice", 19);
// Mint blocks 21-29
long expectedFees = 0L;
for (int i=21; i<=29; i++) {
// Create payment transaction so that an additional fee is added to the next block
Account recipient = AccountUtils.createRandomAccount(repository);
TransactionData paymentTransactionData = new PaymentTransactionData(TestTransaction.generateBase(bob), recipient.getAddress(), 100000L);
TransactionUtils.signAndImportValid(repository, paymentTransactionData, bob);
block = BlockUtils.mintBlockWithReorgs(repository, 8);
expectedFees += block.getBlockData().getTotalFees();
// Batch distribution now active
assertTrue(block.isBatchRewardDistributionActive());
// It's not a distribution block because we haven't reached the batch size yet
assertFalse(block.isRewardDistributionBlock());
}
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 29);
AccountUtils.assertBlocksMinted(repository, "alice", 19);
// No payouts since block 20 due to batching (to be paid at block 30)
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
// Block reward to be used for next batch payout
blockReward = BlockUtils.getNextBlockReward(repository);
// Mint block 30
block = BlockUtils.mintBlockWithReorgs(repository, 9);
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 30);
expectedFees += block.getBlockData().getTotalFees();
assertTrue(expectedFees > 0);
AccountUtils.assertBlocksMinted(repository, "alice", 29);
// Batch distribution still active
assertTrue(block.isBatchRewardDistributionActive());
// It's a distribution block
assertTrue(block.isRewardDistributionBlock());
// Balance should increase by the block reward multiplied by the batch size
expectedBalance = aliceCurrentBalance + (blockReward * BlockChain.getInstance().getBlockRewardBatchSize()) + expectedFees;
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
// Mint blocks 31-39
for (int i=31; i<=39; i++) {
block = BlockUtils.mintBlockWithReorgs(repository, 13);
// Batch distribution still active
assertTrue(block.isBatchRewardDistributionActive());
// It's not a distribution block because we haven't reached the batch size yet
assertFalse(block.isRewardDistributionBlock());
}
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 39);
AccountUtils.assertBlocksMinted(repository, "alice", 29);
// No payouts since block 30 due to batching (to be paid at block 40)
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
// Batch distribution still active
assertTrue(block.isBatchRewardDistributionActive());
// It's not a distribution block
assertFalse(block.isRewardDistributionBlock());
}
}
@Test
public void testBatchRewardOnlineAccounts() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 5);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint block 7
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
// Mint block 8
onlineAccounts = Arrays.asList(aliceSelfShare, chloeSelfShare);
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(2, block8.getBlockData().getOnlineAccountsCount());
// Mint block 9
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
// Mint block 10
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 11);
// Online accounts should be included from block 8
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
// It's a distribution block
assertTrue(block10.isBatchRewardDistributionBlock());
}
}
@Test
public void testBatchReward1000Blocks() throws DataException, IllegalAccessException {
// Set reward batching to every 1000 blocks, starting at block 1000, looking back the last 25 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 1000, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-1000 - these should be regular non-batched reward distribution blocks
for (int i=2; i<=1000; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 2);
assertFalse(block.isBatchRewardDistributionActive());
assertTrue(block.isRewardDistributionBlock());
assertFalse(block.isBatchRewardDistributionBlock());
assertTrue(block.isOnlineAccountsBlock());
}
// Mint blocks 1001-1974 - these should have no online accounts or rewards
for (int i=1001; i<=1974; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 2);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
assertFalse(block.isBatchRewardDistributionBlock());
assertFalse(block.isOnlineAccountsBlock());
assertEquals(0, block.getBlockData().getOnlineAccountsCount());
}
// Mint blocks 1975-1999 - these should have online accounts but no rewards
for (int i=1975; i<=1998; i++) {
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
assertFalse(block.isBatchRewardDistributionBlock());
assertTrue(block.isOnlineAccountsBlock());
assertEquals(2, block.getBlockData().getOnlineAccountsCount());
}
// Mint block 1999 - same as above, but with more online accounts
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
assertFalse(block.isBatchRewardDistributionBlock());
assertTrue(block.isOnlineAccountsBlock());
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
// Mint block 2000
Block block2000 = BlockUtils.mintBlockWithReorgs(repository, 12);
// Online accounts should be included from block 1999
assertEquals(3, block2000.getBlockData().getOnlineAccountsCount());
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2000);
// It's a distribution block (which is technically also an online accounts block)
assertTrue(block2000.isBatchRewardDistributionBlock());
assertTrue(block2000.isRewardDistributionBlock());
assertTrue(block2000.isBatchRewardDistributionActive());
assertTrue(block2000.isOnlineAccountsBlock());
}
}
@Test
public void testBatchRewardHighestOnlineAccountsCount() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 3);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Capture initial balances now that the online accounts test is ready to begin
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
// Mint block 7
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
// Mint block 8
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block8.getBlockData().getOnlineAccountsCount());
// Mint block 9
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
// Mint block 10
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 7);
// Online accounts should be included from block 8
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
// Dilbert's balance should remain the same as he wasn't included in block 8
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT));
// Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10)
AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT));
AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT));
AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT));
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
// It's a distribution block
assertTrue(block10.isBatchRewardDistributionBlock());
}
}
@Test
public void testBatchRewardNoOnlineAccounts() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
// Mint blocks 2-6 with no online accounts
for (int i=2; i<=6; i++) {
Block block = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
assertNotNull("Minted block must not be null", block);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint block 7 with no online accounts
Block block7 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
assertNull("Minted block must be null", block7);
// Mint block 7, this time with an online account
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare);
block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertNotNull("Minted block must not be null", block7);
assertEquals(1, block7.getBlockData().getOnlineAccountsCount());
// Mint block 8 with no online accounts
Block block8 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
assertNull("Minted block must be null", block8);
// Mint block 8, this time with an online account
block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertNotNull("Minted block must not be null", block8);
assertEquals(1, block8.getBlockData().getOnlineAccountsCount());
// Mint block 9 with no online accounts
Block block9 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
assertNull("Minted block must be null", block9);
// Mint block 9, this time with an online account
block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertNotNull("Minted block must not be null", block9);
assertEquals(1, block9.getBlockData().getOnlineAccountsCount());
// Mint block 10
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 8);
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
// It's a distribution block
assertTrue(block10.isBatchRewardDistributionBlock());
}
}
@Test
public void testMissingOnlineAccountsInDistributionBlock() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 9);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint blocks 7-9
for (int i=7; i<=9; i++) {
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
}
// Mint block 10
Block block10 = Block.mint(repository, repository.getBlockRepository().getLastBlock(), aliceSelfShare);
assertNotNull(block10);
// Remove online accounts (incorrect as there should be 3)
block10.getBlockData().setEncodedOnlineAccounts(new byte[0]);
block10.sign();
block10.clearOnlineAccountsValidationCache();
// Must be invalid because online accounts don't match
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
}
}
@Test
public void testSignaturesIncludedInDistributionBlock() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 4);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint blocks 7-9
for (int i=7; i<=9; i++) {
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
}
// Mint block 10
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
assertNotNull(block10);
// Include online accounts signatures
block10.getBlockData().setOnlineAccountsSignatures(previousBlock.getOnlineAccountsSignatures());
block10.sign();
block10.clearOnlineAccountsValidationCache();
// Must be invalid because signatures aren't allowed to be included
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
}
}
@Test
public void testOnlineAccountsTimestampIncludedInDistributionBlock() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 6);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint blocks 7-9
for (int i=7; i<=9; i++) {
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
}
// Mint block 10
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
assertNotNull(block10);
// Include online accounts timestamp
block10.getBlockData().setOnlineAccountsTimestamp(previousBlock.getOnlineAccountsTimestamp());
block10.sign();
block10.clearOnlineAccountsValidationCache();
// Must be invalid because timestamp isn't allowed to be included
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
}
}
@Test
public void testIncorrectOnlineAccountsCountInDistributionBlock() throws DataException, IllegalAccessException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
for (int i=2; i<=6; i++) {
Block block = BlockUtils.mintBlockWithReorgs(repository, 5);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Mint blocks 7-9
for (int i=7; i<=9; i++) {
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
}
// Mint block 10
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
assertNotNull(block10);
// Update online accounts count so that it is incorrect
block10.getBlockData().setOnlineAccountsCount(10);
block10.sign();
block10.clearOnlineAccountsValidationCache();
// Must be invalid because online accounts count is incorrect
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
}
}
@Test
public void testBatchRewardBlockSerialization() throws DataException, IllegalAccessException, TransformationException {
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
// Create self shares for bob, chloe and dilbert
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
// Mint blocks 2-6
Block block = null;
for (int i=2; i<=6; i++) {
block = BlockUtils.mintBlockWithReorgs(repository, 7);
assertTrue(block.isBatchRewardDistributionActive());
assertFalse(block.isRewardDistributionBlock());
}
// Test serialising and deserializing a block with no online accounts
BlockData block6Data = block.getBlockData();
byte[] block6Bytes = BlockTransformer.toBytes(block);
BlockData block6DataDeserialized = BlockTransformer.fromBytes(block6Bytes).getBlockData();
BlockUtils.assertEqual(block6Data, block6DataDeserialized);
// Capture initial balances now that the online accounts test is ready to begin
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
// Mint block 7
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
// Mint block 8
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block8.getBlockData().getOnlineAccountsCount());
// Mint block 9
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
// Mint block 10
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 15);
// Online accounts should be included from block 8
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
// Dilbert's balance should remain the same as he wasn't included in block 8
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT));
// Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10)
AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT));
AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT));
AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT));
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
// It's a distribution block
assertTrue(block10.isBatchRewardDistributionBlock());
}
}
}

View File

@ -19,6 +19,9 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -23,6 +23,9 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 9999999999999,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -23,6 +23,9 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -24,6 +24,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@ -25,6 +25,9 @@
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },