diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 008a1737..9c70e4a8 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -1204,32 +1204,14 @@ public class Block { } protected void increaseAccountLevels() throws DataException { - List blocksNeededByLevel = BlockChain.getInstance().getBlocksNeededByLevel(); - - // Pre-calculate cumulative blocks required for each level - int cumulativeBlocks = 0; - int[] cumulativeBlocksByLevel = new int[blocksNeededByLevel.size() + 1]; - for (int level = 0; level < cumulativeBlocksByLevel.length; ++level) { - cumulativeBlocksByLevel[level] = cumulativeBlocks; - - if (level < blocksNeededByLevel.size()) - cumulativeBlocks += blocksNeededByLevel.get(level); - } - - List expandedAccounts = this.getExpandedAccounts(); - // We need to do this for both forgers and recipients - this.increaseAccountLevels(expandedAccounts, cumulativeBlocksByLevel, - expandedAccount -> expandedAccount.isForgerFounder, - expandedAccount -> expandedAccount.forgerAccountData); - - this.increaseAccountLevels(expandedAccounts, cumulativeBlocksByLevel, - expandedAccount -> expandedAccount.isRecipientFounder, - expandedAccount -> expandedAccount.recipientAccountData); + this.increaseAccountLevels(expandedAccount -> expandedAccount.isForgerFounder, expandedAccount -> expandedAccount.forgerAccountData); + this.increaseAccountLevels(expandedAccount -> expandedAccount.isRecipientFounder, expandedAccount -> expandedAccount.recipientAccountData); } - private void increaseAccountLevels(List expandedAccounts, int[] cumulativeBlocksByLevel, - Predicate isFounder, Function getAccountData) throws DataException { + private void increaseAccountLevels(Predicate isFounder, Function getAccountData) throws DataException { + final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + final List expandedAccounts = this.getExpandedAccounts(); final boolean isProcessingRecipients = getAccountData.apply(expandedAccounts.get(0)) == expandedAccounts.get(0).recipientAccountData; // Increase blocks generated count for all accounts @@ -1244,21 +1226,21 @@ public class Block { accountData.setBlocksGenerated(accountData.getBlocksGenerated() + 1); repository.getAccountRepository().setBlocksGenerated(accountData); - LOGGER.trace(() -> String.format("Block generator %s has generated %d block%s", accountData.getAddress(), accountData.getBlocksGenerated(), (accountData.getBlocksGenerated() != 1 ? "s" : ""))); + LOGGER.trace(() -> String.format("Block generator %s up to %d generated block%s", accountData.getAddress(), accountData.getBlocksGenerated(), (accountData.getBlocksGenerated() != 1 ? "s" : ""))); } // We are only interested in accounts that are NOT founders and NOT already highest level - final int maximumLevel = cumulativeBlocksByLevel.length - 1; + final int maximumLevel = cumulativeBlocksByLevel.size() - 1; List candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() < maximumLevel).collect(Collectors.toList()); for (int c = 0; c < candidateAccounts.size(); ++c) { ExpandedAccount expandedAccount = candidateAccounts.get(c); final AccountData accountData = getAccountData.apply(expandedAccount); - final int effectiveBlocksGenerated = cumulativeBlocksByLevel[accountData.getInitialLevel()] + accountData.getBlocksGenerated(); + final int effectiveBlocksGenerated = cumulativeBlocksByLevel.get(accountData.getInitialLevel()) + accountData.getBlocksGenerated(); - for (int newLevel = cumulativeBlocksByLevel.length - 1; newLevel > 0; --newLevel) - if (effectiveBlocksGenerated >= cumulativeBlocksByLevel[newLevel]) { + for (int newLevel = maximumLevel; newLevel > 0; --newLevel) + if (effectiveBlocksGenerated >= cumulativeBlocksByLevel.get(newLevel)) { if (newLevel > accountData.getLevel()) { // Account has increased in level! accountData.setLevel(newLevel); @@ -1530,7 +1512,53 @@ public class Block { } protected void decreaseAccountLevels() throws DataException { - // TODO ! + // We need to do this for both forgers and recipients + this.decreaseAccountLevels(expandedAccount -> expandedAccount.isForgerFounder, expandedAccount -> expandedAccount.forgerAccountData); + this.decreaseAccountLevels(expandedAccount -> expandedAccount.isRecipientFounder, expandedAccount -> expandedAccount.recipientAccountData); + } + + private void decreaseAccountLevels(Predicate isFounder, Function getAccountData) throws DataException { + final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + final List expandedAccounts = this.getExpandedAccounts(); + final boolean isProcessingRecipients = getAccountData.apply(expandedAccounts.get(0)) == expandedAccounts.get(0).recipientAccountData; + + // Decrease blocks generated count for all accounts + for (int a = 0; a < expandedAccounts.size(); ++a) { + ExpandedAccount expandedAccount = expandedAccounts.get(a); + + // Don't decrease twice if recipient is also forger. + if (isProcessingRecipients && expandedAccount.isRecipientAlsoForger) + continue; + + AccountData accountData = getAccountData.apply(expandedAccount); + + accountData.setBlocksGenerated(accountData.getBlocksGenerated() - 1); + repository.getAccountRepository().setBlocksGenerated(accountData); + LOGGER.trace(() -> String.format("Block generator %s down to %d generated block%s", accountData.getAddress(), accountData.getBlocksGenerated(), (accountData.getBlocksGenerated() != 1 ? "s" : ""))); + } + + // We are only interested in accounts that are NOT founders and NOT already lowest level + final int maximumLevel = cumulativeBlocksByLevel.size() - 1; + List candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() > 0).collect(Collectors.toList()); + + for (int c = 0; c < candidateAccounts.size(); ++c) { + ExpandedAccount expandedAccount = candidateAccounts.get(c); + final AccountData accountData = getAccountData.apply(expandedAccount); + + final int effectiveBlocksGenerated = cumulativeBlocksByLevel.get(accountData.getInitialLevel()) + accountData.getBlocksGenerated(); + + for (int newLevel = maximumLevel; newLevel > 0; --newLevel) + if (effectiveBlocksGenerated >= cumulativeBlocksByLevel.get(newLevel)) { + if (newLevel < accountData.getLevel()) { + // Account has decreased in level! + accountData.setLevel(newLevel); + repository.getAccountRepository().setLevel(accountData); + LOGGER.trace(() -> String.format("Block generator %s reduced to level %d", accountData.getAddress(), accountData.getLevel())); + } + + break; + } + } } protected void distributeByAccountLevel(BigDecimal totalAmount) throws DataException { diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index 45c8b692..d27a0b6e 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -6,7 +6,9 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.MathContext; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; @@ -111,13 +113,22 @@ public class BlockChain { BigDecimal qoraHoldersShare; /** - * Number of generated blocks required to reach next level. + * Number of generated blocks required to reach next level from previous. *

* Use account's current level as index.
* If account's level isn't valid as an index, then account's level is at maximum. */ List blocksNeededByLevel; + /** + * Cumulative number of generated blocks required to reach level from scratch. + *

+ * Generated just after blockchain config is parsed and validated. + *

+ * Should NOT be present in blockchain config file! + */ + List cumulativeBlocksByLevel; + /** Block times by block height */ public static class BlockTimingByHeight { public int height; @@ -221,9 +232,7 @@ public class BlockChain { blockchain.validateConfig(); // Minor fix-up - blockchain.maxBytesPerUnitFee = blockchain.maxBytesPerUnitFee.setScale(8); - blockchain.unitFee = blockchain.unitFee.setScale(8); - blockchain.minFeePerByte = blockchain.unitFee.divide(blockchain.maxBytesPerUnitFee, MathContext.DECIMAL32); + blockchain.fixUp(); // Successfully read config now in effect instance = blockchain; @@ -291,6 +300,10 @@ public class BlockChain { return this.blocksNeededByLevel; } + public List getCumulativeBlocksByLevel() { + return this.cumulativeBlocksByLevel; + } + public BigDecimal getQoraHoldersShare() { return this.qoraHoldersShare; } @@ -402,6 +415,30 @@ public class BlockChain { Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); } + /** Minor normalization, cached value generation, etc. */ + private void fixUp() { + this.maxBytesPerUnitFee = this.maxBytesPerUnitFee.setScale(8); + this.unitFee = this.unitFee.setScale(8); + this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32); + + // Pre-calculate cumulative blocks required for each level + int cumulativeBlocks = 0; + this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1); + for (int level = 0; level <= this.blocksNeededByLevel.size(); ++level) { + this.cumulativeBlocksByLevel.add(cumulativeBlocks); + + if (level < this.blocksNeededByLevel.size()) + cumulativeBlocks += this.blocksNeededByLevel.get(level); + } + + // Convert collections to unmodifiable form + this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight); + this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel); + this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel); + this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel); + this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight); + } + /** * Some sort start-up/initialization/checking method. * @@ -412,7 +449,6 @@ public class BlockChain { if (!isGenesisBlockValid()) rebuildBlockchain(); - // TODO: walk through blocks try (final Repository repository = RepositoryManager.getRepository()) { Block parentBlock = GenesisBlock.getInstance(repository); BlockData parentBlockData = parentBlock.getBlockData();