From a1e83109a8f1b7cfbeaaf265ec816efed50a323b Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 3 Oct 2019 14:20:46 +0100 Subject: [PATCH] Qortal-style block rewards --- src/main/java/org/qora/account/Account.java | 11 ++ src/main/java/org/qora/block/Block.java | 195 ++++++++++++------- src/main/java/org/qora/block/BlockChain.java | 24 ++- 3 files changed, 162 insertions(+), 68 deletions(-) diff --git a/src/main/java/org/qora/account/Account.java b/src/main/java/org/qora/account/Account.java index f4775fd9..28e8b2b1 100644 --- a/src/main/java/org/qora/account/Account.java +++ b/src/main/java/org/qora/account/Account.java @@ -21,6 +21,7 @@ public class Account { private static final Logger LOGGER = LogManager.getLogger(Account.class); public static final int ADDRESS_LENGTH = 25; + public static final int FOUNDER_FLAG = 0x1; protected Repository repository; protected String address; @@ -202,8 +203,18 @@ public class Account { this.repository.getAccountRepository().setFlags(accountData); } + public boolean isFounder() throws DataException { + Integer flags = this.getFlags(); + return flags != null && (flags & FOUNDER_FLAG) != 0; + } + // Forging Enabler + public boolean canForge() throws DataException { + Integer level = this.getLevel(); + return level != null && level > 0; + } + public void setForgingEnabler(String address) throws DataException { AccountData accountData = this.buildAccountData(); accountData.setForgingEnabler(address); diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 6c42e0aa..852818b9 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -23,6 +23,7 @@ import org.qora.asset.Asset; import org.qora.at.AT; import org.qora.block.BlockChain; import org.qora.block.BlockChain.BlockTimingByHeight; +import org.qora.block.BlockChain.ShareByLevel; import org.qora.controller.Controller; import org.qora.crypto.Crypto; import org.qora.data.account.ProxyForgerData; @@ -1057,6 +1058,9 @@ public class Block { int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); this.blockData.setHeight(blockchainHeight + 1); + // Increase account levels + increaseAccountLevels(); + // Block rewards go before transactions processed processBlockRewards(); @@ -1085,6 +1089,10 @@ public class Block { linkTransactionsToBlock(); } + protected void increaseAccountLevels() throws DataException { + // TODO! + } + protected void processBlockRewards() throws DataException { BigDecimal reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight()); @@ -1092,22 +1100,7 @@ public class Block { if (reward == null) return; - // Is generator public key actually a proxy forge key? - ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); - if (proxyForgerData != null) { - // Split reward between forger and recipient - Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); - BigDecimal recipientShare = reward.multiply(proxyForgerData.getShare().movePointLeft(2)).setScale(8, RoundingMode.DOWN); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).add(recipientShare)); - - Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); - BigDecimal forgerShare = reward.subtract(recipientShare); - forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).add(forgerShare)); - return; - } - - // Give block reward to generator - this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(reward)); + distributeByAccountLevel(reward); } protected void processTransactions() throws DataException { @@ -1190,22 +1183,7 @@ public class Block { if (blockFees.compareTo(BigDecimal.ZERO) <= 0) return; - // Is generator public key actually a proxy forge key? - ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); - if (proxyForgerData != null) { - // Split fees between forger and recipient - Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); - BigDecimal recipientShare = blockFees.multiply(proxyForgerData.getShare().movePointLeft(2)).setScale(8, RoundingMode.DOWN); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).add(recipientShare)); - - Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); - BigDecimal forgerShare = blockFees.subtract(recipientShare); - forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).add(forgerShare)); - return; - } - - // Give transaction fees to generator - this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFees)); + distributeByAccountLevel(blockFees); } protected void processAtFeesAndStates() throws DataException { @@ -1254,6 +1232,9 @@ public class Block { * @throws DataException */ public void orphan() throws DataException { + // Deduct any transaction fees from generator/proxy + deductTransactionFees(); + // Orphan, and unlink, transactions from this block orphanTransactionsFromBlock(); @@ -1263,8 +1244,8 @@ public class Block { // Block rewards removed after transactions undone orphanBlockRewards(); - // Deduct any transaction fees from generator/proxy - deductTransactionFees(); + // Decrease account levels + decreaseAccountLevels(); // Return AT fees and delete AT states from repository orphanAtFeesAndStates(); @@ -1341,22 +1322,7 @@ public class Block { if (reward == null) return; - // Is generator public key actually a proxy forge key? - ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); - if (proxyForgerData != null) { - // Split reward between forger and recipient - Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); - BigDecimal recipientShare = reward.multiply(proxyForgerData.getShare().movePointLeft(2)).setScale(8, RoundingMode.DOWN); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).subtract(recipientShare)); - - Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); - BigDecimal forgerShare = reward.subtract(recipientShare); - forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).subtract(forgerShare)); - return; - } - - // Take block reward from generator - this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(reward)); + distributeByAccountLevel(reward.negate()); } protected void deductTransactionFees() throws DataException { @@ -1366,22 +1332,7 @@ public class Block { if (blockFees.compareTo(BigDecimal.ZERO) <= 0) return; - // Is generator public key actually a proxy forge key? - ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); - if (proxyForgerData != null) { - // Split fees between forger and recipient - Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); - BigDecimal recipientShare = blockFees.multiply(proxyForgerData.getShare().movePointLeft(2)).setScale(8, RoundingMode.DOWN); - recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).subtract(recipientShare)); - - Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); - BigDecimal forgerShare = blockFees.subtract(recipientShare); - forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).subtract(forgerShare)); - return; - } - - // Deduct transaction fees to generator - this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFees)); + distributeByAccountLevel(blockFees.negate()); } protected void orphanAtFeesAndStates() throws DataException { @@ -1397,4 +1348,116 @@ public class Block { atRepository.deleteATStates(this.blockData.getHeight()); } + protected void decreaseAccountLevels() throws DataException { + // TODO ! + } + + protected void distributeByAccountLevel(BigDecimal totalAmount) throws DataException { + class AccountInfo { + final ProxyForgerData proxyForgerData; + final Account forgerAccount; + final boolean isFounder; + final int level; + final int shareBin; + final Account recipientAccount; + + AccountInfo(Repository repository, int accountIndex, List sharesByLevel) throws DataException { + this.proxyForgerData = repository.getAccountRepository().getProxyAccountByIndex(accountIndex); + + this.forgerAccount = new PublicKeyAccount(repository, this.proxyForgerData.getForgerPublicKey()); + this.recipientAccount = new Account(repository, this.proxyForgerData.getRecipient()); + + if (this.forgerAccount.isFounder()) { + this.isFounder = true; + this.level = 0; + this.shareBin = -1; + return; + } + + this.isFounder = false; + this.level = this.forgerAccount.getLevel(); + + for (int s = 0; s < sharesByLevel.size(); ++s) + if (sharesByLevel.get(s).levels.contains(this.level)) { + this.shareBin = s; + return; + } + + this.shareBin = -1; + } + + void distribute(BigDecimal accountAmount) throws DataException { + final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100L); + + Account forgerAccount = this.forgerAccount; + Account recipientAccount = this.recipientAccount; + + if (forgerAccount.getAddress().equals(recipientAccount.getAddress())) { + // forger & recipient the same - simpler case + LOGGER.trace(() -> String.format("Forger/recipient account %s share: %s", forgerAccount.getAddress(), accountAmount.toPlainString())); + forgerAccount.setConfirmedBalance(Asset.QORA, forgerAccount.getConfirmedBalance(Asset.QORA).add(accountAmount)); + } else { + // forger & recipient different - extra work needed + BigDecimal recipientAmount = accountAmount.multiply(this.proxyForgerData.getShare()).divide(ONE_HUNDRED, RoundingMode.DOWN); + BigDecimal forgerAmount = accountAmount.subtract(recipientAmount); + + LOGGER.trace(() -> String.format("Forger account %s share: %s", forgerAccount.getAddress(), forgerAmount.toPlainString())); + forgerAccount.setConfirmedBalance(Asset.QORA, forgerAccount.getConfirmedBalance(Asset.QORA).add(forgerAmount)); + + LOGGER.trace(() -> String.format("Recipient account %s share: %s", recipientAccount.getAddress(), recipientAmount.toPlainString())); + recipientAccount.setConfirmedBalance(Asset.QORA, recipientAccount.getConfirmedBalance(Asset.QORA).add(recipientAmount)); + } + } + } + + List sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel(); + + ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts()); + List expandedAccounts = new ArrayList(); + + IntIterator iterator = accountIndexes.iterator(); + while (iterator.hasNext()) { + int accountIndex = iterator.next(); + AccountInfo accountInfo = new AccountInfo(repository, accountIndex, sharesByLevel); + expandedAccounts.add(accountInfo); + } + + // Distribute amount across bins + BigDecimal sharedAmount = BigDecimal.ZERO; + for (int s = 0; s < sharesByLevel.size(); ++s) { + final int binIndex = s; + + BigDecimal binAmount = sharesByLevel.get(binIndex).share.multiply(totalAmount).setScale(8, RoundingMode.DOWN); + LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, totalAmount.toPlainString(), binAmount.toPlainString())); + + // Spread across all accounts in bin + List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> !accountInfo.isFounder && accountInfo.shareBin == binIndex).collect(Collectors.toList()); + if (binnedAccounts.isEmpty()) + continue; + + BigDecimal binSize = BigDecimal.valueOf(binnedAccounts.size()); + BigDecimal accountAmount = binAmount.divide(binSize, RoundingMode.DOWN); + + for (int a = 0; a < binnedAccounts.size(); ++a) { + AccountInfo accountInfo = binnedAccounts.get(a); + accountInfo.distribute(accountAmount); + sharedAmount = sharedAmount.add(accountAmount); + } + } + + // Spread remainder across founder accounts + BigDecimal foundersAmount = totalAmount.subtract(sharedAmount); + LOGGER.debug(String.format("Shared %s of %s, remaining %s to founders", sharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString())); + + List founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isFounder).collect(Collectors.toList()); + BigDecimal foundersCount = BigDecimal.valueOf(founderAccounts.size()); + BigDecimal accountAmount = foundersAmount.divide(foundersCount, RoundingMode.DOWN); + + for (int a = 0; a < founderAccounts.size(); ++a) { + AccountInfo accountInfo = founderAccounts.get(a); + accountInfo.distribute(accountAmount); + sharedAmount = sharedAmount.add(accountAmount); + } + } + } diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index b602c0a0..1d744be2 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -99,6 +99,13 @@ public class BlockChain { } List rewardsByHeight; + /** Share of block reward/fees by account level */ + public static class ShareByLevel { + public List levels; + public BigDecimal share; + } + List sharesByLevel; + /** Block times by block height */ public static class BlockTimingByHeight { public int height; @@ -273,6 +280,10 @@ public class BlockChain { return this.rewardsByHeight; } + public List getBlockSharesByLevel() { + return this.sharesByLevel; + } + public List getForgingTiers() { return this.forgingTiers; } @@ -351,8 +362,14 @@ public class BlockChain { if (this.genesisInfo == null) Settings.throwValidationError("No \"genesisInfo\" entry found in blockchain config"); - if (this.featureTriggers == null) - Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config"); + if (this.rewardsByHeight == null) + Settings.throwValidationError("No \"rewardsByHeight\" entry found in blockchain config"); + + if (this.sharesByLevel == null) + Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config"); + + if (this.blockTimingsByHeight == null) + Settings.throwValidationError("No \"blockTimingsByHeight\" entry found in blockchain config"); if (this.blockTimestampMargin <= 0) Settings.throwValidationError("Invalid \"blockTimestampMargin\" in blockchain config"); @@ -363,6 +380,9 @@ public class BlockChain { if (this.maxBlockSize <= 0) Settings.throwValidationError("Invalid \"maxBlockSize\" in blockchain config"); + if (this.featureTriggers == null) + Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config"); + // Check all featureTriggers are present for (FeatureTrigger featureTrigger : FeatureTrigger.values()) if (!this.featureTriggers.containsKey(featureTrigger.name()))