forked from Qortal/qortal
Merge branch 'block-rewards' into launch
This commit is contained in:
commit
ea4c51026b
@ -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,218 @@ 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(long totalAmount) throws DataException {
|
||||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
final long totalAmountForLogging = totalAmount;
|
||||||
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
LOGGER.trace(() -> String.format("Distributing: %s", Amounts.prettyAmount(totalAmountForLogging)));
|
||||||
|
|
||||||
// Distribute amount across bins
|
final boolean isProcessingNotOrphaning = totalAmount >= 0;
|
||||||
long sharedAmount = 0;
|
|
||||||
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 = determineBlockRewardCandidates(isProcessingNotOrphaning);
|
||||||
|
|
||||||
// Spread across all accounts in bin. getShareBin() returns -1 for minter accounts that are also founders, so they are effectively filtered out.
|
// Now distribute to candidates
|
||||||
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Distribute to these reward candidate accounts
|
||||||
|
final long distributionAmount = Amounts.roundDownScaledMultiply(totalAmount, rewardCandidate.share);
|
||||||
|
|
||||||
|
long sharedAmount = rewardCandidate.distribute(distributionAmount, balanceChanges);
|
||||||
|
remainingAmount -= sharedAmount;
|
||||||
|
|
||||||
|
// Reallocate any amount we didn't distribute, e.g. from maxxed legacy QORA holders
|
||||||
|
if (sharedAmount != distributionAmount)
|
||||||
|
totalAmount += Amounts.scaledDivide(distributionAmount - sharedAmount, 1_00000000 - rewardCandidate.share);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<BlockRewardCandidate> determineBlockRewardCandidates(boolean isProcessingNotOrphaning) throws DataException {
|
||||||
|
// How to distribute reward among groups, with ratio, IN ORDER
|
||||||
|
List<BlockRewardCandidate> rewardCandidates = new ArrayList<>();
|
||||||
|
|
||||||
|
// All online accounts
|
||||||
|
final List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Distribution rules:
|
||||||
|
*
|
||||||
|
* Distribution is based on the minting account of 'online' reward-shares.
|
||||||
|
*
|
||||||
|
* If ANY founders are online, then they receive the leftover non-distributed reward.
|
||||||
|
* If NO founders are online, then account-level-based rewards are scaled up so 100% of reward is allocated.
|
||||||
|
*
|
||||||
|
* If ANY non-maxxed legacy QORA holders exist then they are always allocated their fixed share (e.g. 20%).
|
||||||
|
*
|
||||||
|
* There has to be either at least one 'online' account for blocks to be minted
|
||||||
|
* so there is always either one account-level-based or founder reward candidate.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* With at least one founder online:
|
||||||
|
* Level 1/2 accounts: 5%
|
||||||
|
* Legacy QORA holders: 20%
|
||||||
|
* Founders: ~75%
|
||||||
|
*
|
||||||
|
* No online founders:
|
||||||
|
* Level 1/2 accounts: 5%
|
||||||
|
* Level 5/6 accounts: 15%
|
||||||
|
* Legacy QORA holders: 20%
|
||||||
|
* Total: 40%
|
||||||
|
*
|
||||||
|
* After scaling account-level-based shares to fill 100%:
|
||||||
|
* Level 1/2 accounts: 20%
|
||||||
|
* Level 5/6 accounts: 60%
|
||||||
|
* Legacy QORA holders: 20%
|
||||||
|
* Total: 100%
|
||||||
|
*/
|
||||||
|
long totalShares = 0;
|
||||||
|
|
||||||
|
// Determine whether we have any online founders
|
||||||
|
final List<ExpandedAccount> onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList());
|
||||||
|
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
|
||||||
|
|
||||||
|
// Determine reward candidates based on account level
|
||||||
|
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, accountLevelShareBin.share, accountLevelBinDistributor);
|
||||||
ExpandedAccount expandedAccount = binnedAccounts.get(a);
|
rewardCandidates.add(rewardCandidate);
|
||||||
expandedAccount.distribute(perAccountAmount);
|
|
||||||
sharedAmount += perAccountAmount;
|
totalShares += rewardCandidate.share;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
|
||||||
|
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||||
|
final boolean haveQoraHolders = !qoraHolders.isEmpty();
|
||||||
|
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
|
||||||
|
|
||||||
|
// Perform account-level-based reward scaling if appropriate
|
||||||
|
if (!haveFounders) {
|
||||||
|
// Recalculate distribution ratios based on candidates
|
||||||
|
|
||||||
|
// Nothing shared? This shouldn't happen
|
||||||
|
if (totalShares == 0)
|
||||||
|
throw new DataException("Unexpected lack of block reward candidates?");
|
||||||
|
|
||||||
|
// Re-scale individual reward candidate's share as if total shared was 100% - legacy QORA holders' share
|
||||||
|
long scalingFactor;
|
||||||
|
if (haveQoraHolders)
|
||||||
|
scalingFactor = Amounts.scaledDivide(totalShares, 1_00000000 - qoraHoldersShare);
|
||||||
|
else
|
||||||
|
scalingFactor = totalShares;
|
||||||
|
|
||||||
|
for (BlockRewardCandidate rewardCandidate : rewardCandidates)
|
||||||
|
rewardCandidate.share = Amounts.scaledDivide(rewardCandidate.share, scalingFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add legacy QORA holders as reward candidate with fixed share (if appropriate)
|
||||||
|
if (haveQoraHolders) {
|
||||||
|
// Yes: add to reward candidates list
|
||||||
|
BlockRewardDistributor legacyQoraHoldersDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardToQoraHolders(distributionAmount, qoraHolders, balanceChanges, this);
|
||||||
|
|
||||||
|
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Legacy QORA holders", qoraHoldersShare, legacyQoraHoldersDistributor);
|
||||||
|
|
||||||
|
if (haveFounders)
|
||||||
|
// We have founders, so distribute legacy QORA holders just before founders so founders get any non-distributed
|
||||||
|
rewardCandidates.add(rewardCandidate);
|
||||||
|
else
|
||||||
|
// No founder, so distribute legacy QORA holders first, so all account-level-based rewards get share of any non-distributed
|
||||||
|
rewardCandidates.add(0, rewardCandidate);
|
||||||
|
|
||||||
|
totalShares += rewardCandidate.share;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add founders as reward candidate if appropriate
|
||||||
|
if (haveFounders) {
|
||||||
|
// Yes: add to reward candidates list
|
||||||
|
BlockRewardDistributor founderDistributor = (distributionAmount, balanceChanges) -> distributeBlockRewardShare(distributionAmount, onlineFounderAccounts, balanceChanges);
|
||||||
|
|
||||||
|
final long foundersShare = 1_00000000 - totalShares;
|
||||||
|
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate("Founders", foundersShare, founderDistributor);
|
||||||
|
rewardCandidates.add(rewardCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewardCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@ -1678,14 +1823,14 @@ public class Block {
|
|||||||
long finalTotalQoraHeld = totalQoraHeld;
|
long finalTotalQoraHeld = totalQoraHeld;
|
||||||
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", Amounts.prettyAmount(finalTotalQoraHeld)));
|
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", Amounts.prettyAmount(finalTotalQoraHeld)));
|
||||||
|
|
||||||
long sharedAmount = 0;
|
|
||||||
if (totalQoraHeld <= 0)
|
if (totalQoraHeld <= 0)
|
||||||
return sharedAmount;
|
return 0;
|
||||||
|
|
||||||
// Could do with a faster 128bit integer library, but until then...
|
// Could do with a faster 128bit integer library, but until then...
|
||||||
BigInteger qoraHoldersAmountBI = BigInteger.valueOf(qoraHoldersAmount);
|
BigInteger qoraHoldersAmountBI = BigInteger.valueOf(qoraHoldersAmount);
|
||||||
BigInteger totalQoraHeldBI = BigInteger.valueOf(totalQoraHeld);
|
BigInteger totalQoraHeldBI = BigInteger.valueOf(totalQoraHeld);
|
||||||
|
|
||||||
|
long sharedAmount = 0;
|
||||||
for (int h = 0; h < qoraHolders.size(); ++h) {
|
for (int h = 0; h < qoraHolders.size(); ++h) {
|
||||||
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
||||||
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getBalance());
|
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getBalance());
|
||||||
@ -1694,15 +1839,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 +1863,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 +1882,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 +1904,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)
|
||||||
@ -116,7 +118,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 +132,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 +141,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 +318,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;
|
||||||
}
|
}
|
||||||
@ -433,6 +439,15 @@ public class BlockChain {
|
|||||||
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
|
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
|
||||||
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
||||||
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
||||||
|
|
||||||
|
// Check block reward share bounds
|
||||||
|
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)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Minor normalization, cached value generation, etc. */
|
/** Minor normalization, cached value generation, etc. */
|
||||||
@ -447,6 +462,17 @@ public class BlockChain {
|
|||||||
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
@ -269,4 +269,71 @@ public class RewardTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check account-level-based reward scaling when no founders are online. */
|
||||||
|
@Test
|
||||||
|
public void testNoFounderRewardScaling() throws DataException {
|
||||||
|
Common.useSettings("test-settings-v2-reward-scaling.json");
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
// Dilbert needs to create a self-share
|
||||||
|
byte[] dilbertSelfSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); // Block minted by Alice
|
||||||
|
PrivateKeyAccount dilbertSelfShareAccount = new PrivateKeyAccount(repository, dilbertSelfSharePrivateKey);
|
||||||
|
|
||||||
|
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
|
||||||
|
|
||||||
|
long blockReward = BlockUtils.getNextBlockReward(repository);
|
||||||
|
|
||||||
|
BlockMinter.mintTestingBlock(repository, dilbertSelfShareAccount);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dilbert is only account 'online'.
|
||||||
|
* No founders online.
|
||||||
|
* Some legacy QORA holders.
|
||||||
|
*
|
||||||
|
* So Dilbert should receive 100% - legacy QORA holder's share.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
|
||||||
|
final long remainingShare = 1_00000000 - qoraHoldersShare;
|
||||||
|
|
||||||
|
long dilbertExpectedBalance = initialBalances.get("dilbert").get(Asset.QORT);
|
||||||
|
dilbertExpectedBalance += Amounts.roundDownScaledMultiply(blockReward, remainingShare);
|
||||||
|
|
||||||
|
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertExpectedBalance);
|
||||||
|
|
||||||
|
// After several blocks, the legacy QORA holder should be maxxed out
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Now Dilbert should be receiving 100% of block reward
|
||||||
|
blockReward = BlockUtils.getNextBlockReward(repository);
|
||||||
|
|
||||||
|
BlockMinter.mintTestingBlock(repository, dilbertSelfShareAccount);
|
||||||
|
|
||||||
|
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertExpectedBalance + blockReward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check leftover legacy QORA reward goes to online founders. */
|
||||||
|
@Test
|
||||||
|
public void testLeftoverReward() throws DataException {
|
||||||
|
Common.useSettings("test-settings-v2-leftover-reward.json");
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
|
||||||
|
|
||||||
|
long blockReward = BlockUtils.getNextBlockReward(repository);
|
||||||
|
|
||||||
|
BlockUtils.mintBlock(repository); // Block minted by Alice self-share
|
||||||
|
|
||||||
|
// Chloe maxxes out her legacy QORA reward so some is leftover to reward to Alice.
|
||||||
|
|
||||||
|
TestAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||||
|
final long chloeQortFromQora = chloe.getConfirmedBalance(Asset.QORT_FROM_QORA);
|
||||||
|
|
||||||
|
long expectedBalance = initialBalances.get("alice").get(Asset.QORT) + blockReward - chloeQortFromQora;
|
||||||
|
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
70
src/test/resources/test-chain-v2-leftover-reward.json
Normal file
70
src/test/resources/test-chain-v2-leftover-reward.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"isTestChain": true,
|
||||||
|
"blockTimestampMargin": 500,
|
||||||
|
"transactionExpiryPeriod": 86400000,
|
||||||
|
"maxBlockSize": 2097152,
|
||||||
|
"maxBytesPerUnitFee": 1024,
|
||||||
|
"unitFee": "0.1",
|
||||||
|
"requireGroupForApproval": false,
|
||||||
|
"minAccountLevelToRewardShare": 5,
|
||||||
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
|
"founderEffectiveMintingLevel": 10,
|
||||||
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"rewardsByHeight": [
|
||||||
|
{ "height": 1, "reward": 100 },
|
||||||
|
{ "height": 11, "reward": 10 },
|
||||||
|
{ "height": 21, "reward": 1 }
|
||||||
|
],
|
||||||
|
"sharesByLevel": [
|
||||||
|
{ "levels": [ 1, 2 ], "share": 0.05 },
|
||||||
|
{ "levels": [ 3, 4 ], "share": 0.10 },
|
||||||
|
{ "levels": [ 5, 6 ], "share": 0.15 },
|
||||||
|
{ "levels": [ 7, 8 ], "share": 0.20 },
|
||||||
|
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||||
|
],
|
||||||
|
"qoraHoldersShare": 0.20,
|
||||||
|
"qoraPerQortReward": 250,
|
||||||
|
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||||
|
"blockTimingsByHeight": [
|
||||||
|
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||||
|
],
|
||||||
|
"ciyamAtSettings": {
|
||||||
|
"feePerStep": "0.0001",
|
||||||
|
"maxStepsPerRound": 500,
|
||||||
|
"stepsPerFunctionCall": 10,
|
||||||
|
"minutesPerBlock": 1
|
||||||
|
},
|
||||||
|
"featureTriggers": {
|
||||||
|
"messageHeight": 0,
|
||||||
|
"atHeight": 0,
|
||||||
|
"assetsTimestamp": 0,
|
||||||
|
"votingTimestamp": 0,
|
||||||
|
"arbitraryTimestamp": 0,
|
||||||
|
"powfixTimestamp": 0,
|
||||||
|
"qortalTimestamp": 0,
|
||||||
|
"newAssetPricingTimestamp": 0,
|
||||||
|
"groupApprovalTimestamp": 0
|
||||||
|
},
|
||||||
|
"genesisInfo": {
|
||||||
|
"version": 4,
|
||||||
|
"timestamp": 0,
|
||||||
|
"transactions": [
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
|
||||||
|
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||||
|
|
||||||
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "4000", "assetId": 1 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||||
|
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
70
src/test/resources/test-chain-v2-reward-scaling.json
Normal file
70
src/test/resources/test-chain-v2-reward-scaling.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"isTestChain": true,
|
||||||
|
"blockTimestampMargin": 500,
|
||||||
|
"transactionExpiryPeriod": 86400000,
|
||||||
|
"maxBlockSize": 2097152,
|
||||||
|
"maxBytesPerUnitFee": 1024,
|
||||||
|
"unitFee": "0.1",
|
||||||
|
"requireGroupForApproval": false,
|
||||||
|
"minAccountLevelToRewardShare": 5,
|
||||||
|
"maxRewardSharesPerMintingAccount": 20,
|
||||||
|
"founderEffectiveMintingLevel": 10,
|
||||||
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"rewardsByHeight": [
|
||||||
|
{ "height": 1, "reward": 100 },
|
||||||
|
{ "height": 11, "reward": 10 },
|
||||||
|
{ "height": 21, "reward": 1 }
|
||||||
|
],
|
||||||
|
"sharesByLevel": [
|
||||||
|
{ "levels": [ 1, 2 ], "share": 0.05 },
|
||||||
|
{ "levels": [ 3, 4 ], "share": 0.10 },
|
||||||
|
{ "levels": [ 5, 6 ], "share": 0.15 },
|
||||||
|
{ "levels": [ 7, 8 ], "share": 0.20 },
|
||||||
|
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||||
|
],
|
||||||
|
"qoraHoldersShare": 0.20,
|
||||||
|
"qoraPerQortReward": 250,
|
||||||
|
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||||
|
"blockTimingsByHeight": [
|
||||||
|
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||||
|
],
|
||||||
|
"ciyamAtSettings": {
|
||||||
|
"feePerStep": "0.0001",
|
||||||
|
"maxStepsPerRound": 500,
|
||||||
|
"stepsPerFunctionCall": 10,
|
||||||
|
"minutesPerBlock": 1
|
||||||
|
},
|
||||||
|
"featureTriggers": {
|
||||||
|
"messageHeight": 0,
|
||||||
|
"atHeight": 0,
|
||||||
|
"assetsTimestamp": 0,
|
||||||
|
"votingTimestamp": 0,
|
||||||
|
"arbitraryTimestamp": 0,
|
||||||
|
"powfixTimestamp": 0,
|
||||||
|
"qortalTimestamp": 0,
|
||||||
|
"newAssetPricingTimestamp": 0,
|
||||||
|
"groupApprovalTimestamp": 0
|
||||||
|
},
|
||||||
|
"genesisInfo": {
|
||||||
|
"version": 4,
|
||||||
|
"timestamp": 0,
|
||||||
|
"transactions": [
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
|
||||||
|
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||||
|
|
||||||
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "40000", "assetId": 1 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||||
|
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
7
src/test/resources/test-settings-v2-leftover-reward.json
Normal file
7
src/test/resources/test-settings-v2-leftover-reward.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"restrictedApi": false,
|
||||||
|
"blockchainConfig": "src/test/resources/test-chain-v2-leftover-reward.json",
|
||||||
|
"wipeUnconfirmedOnStart": false,
|
||||||
|
"testNtpOffset": 0,
|
||||||
|
"minPeers": 0
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"restrictedApi": false,
|
"restrictedApi": false,
|
||||||
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
|
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder-extremes.json",
|
||||||
"wipeUnconfirmedOnStart": false,
|
"wipeUnconfirmedOnStart": false,
|
||||||
"testNtpOffset": 0,
|
"testNtpOffset": 0,
|
||||||
"minPeers": 0
|
"minPeers": 0
|
||||||
|
7
src/test/resources/test-settings-v2-reward-scaling.json
Normal file
7
src/test/resources/test-settings-v2-reward-scaling.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"restrictedApi": false,
|
||||||
|
"blockchainConfig": "src/test/resources/test-chain-v2-reward-scaling.json",
|
||||||
|
"wipeUnconfirmedOnStart": false,
|
||||||
|
"testNtpOffset": 0,
|
||||||
|
"minPeers": 0
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user