3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-14 11:15:49 +00:00

Initial implementation of batch block reward distributions.

There are 3 values that need to be specified for this feature trigger:

- blockRewardBatchSize: the number of blocks per batch (1000 recommended).

- blockRewardBatchStartHeight: the block height at which the system switches to batch reward distributions. Must be a multiple of blockRewardBatchSize. Ideally this would be set to a block height that will occur at least 1-2 weeks after the release / auto update goes live.

- blockRewardBatchAccountsBlockCount: the number of blocks to include online accounts in the lead up to the batch reward distribution block (25 recommended).

Once active, rewards are no longer distributed every block and instead are distributed every 1000th block. This includes all fees from the distribution block and the previous 999 blocks.

The online accounts used for the distribution are taken from one of the previous 25 blocks (e.g. blocks xxxx975-xxxx999). The validation ensures that it is always the block with the highest number of online accounts in this range. If this number of online accounts is shared by multiple blocks, it will pick the one with the lowest height.

The idea behind 25 blocks is that it's low enough to reduce load in the other 975 blocks, but high enough for it to be extremely difficult for block signers to influence reward payouts.

Batch distribution blocks contain a copy of the online accounts from one of these 25 preceding blocks. However, online account signatures and online accounts timestamp are excluded to save space. The core will validate that the copy of online accounts is exactly matching those of the earlier validated block, and are therefore valid.

Fairly comprehensive unit tests have been written but this needs a lot more testing on testnets before it can be considered stable.

Future note: Once released, the online accounts mempow can optionally be scaled back so that it is no longer running 24/7, and instead is only running for 2-3 hours each day in the lead up to the batch distribution block. In each 1000 block cycle, the mempow would ideally start at least an hour before block xxxx975 is due to be minted, and continue for an hour after block xxxx000 (the reward distribution block). None of the ideas mentioned in this last paragraph are coded yet.
This commit is contained in:
CalDescent 2023-09-17 15:04:37 +01:00
parent d453e80c6b
commit 8b69b65712
26 changed files with 1209 additions and 85 deletions

View File

@ -376,83 +376,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,
@ -1064,6 +1088,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;
@ -1483,11 +1541,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
@ -1547,9 +1609,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<>();
@ -1599,8 +1665,34 @@ 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 startTime = NTP.getTime();
Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock);
LOGGER.info("[process] Fetching total fees took {} ms", (NTP.getTime()-startTime));
if (totalFees == null) {
throw new DataException("Unable to calculate total fees for block range");
}
reward += totalFees;
LOGGER.info("[process] Total fees for range {} - {}: {}", 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.info("[process] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
LOGGER.info("[process] Block reward for height {}: {}", this.blockData.getHeight(), reward);
// Nothing to reward?
if (reward <= 0)
@ -1758,11 +1850,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
@ -1838,8 +1933,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.info("[orphan] Total fees for range {} - {}: {}", 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.info("[orphan] Total fees for block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
LOGGER.info("[orphan] Block reward for height {}: {}", this.blockData.getHeight(), reward);
// Nothing to reward?
if (reward <= 0)
@ -1880,9 +1999,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)
@ -1905,6 +2028,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

@ -212,6 +212,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;
@ -368,6 +381,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;
@ -653,6 +681,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

@ -23,6 +23,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 org.qortal.utils.Base58;
import org.qortal.utils.NTP;
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

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

View File

@ -191,10 +191,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

@ -357,6 +357,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

@ -941,7 +941,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

@ -460,6 +460,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

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

View File

@ -1,13 +1,16 @@
package org.qortal.test.common;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.qortal.crypto.Qortal25519Extras.signForAggregation;
import java.security.SecureRandom;
import java.util.*;
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;
@ -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 },