forked from Qortal/qortal
Merge pull request #143 from AlphaX-Projects/merge-batch-payout
Merge batch payout
This commit is contained in:
commit
dbbe78263d
@ -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;
|
||||
|
@ -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. */
|
||||
|
@ -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();
|
||||
|
||||
|
@ -780,6 +780,13 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
|
||||
|
||||
// Utils
|
||||
|
||||
public void removeAllOnlineAccounts() {
|
||||
this.currentOnlineAccounts.clear();
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 "
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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 },
|
||||
|
@ -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<>();
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
682
src/test/java/org/qortal/test/minting/BatchRewardTests.java
Normal file
682
src/test/java/org/qortal/test/minting/BatchRewardTests.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
Loading…
x
Reference in New Issue
Block a user