forked from Qortal/qortal
Rework block rewards to be faster and only reward *online* founders.
Now we sum generic block reward + transaction fees before performing distribution only once. Added Map to collate account-balance changes during block reward distribution so the final changes can be applied in one batch, reducing DB load. Some other optimizations like a faster ExpandedAccount.getShareBin(). Passes test EXCEPT RewardTests.testLegacyQoraReward(), pending decision on how to reallocate 'unspent' block reward.
This commit is contained in:
parent
2493d5f7a8
commit
b5512dfa91
@ -24,7 +24,7 @@ import org.qortal.account.PublicKeyAccount;
|
|||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.at.AT;
|
import org.qortal.at.AT;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
import org.qortal.block.BlockChain.ShareByLevel;
|
import org.qortal.block.BlockChain.AccountLevelShareBin;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
@ -126,8 +126,13 @@ public class Block {
|
|||||||
/** Locally-generated AT fees */
|
/** Locally-generated AT fees */
|
||||||
protected long ourAtFees; // Generated locally
|
protected long ourAtFees; // Generated locally
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface BlockRewardDistributor {
|
||||||
|
long distribute(long amount, Map<Account, Long> balanceChanges) throws DataException;
|
||||||
|
}
|
||||||
|
|
||||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||||
static class ExpandedAccount {
|
private static class ExpandedAccount {
|
||||||
private final RewardShareData rewardShareData;
|
private final RewardShareData rewardShareData;
|
||||||
private final int sharePercent;
|
private final int sharePercent;
|
||||||
private final boolean isRecipientAlsoMinter;
|
private final boolean isRecipientAlsoMinter;
|
||||||
@ -166,28 +171,29 @@ public class Block {
|
|||||||
* This is a method, not a final variable, because account's level can change between construction and call,
|
* This is a method, not a final variable, because account's level can change between construction and call,
|
||||||
* e.g. during Block.process() where account levels are bumped right before Block.distributeBlockReward().
|
* e.g. during Block.process() where account levels are bumped right before Block.distributeBlockReward().
|
||||||
*
|
*
|
||||||
* @return share "bin" (index into BlockShareByLevel blockchain config, so 0+), or -1 if no bin found
|
* @return account-level share "bin" from blockchain config, or null if founder / none found
|
||||||
*/
|
*/
|
||||||
int getShareBin() {
|
public AccountLevelShareBin getShareBin() {
|
||||||
if (this.isMinterFounder)
|
if (this.isMinterFounder)
|
||||||
return -1;
|
return null;
|
||||||
|
|
||||||
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
|
||||||
final int accountLevel = this.mintingAccountData.getLevel();
|
final int accountLevel = this.mintingAccountData.getLevel();
|
||||||
|
if (accountLevel <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
for (int s = 0; s < sharesByLevel.size(); ++s)
|
final AccountLevelShareBin[] shareBinsByLevel = BlockChain.getInstance().getShareBinsByAccountLevel();
|
||||||
if (sharesByLevel.get(s).levels.contains(accountLevel))
|
if (accountLevel > shareBinsByLevel.length)
|
||||||
return s;
|
return null;
|
||||||
|
|
||||||
return -1;
|
return shareBinsByLevel[accountLevel];
|
||||||
}
|
}
|
||||||
|
|
||||||
void distribute(long accountAmount) throws DataException {
|
public long distribute(long accountAmount, Map<Account, Long> balanceChanges) {
|
||||||
if (this.isRecipientAlsoMinter) {
|
if (this.isRecipientAlsoMinter) {
|
||||||
// minter & recipient the same - simpler case
|
// minter & recipient the same - simpler case
|
||||||
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(accountAmount)));
|
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(accountAmount)));
|
||||||
if (accountAmount != 0)
|
if (accountAmount != 0)
|
||||||
this.mintingAccount.modifyAssetBalance(Asset.QORT, accountAmount);
|
balanceChanges.merge(this.mintingAccount, accountAmount, Long::sum);
|
||||||
} else {
|
} else {
|
||||||
// minter & recipient different - extra work needed
|
// minter & recipient different - extra work needed
|
||||||
long recipientAmount = (accountAmount * this.sharePercent) / 100L / 100L; // because scaled by 2dp and 'percent' means "per 100"
|
long recipientAmount = (accountAmount * this.sharePercent) / 100L / 100L; // because scaled by 2dp and 'percent' means "per 100"
|
||||||
@ -195,12 +201,15 @@ public class Block {
|
|||||||
|
|
||||||
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(minterAmount)));
|
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(minterAmount)));
|
||||||
if (minterAmount != 0)
|
if (minterAmount != 0)
|
||||||
this.mintingAccount.modifyAssetBalance(Asset.QORT, minterAmount);
|
balanceChanges.merge(this.mintingAccount, minterAmount, Long::sum);
|
||||||
|
|
||||||
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), Amounts.prettyAmount(recipientAmount)));
|
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), Amounts.prettyAmount(recipientAmount)));
|
||||||
if (recipientAmount != 0)
|
if (recipientAmount != 0)
|
||||||
this.recipientAccount.modifyAssetBalance(Asset.QORT, recipientAmount);
|
balanceChanges.merge(this.recipientAccount, recipientAmount, Long::sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We always distribute all of the amount
|
||||||
|
return accountAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
||||||
@ -1229,11 +1238,8 @@ public class Block {
|
|||||||
// Increase account levels
|
// Increase account levels
|
||||||
increaseAccountLevels();
|
increaseAccountLevels();
|
||||||
|
|
||||||
// Block rewards go before transactions processed
|
// Distribute block rewards, including transaction fees, before transactions processed
|
||||||
processBlockRewards();
|
processBlockRewards();
|
||||||
|
|
||||||
// Give transaction fees to minter/reward-share account(s)
|
|
||||||
rewardTransactionFees();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're about to (test-)process a batch of transactions,
|
// We're about to (test-)process a batch of transactions,
|
||||||
@ -1307,10 +1313,13 @@ public class Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void processBlockRewards() throws DataException {
|
protected void processBlockRewards() throws DataException {
|
||||||
|
// General block reward
|
||||||
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
||||||
|
// Add transaction fees
|
||||||
|
reward += this.blockData.getTotalFees();
|
||||||
|
|
||||||
// No reward for our height?
|
// Nothing to reward?
|
||||||
if (reward == 0)
|
if (reward <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
distributeBlockReward(reward);
|
distributeBlockReward(reward);
|
||||||
@ -1389,16 +1398,6 @@ public class Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void rewardTransactionFees() throws DataException {
|
|
||||||
long blockFees = this.blockData.getTotalFees();
|
|
||||||
|
|
||||||
// No transaction fees?
|
|
||||||
if (blockFees <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
distributeBlockReward(blockFees);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void processAtFeesAndStates() throws DataException {
|
protected void processAtFeesAndStates() throws DataException {
|
||||||
ATRepository atRepository = this.repository.getATRepository();
|
ATRepository atRepository = this.repository.getATRepository();
|
||||||
|
|
||||||
@ -1463,10 +1462,7 @@ public class Block {
|
|||||||
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
||||||
this.cachedExpandedAccounts = null;
|
this.cachedExpandedAccounts = null;
|
||||||
|
|
||||||
// Deduct any transaction fees from minter/reward-share account(s)
|
// Block rewards, including transaction fees, removed after transactions undone
|
||||||
deductTransactionFees();
|
|
||||||
|
|
||||||
// Block rewards removed after transactions undone
|
|
||||||
orphanBlockRewards();
|
orphanBlockRewards();
|
||||||
|
|
||||||
// Decrease account levels
|
// Decrease account levels
|
||||||
@ -1541,25 +1537,18 @@ public class Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void orphanBlockRewards() throws DataException {
|
protected void orphanBlockRewards() throws DataException {
|
||||||
|
// General block reward
|
||||||
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
||||||
|
// Add transaction fees
|
||||||
|
reward += this.blockData.getTotalFees();
|
||||||
|
|
||||||
// No reward for our height?
|
// Nothing to reward?
|
||||||
if (reward == 0)
|
if (reward <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
distributeBlockReward(- reward);
|
distributeBlockReward(- reward);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void deductTransactionFees() throws DataException {
|
|
||||||
long blockFees = this.blockData.getTotalFees();
|
|
||||||
|
|
||||||
// No transaction fees?
|
|
||||||
if (blockFees <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
distributeBlockReward(- blockFees);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void orphanAtFeesAndStates() throws DataException {
|
protected void orphanAtFeesAndStates() throws DataException {
|
||||||
ATRepository atRepository = this.repository.getATRepository();
|
ATRepository atRepository = this.repository.getATRepository();
|
||||||
for (ATStateData atStateData : this.getATStates()) {
|
for (ATStateData atStateData : this.getATStates()) {
|
||||||
@ -1615,62 +1604,163 @@ public class Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void distributeBlockReward(long totalAmount) throws DataException {
|
private static class BlockRewardCandidate {
|
||||||
LOGGER.trace(() -> String.format("Distributing: %s", Amounts.prettyAmount(totalAmount)));
|
public final String description;
|
||||||
|
public long share;
|
||||||
|
public final BlockRewardDistributor distributionMethod;
|
||||||
|
|
||||||
// Distribute according to account level
|
public BlockRewardCandidate(String description, long share, BlockRewardDistributor distributionMethod) {
|
||||||
long sharedByLevelAmount = distributeBlockRewardByLevel(totalAmount);
|
this.description = description;
|
||||||
LOGGER.trace(() -> String.format("Shared %s of %s based on account levels", Amounts.prettyAmount(sharedByLevelAmount), Amounts.prettyAmount(totalAmount)));
|
this.share = share;
|
||||||
|
this.distributionMethod = distributionMethod;
|
||||||
|
}
|
||||||
|
|
||||||
// Distribute amongst legacy QORA holders
|
public long distribute(long distibutionAmount, Map<Account, Long> balanceChanges) throws DataException {
|
||||||
long sharedByQoraHoldersAmount = distributeBlockRewardToQoraHolders(totalAmount);
|
return this.distributionMethod.distribute(distibutionAmount, balanceChanges);
|
||||||
LOGGER.trace(() -> String.format("Shared %s of %s to legacy QORA holders", Amounts.prettyAmount(sharedByQoraHoldersAmount), Amounts.prettyAmount(totalAmount)));
|
}
|
||||||
|
|
||||||
// Spread remainder across founder accounts
|
|
||||||
long foundersAmount = totalAmount - sharedByLevelAmount - sharedByQoraHoldersAmount;
|
|
||||||
distributeBlockRewardToFounders(foundersAmount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long distributeBlockRewardByLevel(long totalAmount) throws DataException {
|
protected void distributeBlockReward(final long totalAmount) throws DataException {
|
||||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
LOGGER.trace(() -> String.format("Distributing: %s", Amounts.prettyAmount(totalAmount)));
|
||||||
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
|
||||||
|
|
||||||
// Distribute amount across bins
|
// All online accounts
|
||||||
long sharedAmount = 0;
|
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||||
for (int s = 0; s < sharesByLevel.size(); ++s) {
|
|
||||||
final int binIndex = s;
|
|
||||||
|
|
||||||
long binAmount = Amounts.roundDownScaledMultiply(totalAmount, sharesByLevel.get(binIndex).share);
|
// How to distribute reward among groups, with ratio, IN ORDER
|
||||||
LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, Amounts.prettyAmount(totalAmount), Amounts.prettyAmount(binAmount)));
|
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
|
||||||
|
|
||||||
// Spread across all accounts in bin. getShareBin() returns -1 for minter accounts that are also founders, so they are effectively filtered out.
|
// Determine reward candidates based on account level
|
||||||
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
|
List<AccountLevelShareBin> accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins();
|
||||||
|
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
|
||||||
|
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
|
||||||
|
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||||
|
// Object reference compare is OK as all references are read-only from blockchain config.
|
||||||
|
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == accountLevelShareBin).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// No online accounts in this bin? Skip to next one
|
||||||
if (binnedAccounts.isEmpty())
|
if (binnedAccounts.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
long perAccountAmount = binAmount / binnedAccounts.size();
|
String description = String.format("Bin %d", binIndex);
|
||||||
|
BlockRewardDistributor accountLevelBinDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, binnedAccounts, balanceChanges);
|
||||||
|
|
||||||
for (int a = 0; a < binnedAccounts.size(); ++a) {
|
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate(description, accountLevelShareBins.get(binIndex).share, accountLevelBinDistributor);
|
||||||
ExpandedAccount expandedAccount = binnedAccounts.get(a);
|
rewardCandidates.add(rewardCandidate);
|
||||||
expandedAccount.distribute(perAccountAmount);
|
}
|
||||||
sharedAmount += perAccountAmount;
|
|
||||||
}
|
// Determine reward candidates based on legacy QORA held
|
||||||
|
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
|
||||||
|
final boolean isProcessingNotOrphaning = totalAmount >= 0;
|
||||||
|
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||||
|
|
||||||
|
// Any eligible legacy QORA holders?
|
||||||
|
if (!qoraHolders.isEmpty()) {
|
||||||
|
// Yes: add to reward candidates list
|
||||||
|
BlockRewardDistributor legacyQoraHoldersDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardToQoraHolders(distributionAmount, qoraHolders, balanceChanges, this);
|
||||||
|
|
||||||
|
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Legacy QORA holders", BlockChain.getInstance().getQoraHoldersShare(), legacyQoraHoldersDistributor);
|
||||||
|
rewardCandidates.add(rewardCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether we reward founders
|
||||||
|
final List<ExpandedAccount> onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList());
|
||||||
|
if (!onlineFounderAccounts.isEmpty()) {
|
||||||
|
// Yes: add to reward candidates list
|
||||||
|
BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges);
|
||||||
|
|
||||||
|
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", BlockChain.getInstance().getFoundersShare(), founderDistributor);
|
||||||
|
rewardCandidates.add(rewardCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate distribution ratios based on candidates
|
||||||
|
long totalShared = 0;
|
||||||
|
for (BlockRewardCandidate rewardCandidate : rewardCandidates)
|
||||||
|
totalShared += rewardCandidate.share;
|
||||||
|
|
||||||
|
// No eligible candidates?
|
||||||
|
// Example scenario: the only online accounts are legacy QORA holders and they've already reached their max capped QORT-from-QORA.
|
||||||
|
if (totalShared == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Re-scale individual reward candidate's share as if total shared was 100%
|
||||||
|
for (BlockRewardCandidate rewardCandidate : rewardCandidates)
|
||||||
|
rewardCandidate.share = Amounts.scaledDivide(rewardCandidate.share, totalShared);
|
||||||
|
|
||||||
|
// Now distribute to candidates
|
||||||
|
|
||||||
|
// Collate all balance changes and then apply in one final step
|
||||||
|
Map<Account, Long> balanceChanges = new HashMap<>();
|
||||||
|
|
||||||
|
long remainingAmount = totalAmount;
|
||||||
|
for (int r = 0; r < rewardCandidates.size(); ++r) {
|
||||||
|
BlockRewardCandidate rewardCandidate = rewardCandidates.get(r);
|
||||||
|
|
||||||
|
long distributionAmount;
|
||||||
|
if (r < rewardCandidates.size() - 1)
|
||||||
|
// Distribute according to sharePercent
|
||||||
|
distributionAmount = Amounts.roundDownScaledMultiply(totalAmount, rewardCandidate.share);
|
||||||
|
else
|
||||||
|
/*
|
||||||
|
* Last reward candidate gets full remaining amount. Typically this will be online founders.
|
||||||
|
*
|
||||||
|
* The only time the amount would differ from above sharePercent calculation is
|
||||||
|
* if the *previous* rewardCandidate was "legacy QORA holders" and not all of their
|
||||||
|
* allotment was distributed.
|
||||||
|
*/
|
||||||
|
distributionAmount = remainingAmount;
|
||||||
|
|
||||||
|
// Distribute to these reward candidate accounts, reducing remainingAmount by how much was actually distributed
|
||||||
|
long sharedAmount = rewardCandidate.distribute(distributionAmount, balanceChanges);
|
||||||
|
remainingAmount -= sharedAmount;
|
||||||
|
|
||||||
|
final long remainingAmountForLogging = remainingAmount;
|
||||||
|
LOGGER.trace(() -> String.format("%s share: %s. Actually shared: %s. Remaining: %s",
|
||||||
|
rewardCandidate.description,
|
||||||
|
Amounts.prettyAmount(distributionAmount),
|
||||||
|
Amounts.prettyAmount(sharedAmount),
|
||||||
|
Amounts.prettyAmount(remainingAmountForLogging)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply balance changes
|
||||||
|
for (Map.Entry<Account, Long> balanceChange : balanceChanges.entrySet())
|
||||||
|
balanceChange.getKey().modifyAssetBalance(Asset.QORT, balanceChange.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<Account, Long> balanceChanges) {
|
||||||
|
// Collate all expanded accounts by minting account
|
||||||
|
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();
|
||||||
|
|
||||||
|
for (ExpandedAccount expandedAccount : accounts)
|
||||||
|
accountsByMinter.compute(expandedAccount.mintingAccount.getAddress(), (minterAddress, otherAccounts) -> {
|
||||||
|
if (otherAccounts == null) {
|
||||||
|
return new ArrayList<>(Arrays.asList(expandedAccount));
|
||||||
|
} else {
|
||||||
|
otherAccounts.add(expandedAccount);
|
||||||
|
return otherAccounts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Divide distribution amount by number of *minting* accounts
|
||||||
|
long perMintingAccountAmount = distributionAmount / accountsByMinter.keySet().size();
|
||||||
|
|
||||||
|
// Distribute, reducing totalAmount by how much was actually distributed
|
||||||
|
long sharedAmount = 0;
|
||||||
|
for (List<ExpandedAccount> recipientAccounts : accountsByMinter.values()) {
|
||||||
|
long perRecipientAccountAmount = perMintingAccountAmount / recipientAccounts.size();
|
||||||
|
|
||||||
|
for (ExpandedAccount expandedAccount : recipientAccounts)
|
||||||
|
sharedAmount += expandedAccount.distribute(perRecipientAccountAmount, balanceChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sharedAmount;
|
return sharedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long distributeBlockRewardToQoraHolders(long totalAmount) throws DataException {
|
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<AccountBalanceData> qoraHolders, Map<Account, Long> balanceChanges, Block block) throws DataException {
|
||||||
long qoraHoldersAmount = Amounts.roundDownScaledMultiply(totalAmount, BlockChain.getInstance().getQoraHoldersShare());
|
final boolean isProcessingNotOrphaning = qoraHoldersAmount >= 0;
|
||||||
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", Amounts.prettyAmount(totalAmount), Amounts.prettyAmount(qoraHoldersAmount)));
|
|
||||||
|
|
||||||
final boolean isProcessingNotOrphaning = totalAmount >= 0;
|
|
||||||
|
|
||||||
long qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
long qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
||||||
BigInteger qoraPerQortRewardBI = BigInteger.valueOf(qoraPerQortReward);
|
BigInteger qoraPerQortRewardBI = BigInteger.valueOf(qoraPerQortReward);
|
||||||
|
|
||||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
|
||||||
|
|
||||||
long totalQoraHeld = 0;
|
long totalQoraHeld = 0;
|
||||||
for (int i = 0; i < qoraHolders.size(); ++i)
|
for (int i = 0; i < qoraHolders.size(); ++i)
|
||||||
totalQoraHeld += qoraHolders.get(i).getBalance();
|
totalQoraHeld += qoraHolders.get(i).getBalance();
|
||||||
@ -1694,15 +1784,15 @@ public class Block {
|
|||||||
// long holderReward = (qoraHoldersAmount * qoraHolder.getBalance()) / totalQoraHeld;
|
// long holderReward = (qoraHoldersAmount * qoraHolder.getBalance()) / totalQoraHeld;
|
||||||
long holderReward = qoraHoldersAmountBI.multiply(qoraHolderBalanceBI).divide(totalQoraHeldBI).longValue();
|
long holderReward = qoraHoldersAmountBI.multiply(qoraHolderBalanceBI).divide(totalQoraHeldBI).longValue();
|
||||||
|
|
||||||
long finalHolderReward = holderReward;
|
final long holderRewardForLogging = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
||||||
qoraHolder.getAddress(), Amounts.prettyAmount(qoraHolder.getBalance()), finalTotalQoraHeld, Amounts.prettyAmount(finalHolderReward)));
|
qoraHolder.getAddress(), Amounts.prettyAmount(qoraHolder.getBalance()), finalTotalQoraHeld, Amounts.prettyAmount(holderRewardForLogging)));
|
||||||
|
|
||||||
// Too small to register this time?
|
// Too small to register this time?
|
||||||
if (holderReward == 0)
|
if (holderReward == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
Account qoraHolderAccount = new Account(block.repository, qoraHolder.getAddress());
|
||||||
|
|
||||||
long newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA) + holderReward;
|
long newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA) + holderReward;
|
||||||
|
|
||||||
@ -1718,16 +1808,16 @@ public class Block {
|
|||||||
newQortFromQoraBalance -= adjustment;
|
newQortFromQoraBalance -= adjustment;
|
||||||
|
|
||||||
// This is also the QORA holder's final QORT-from-QORA block
|
// This is also the QORA holder's final QORT-from-QORA block
|
||||||
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight());
|
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, block.blockData.getHeight());
|
||||||
this.repository.getAccountRepository().save(qortFromQoraData);
|
block.repository.getAccountRepository().save(qortFromQoraData);
|
||||||
|
|
||||||
long finalAdjustedHolderReward = holderReward;
|
long finalAdjustedHolderReward = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
||||||
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), this.blockData.getHeight()));
|
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Orphaning
|
// Orphaning
|
||||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
QortFromQoraData qortFromQoraData = block.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||||
if (qortFromQoraData != null) {
|
if (qortFromQoraData != null) {
|
||||||
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
|
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
|
||||||
// So we use + here as qortFromQora is negative during orphaning.
|
// So we use + here as qortFromQora is negative during orphaning.
|
||||||
@ -1737,15 +1827,15 @@ public class Block {
|
|||||||
holderReward -= adjustment;
|
holderReward -= adjustment;
|
||||||
newQortFromQoraBalance -= adjustment;
|
newQortFromQoraBalance -= adjustment;
|
||||||
|
|
||||||
this.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
|
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
|
||||||
|
|
||||||
long finalAdjustedHolderReward = holderReward;
|
long finalAdjustedHolderReward = holderReward;
|
||||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
||||||
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), this.blockData.getHeight()));
|
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qoraHolderAccount.modifyAssetBalance(Asset.QORT, holderReward);
|
balanceChanges.merge(qoraHolderAccount, holderReward, Long::sum);
|
||||||
|
|
||||||
if (newQortFromQoraBalance > 0)
|
if (newQortFromQoraBalance > 0)
|
||||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||||
@ -1759,41 +1849,6 @@ public class Block {
|
|||||||
return sharedAmount;
|
return sharedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void distributeBlockRewardToFounders(long foundersAmount) throws DataException {
|
|
||||||
// Remaining reward portion is spread across all founders, online or not
|
|
||||||
List<AccountData> founderAccounts = this.repository.getAccountRepository().getFlaggedAccounts(Account.FOUNDER_FLAG);
|
|
||||||
|
|
||||||
long foundersCount = founderAccounts.size();
|
|
||||||
long perFounderAmount = foundersAmount / foundersCount;
|
|
||||||
|
|
||||||
LOGGER.trace(() -> String.format("Sharing remaining %s to %d founder%s, %s each",
|
|
||||||
Amounts.prettyAmount(foundersAmount),
|
|
||||||
founderAccounts.size(), (founderAccounts.size() != 1 ? "s" : ""),
|
|
||||||
Amounts.prettyAmount(perFounderAmount)));
|
|
||||||
|
|
||||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
|
||||||
for (int a = 0; a < founderAccounts.size(); ++a) {
|
|
||||||
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
|
|
||||||
|
|
||||||
// If founder is minter in any online reward-shares then founder's amount is spread across these, otherwise founder gets whole amount.
|
|
||||||
List<ExpandedAccount> founderExpandedAccounts = expandedAccounts.stream().filter(
|
|
||||||
accountInfo -> accountInfo.isMinterFounder &&
|
|
||||||
accountInfo.mintingAccountData.getAddress().equals(founderAccount.getAddress())
|
|
||||||
).collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (founderExpandedAccounts.isEmpty()) {
|
|
||||||
// Simple case: no founder-as-minter reward-shares online so founder gets whole amount.
|
|
||||||
founderAccount.modifyAssetBalance(Asset.QORT, perFounderAmount);
|
|
||||||
} else {
|
|
||||||
// Distribute over reward-shares
|
|
||||||
long perFounderRewardShareAmount = perFounderAmount / founderExpandedAccounts.size();
|
|
||||||
|
|
||||||
for (int fea = 0; fea < founderExpandedAccounts.size(); ++fea)
|
|
||||||
founderExpandedAccounts.get(fea).distribute(perFounderRewardShareAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Opportunity to tidy repository, etc. after block process/orphan. */
|
/** Opportunity to tidy repository, etc. after block process/orphan. */
|
||||||
private void postBlockTidy() throws DataException {
|
private void postBlockTidy() throws DataException {
|
||||||
this.repository.getAccountRepository().tidy();
|
this.repository.getAccountRepository().tidy();
|
||||||
|
@ -89,15 +89,17 @@ public class BlockChain {
|
|||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long reward;
|
public long reward;
|
||||||
}
|
}
|
||||||
List<RewardByHeight> rewardsByHeight;
|
private List<RewardByHeight> rewardsByHeight;
|
||||||
|
|
||||||
/** Share of block reward/fees by account level */
|
/** Share of block reward/fees by account level */
|
||||||
public static class ShareByLevel {
|
public static class AccountLevelShareBin {
|
||||||
public List<Integer> levels;
|
public List<Integer> levels;
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long share;
|
public long share;
|
||||||
}
|
}
|
||||||
List<ShareByLevel> sharesByLevel;
|
private List<AccountLevelShareBin> sharesByLevel;
|
||||||
|
/** Generated lookup of share-bin by account level */
|
||||||
|
private AccountLevelShareBin[] shareBinsByLevel;
|
||||||
|
|
||||||
/** Share of block reward/fees to legacy QORA coin holders */
|
/** Share of block reward/fees to legacy QORA coin holders */
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
@ -107,6 +109,9 @@ public class BlockChain {
|
|||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
private Long qoraPerQortReward;
|
private Long qoraPerQortReward;
|
||||||
|
|
||||||
|
/** Share of block reward/fees to founders. CALCULATED */
|
||||||
|
private long foundersShare;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of minted blocks required to reach next level from previous.
|
* Number of minted blocks required to reach next level from previous.
|
||||||
* <p>
|
* <p>
|
||||||
@ -116,7 +121,7 @@ public class BlockChain {
|
|||||||
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
|
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
|
||||||
* then level 3 accounts need to mint 200 blocks to reach level 4.
|
* then level 3 accounts need to mint 200 blocks to reach level 4.
|
||||||
*/
|
*/
|
||||||
List<Integer> blocksNeededByLevel;
|
private List<Integer> blocksNeededByLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cumulative number of minted blocks required to reach next level from scratch.
|
* Cumulative number of minted blocks required to reach next level from scratch.
|
||||||
@ -130,7 +135,7 @@ public class BlockChain {
|
|||||||
* <p>
|
* <p>
|
||||||
* Should NOT be present in blockchain config file!
|
* Should NOT be present in blockchain config file!
|
||||||
*/
|
*/
|
||||||
List<Integer> cumulativeBlocksByLevel;
|
private List<Integer> cumulativeBlocksByLevel;
|
||||||
|
|
||||||
/** Block times by block height */
|
/** Block times by block height */
|
||||||
public static class BlockTimingByHeight {
|
public static class BlockTimingByHeight {
|
||||||
@ -139,7 +144,7 @@ public class BlockChain {
|
|||||||
public long deviation; // ms
|
public long deviation; // ms
|
||||||
public double power;
|
public double power;
|
||||||
}
|
}
|
||||||
List<BlockTimingByHeight> blockTimingsByHeight;
|
private List<BlockTimingByHeight> blockTimingsByHeight;
|
||||||
|
|
||||||
private int minAccountLevelToMint = 1;
|
private int minAccountLevelToMint = 1;
|
||||||
private int minAccountLevelToRewardShare;
|
private int minAccountLevelToRewardShare;
|
||||||
@ -316,10 +321,14 @@ public class BlockChain {
|
|||||||
return this.rewardsByHeight;
|
return this.rewardsByHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ShareByLevel> getBlockSharesByLevel() {
|
public List<AccountLevelShareBin> getAccountLevelShareBins() {
|
||||||
return this.sharesByLevel;
|
return this.sharesByLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
|
||||||
|
return this.shareBinsByLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Integer> getBlocksNeededByLevel() {
|
public List<Integer> getBlocksNeededByLevel() {
|
||||||
return this.blocksNeededByLevel;
|
return this.blocksNeededByLevel;
|
||||||
}
|
}
|
||||||
@ -336,6 +345,10 @@ public class BlockChain {
|
|||||||
return this.qoraPerQortReward;
|
return this.qoraPerQortReward;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getFoundersShare() {
|
||||||
|
return this.foundersShare;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMinAccountLevelToMint() {
|
public int getMinAccountLevelToMint() {
|
||||||
return this.minAccountLevelToMint;
|
return this.minAccountLevelToMint;
|
||||||
}
|
}
|
||||||
@ -447,6 +460,28 @@ public class BlockChain {
|
|||||||
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate founders' share
|
||||||
|
long totalShare = this.qoraHoldersShare;
|
||||||
|
// Add share percents for account-level-based rewards
|
||||||
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
||||||
|
totalShare += accountLevelShareBin.share;
|
||||||
|
|
||||||
|
if (totalShare < 0 || totalShare > 1_00000000L)
|
||||||
|
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
||||||
|
|
||||||
|
this.foundersShare = 1_00000000L - totalShare;
|
||||||
|
|
||||||
|
// Generate lookup-array for account-level share bins
|
||||||
|
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
|
||||||
|
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
|
||||||
|
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
|
||||||
|
|
||||||
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
||||||
|
for (int level : accountLevelShareBin.levels)
|
||||||
|
// level 1 stored at index 0, level 2 stored at index 1, etc.
|
||||||
|
// level 0 not allowed
|
||||||
|
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
|
||||||
|
|
||||||
// Convert collections to unmodifiable form
|
// Convert collections to unmodifiable form
|
||||||
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
|
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
|
||||||
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
|
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
|
||||||
|
@ -241,15 +241,15 @@ public class RewardTests extends Common {
|
|||||||
|
|
||||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||||
|
|
||||||
// 3 founders (online or not) so blockReward divided by 3
|
// 2 founders online so blockReward divided by 2
|
||||||
int founderCount = 3;
|
int founderCount = 2;
|
||||||
long perFounderReward = blockReward / founderCount;
|
long perFounderReward = blockReward / founderCount;
|
||||||
|
|
||||||
// Alice simple self-share so her reward is perFounderReward
|
// Alice simple self-share so her reward is perFounderReward
|
||||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, perFounderReward);
|
AccountUtils.assertBalance(repository, "alice", Asset.QORT, perFounderReward);
|
||||||
|
|
||||||
// Bob not online so his reward is simply perFounderReward
|
// Bob not online so his reward is zero
|
||||||
AccountUtils.assertBalance(repository, "bob", Asset.QORT, perFounderReward);
|
AccountUtils.assertBalance(repository, "bob", Asset.QORT, 0L);
|
||||||
|
|
||||||
// Chloe has two reward-shares, so her reward is divided by 2
|
// Chloe has two reward-shares, so her reward is divided by 2
|
||||||
int chloeSharesCount = 2;
|
int chloeSharesCount = 2;
|
||||||
|
Loading…
Reference in New Issue
Block a user