Merge branch 'block-rewards' into launch

This commit is contained in:
catbref 2020-06-04 10:22:14 +01:00
commit ea4c51026b
8 changed files with 498 additions and 141 deletions

View File

@ -24,7 +24,7 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.AT;
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.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData;
@ -126,8 +126,13 @@ public class Block {
/** Locally-generated AT fees */
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. */
static class ExpandedAccount {
private static class ExpandedAccount {
private final RewardShareData rewardShareData;
private final int sharePercent;
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,
* 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)
return -1;
return null;
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
final int accountLevel = this.mintingAccountData.getLevel();
if (accountLevel <= 0)
return null;
for (int s = 0; s < sharesByLevel.size(); ++s)
if (sharesByLevel.get(s).levels.contains(accountLevel))
return s;
final AccountLevelShareBin[] shareBinsByLevel = BlockChain.getInstance().getShareBinsByAccountLevel();
if (accountLevel > shareBinsByLevel.length)
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) {
// minter & recipient the same - simpler case
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(accountAmount)));
if (accountAmount != 0)
this.mintingAccount.modifyAssetBalance(Asset.QORT, accountAmount);
balanceChanges.merge(this.mintingAccount, accountAmount, Long::sum);
} else {
// minter & recipient different - extra work needed
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)));
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)));
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. */
@ -1229,11 +1238,8 @@ public class Block {
// Increase account levels
increaseAccountLevels();
// Block rewards go before transactions processed
// Distribute block rewards, including transaction fees, before transactions processed
processBlockRewards();
// Give transaction fees to minter/reward-share account(s)
rewardTransactionFees();
}
// We're about to (test-)process a batch of transactions,
@ -1307,10 +1313,13 @@ public class Block {
}
protected void processBlockRewards() throws DataException {
// General block reward
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
// Add transaction fees
reward += this.blockData.getTotalFees();
// No reward for our height?
if (reward == 0)
// Nothing to reward?
if (reward <= 0)
return;
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 {
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.
this.cachedExpandedAccounts = null;
// Deduct any transaction fees from minter/reward-share account(s)
deductTransactionFees();
// Block rewards removed after transactions undone
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();
// Decrease account levels
@ -1541,25 +1537,18 @@ public class Block {
}
protected void orphanBlockRewards() throws DataException {
// General block reward
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
// Add transaction fees
reward += this.blockData.getTotalFees();
// No reward for our height?
if (reward == 0)
// Nothing to reward?
if (reward <= 0)
return;
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 {
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atStateData : this.getATStates()) {
@ -1615,62 +1604,218 @@ public class Block {
}
}
protected void distributeBlockReward(long totalAmount) throws DataException {
LOGGER.trace(() -> String.format("Distributing: %s", Amounts.prettyAmount(totalAmount)));
private static class BlockRewardCandidate {
public final String description;
public long share;
public final BlockRewardDistributor distributionMethod;
// Distribute according to account level
long sharedByLevelAmount = distributeBlockRewardByLevel(totalAmount);
LOGGER.trace(() -> String.format("Shared %s of %s based on account levels", Amounts.prettyAmount(sharedByLevelAmount), Amounts.prettyAmount(totalAmount)));
public BlockRewardCandidate(String description, long share, BlockRewardDistributor distributionMethod) {
this.description = description;
this.share = share;
this.distributionMethod = distributionMethod;
}
// Distribute amongst legacy QORA holders
long sharedByQoraHoldersAmount = distributeBlockRewardToQoraHolders(totalAmount);
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);
public long distribute(long distibutionAmount, Map<Account, Long> balanceChanges) throws DataException {
return this.distributionMethod.distribute(distibutionAmount, balanceChanges);
}
}
private long distributeBlockRewardByLevel(long totalAmount) throws DataException {
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
protected void distributeBlockReward(long totalAmount) throws DataException {
final long totalAmountForLogging = totalAmount;
LOGGER.trace(() -> String.format("Distributing: %s", Amounts.prettyAmount(totalAmountForLogging)));
// Distribute amount across bins
long sharedAmount = 0;
for (int s = 0; s < sharesByLevel.size(); ++s) {
final int binIndex = s;
final boolean isProcessingNotOrphaning = totalAmount >= 0;
long binAmount = Amounts.roundDownScaledMultiply(totalAmount, sharesByLevel.get(binIndex).share);
LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, Amounts.prettyAmount(totalAmount), Amounts.prettyAmount(binAmount)));
// How to distribute reward among groups, with ratio, IN ORDER
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.
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
// 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);
// 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())
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) {
ExpandedAccount expandedAccount = binnedAccounts.get(a);
expandedAccount.distribute(perAccountAmount);
sharedAmount += perAccountAmount;
}
BlockRewardCandidate rewardCandidate = new BlockRewardCandidate(description, accountLevelShareBin.share, accountLevelBinDistributor);
rewardCandidates.add(rewardCandidate);
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;
}
private long distributeBlockRewardToQoraHolders(long totalAmount) throws DataException {
long qoraHoldersAmount = Amounts.roundDownScaledMultiply(totalAmount, BlockChain.getInstance().getQoraHoldersShare());
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", Amounts.prettyAmount(totalAmount), Amounts.prettyAmount(qoraHoldersAmount)));
final boolean isProcessingNotOrphaning = totalAmount >= 0;
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<AccountBalanceData> qoraHolders, Map<Account, Long> balanceChanges, Block block) throws DataException {
final boolean isProcessingNotOrphaning = qoraHoldersAmount >= 0;
long qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
BigInteger qoraPerQortRewardBI = BigInteger.valueOf(qoraPerQortReward);
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
long totalQoraHeld = 0;
for (int i = 0; i < qoraHolders.size(); ++i)
totalQoraHeld += qoraHolders.get(i).getBalance();
@ -1678,14 +1823,14 @@ public class Block {
long finalTotalQoraHeld = totalQoraHeld;
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", Amounts.prettyAmount(finalTotalQoraHeld)));
long sharedAmount = 0;
if (totalQoraHeld <= 0)
return sharedAmount;
return 0;
// Could do with a faster 128bit integer library, but until then...
BigInteger qoraHoldersAmountBI = BigInteger.valueOf(qoraHoldersAmount);
BigInteger totalQoraHeldBI = BigInteger.valueOf(totalQoraHeld);
long sharedAmount = 0;
for (int h = 0; h < qoraHolders.size(); ++h) {
AccountBalanceData qoraHolder = qoraHolders.get(h);
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getBalance());
@ -1694,15 +1839,15 @@ public class Block {
// long holderReward = (qoraHoldersAmount * qoraHolder.getBalance()) / totalQoraHeld;
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",
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?
if (holderReward == 0)
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;
@ -1718,16 +1863,16 @@ public class Block {
newQortFromQoraBalance -= adjustment;
// This is also the QORA holder's final QORT-from-QORA block
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight());
this.repository.getAccountRepository().save(qortFromQoraData);
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, block.blockData.getHeight());
block.repository.getAccountRepository().save(qortFromQoraData);
long finalAdjustedHolderReward = holderReward;
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 {
// Orphaning
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
QortFromQoraData qortFromQoraData = block.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData != null) {
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
// So we use + here as qortFromQora is negative during orphaning.
@ -1737,15 +1882,15 @@ public class Block {
holderReward -= adjustment;
newQortFromQoraBalance -= adjustment;
this.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
long finalAdjustedHolderReward = holderReward;
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)
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
@ -1759,41 +1904,6 @@ public class Block {
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. */
private void postBlockTidy() throws DataException {
this.repository.getAccountRepository().tidy();

View File

@ -89,15 +89,17 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long reward;
}
List<RewardByHeight> rewardsByHeight;
private List<RewardByHeight> rewardsByHeight;
/** Share of block reward/fees by account level */
public static class ShareByLevel {
public static class AccountLevelShareBin {
public List<Integer> levels;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
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 */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
@ -116,7 +118,7 @@ public class BlockChain {
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
* 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.
@ -130,7 +132,7 @@ public class BlockChain {
* <p>
* Should NOT be present in blockchain config file!
*/
List<Integer> cumulativeBlocksByLevel;
private List<Integer> cumulativeBlocksByLevel;
/** Block times by block height */
public static class BlockTimingByHeight {
@ -139,7 +141,7 @@ public class BlockChain {
public long deviation; // ms
public double power;
}
List<BlockTimingByHeight> blockTimingsByHeight;
private List<BlockTimingByHeight> blockTimingsByHeight;
private int minAccountLevelToMint = 1;
private int minAccountLevelToRewardShare;
@ -316,10 +318,14 @@ public class BlockChain {
return this.rewardsByHeight;
}
public List<ShareByLevel> getBlockSharesByLevel() {
public List<AccountLevelShareBin> getAccountLevelShareBins() {
return this.sharesByLevel;
}
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
return this.shareBinsByLevel;
}
public List<Integer> getBlocksNeededByLevel() {
return this.blocksNeededByLevel;
}
@ -433,6 +439,15 @@ public class BlockChain {
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
if (!this.featureTriggers.containsKey(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. */
@ -447,6 +462,17 @@ public class BlockChain {
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
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);

View File

@ -241,15 +241,15 @@ public class RewardTests extends Common {
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// 3 founders (online or not) so blockReward divided by 3
int founderCount = 3;
// 2 founders online so blockReward divided by 2
int founderCount = 2;
long perFounderReward = blockReward / founderCount;
// Alice simple self-share so her reward is perFounderReward
AccountUtils.assertBalance(repository, "alice", Asset.QORT, perFounderReward);
// Bob not online so his reward is simply perFounderReward
AccountUtils.assertBalance(repository, "bob", Asset.QORT, perFounderReward);
// Bob not online so his reward is zero
AccountUtils.assertBalance(repository, "bob", Asset.QORT, 0L);
// Chloe has two reward-shares, so her reward is divided by 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);
}
}
}

View 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 }
]
}
}

View 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 }
]
}
}

View File

@ -0,0 +1,7 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-leftover-reward.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -1,6 +1,6 @@
{
"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,
"testNtpOffset": 0,
"minPeers": 0

View File

@ -0,0 +1,7 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-reward-scaling.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}