Cancel reward-shares with NEGATIVE share instead of ZERO. Also: bug-fixes!

Now reward-shares with zero percent are valid, to allow the 'recipient' party to gain
"number of minted blocks" but no actual block reward.

Correspondly, the special zero share used to cancel reward-shares has been changed to
be any negative value.

Block rewards, founder 'leftovers': if founder is minter account in any online
reward shares, then the per-founder-share is spread across their online reward-shares,
otherwise it's simply/wholy given to that founder.

Created a new DB table to hold "next block height", updated via triggers on Blocks.
This is so various sub-queries can simply read the next-block-height value instead
of complex IFNULL(MAX(height),0)+1 or SELECT height FROM Blocks ORDER BY height DESC.
Prior code was also broken in edge cases, e.g. no genesis block, or ran slow.

Added tests to cover above.

Deleted BTC tests as they're obsolete.

Added/improved other tests.
This commit is contained in:
catbref
2019-12-11 13:40:42 +00:00
parent d01504a541
commit 42bd68230b
13 changed files with 155 additions and 95 deletions

View File

@@ -153,14 +153,23 @@ public class Block {
this.isRecipientAlsoMinter = this.mintingAccountData.getAddress().equals(this.recipientAccountData.getAddress());
}
/**
* Returns share bin for expanded account.
* <p>
* 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
*/
int getShareBin() {
if (this.isMinterFounder)
return -1;
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
final int accountLevel = this.mintingAccountData.getLevel();
for (int s = 0; s < sharesByLevel.size(); ++s)
if (sharesByLevel.get(s).levels.contains(this.mintingAccountData.getLevel()))
if (sharesByLevel.get(s).levels.contains(accountLevel))
return s;
return -1;
@@ -1603,8 +1612,8 @@ public class Block {
BigDecimal binAmount = sharesByLevel.get(binIndex).share.multiply(totalAmount).setScale(8, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, totalAmount.toPlainString(), binAmount.toPlainString()));
// Spread across all accounts in bin
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> !accountInfo.isMinterFounder && accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
// 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());
if (binnedAccounts.isEmpty())
continue;
@@ -1731,8 +1740,20 @@ public class Block {
perFounderAmount.toPlainString()));
for (int a = 0; a < founderAccounts.size(); ++a) {
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
// 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).collect(Collectors.toList());
if (founderExpandedAccounts.isEmpty()) {
// Simple case: no founder-as-minter reward-shares online so founder gets whole amount.
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
} else {
// Distribute over reward-shares
BigDecimal perFounderRewardShareAmount = perFounderAmount.divide(BigDecimal.valueOf(founderExpandedAccounts.size()), RoundingMode.DOWN);
for (int fea = 0; fea < founderExpandedAccounts.size(); ++fea)
founderExpandedAccounts.get(fea).distribute(perFounderRewardShareAmount);
}
}
}

View File

@@ -4,7 +4,7 @@ import com.google.common.primitives.Bytes;
public class MemoryPoW {
private static final int WORK_BUFFER_LENGTH = 4 * 1024 * 1024;
public static final int WORK_BUFFER_LENGTH = 4 * 1024 * 1024;
private static final int WORK_BUFFER_LENGTH_MASK = WORK_BUFFER_LENGTH - 1;
private static final int HASH_LENGTH = 32;

View File

@@ -27,6 +27,7 @@ public class RewardShareTransactionData extends TransactionData {
@Schema(example = "reward_share_public_key")
private byte[] rewardSharePublicKey;
@Schema(description = "Percentage of block rewards that minter shares to recipient, or negative value to cancel existing reward-share")
private BigDecimal sharePercent;
// No need to ever expose this via API

View File

@@ -474,10 +474,11 @@ public class HSQLDBAccountRepository implements AccountRepository {
public void save(AccountBalanceData accountBalanceData) throws DataException {
// If balance is zero and there are no prior historic balance, then simply delete balances for this assetId (typically during orphaning)
if (accountBalanceData.getBalance().signum() == 0) {
String existsSql = "account = ? AND asset_id = ? AND height < (SELECT height - 1 FROM NextBlockHeight)"; // height prior to current block. no matches (obviously) prior to genesis block
boolean hasPriorBalances;
try {
hasPriorBalances = this.repository.exists("HistoricAccountBalances", "account = ? AND asset_id = ? AND height < (SELECT IFNULL(MAX(height), 1) FROM Blocks)",
accountBalanceData.getAddress(), accountBalanceData.getAssetId());
hasPriorBalances = this.repository.exists("HistoricAccountBalances", existsSql, accountBalanceData.getAddress(), accountBalanceData.getAssetId());
} catch (SQLException e) {
throw new DataException("Unable to check for historic account balances in repository", e);
}

View File

@@ -876,6 +876,30 @@ public class HSQLDBDatabaseUpdates {
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
break;
case 62:
// Rework sub-queries that need to know next block height as currently they fail for genesis block and/or are still too slow
// Table to hold next block height.
stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)");
// Initial value - should work for empty DB or populated DB.
stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)");
// We use triggers on Blocks to update a simple "next block height" table
String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)";
stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql);
stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql);
stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql);
// Now update previously slow/broken sub-queries
stmt.execute("DROP TRIGGER Historic_account_balance_insert_trigger");
stmt.execute("DROP TRIGGER Historic_account_balance_update_trigger");
stmt.execute("CREATE TRIGGER Historic_account_balance_insert_trigger AFTER INSERT ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW "
+ "INSERT INTO HistoricAccountBalances VALUES "
+ "(new_row.account, new_row.asset_id, (SELECT height from NextBlockHeight), new_row.balance) "
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
stmt.execute("CREATE TRIGGER Historic_account_balance_update_trigger AFTER UPDATE ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW "
+ "INSERT INTO HistoricAccountBalances VALUES "
+ "(new_row.account, new_row.asset_id, (SELECT height from NextBlockHeight), new_row.balance) "
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
break;
default:
// nothing to do
return false;

View File

@@ -112,9 +112,8 @@ public class RewardShareTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Check reward share given to recipient
if (this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0
|| this.rewardShareTransactionData.getSharePercent().compareTo(MAX_SHARE) > 0)
// Check reward share given to recipient. Negative is potentially OK to end a current reward-share. Zero also fine.
if (this.rewardShareTransactionData.getSharePercent().compareTo(MAX_SHARE) > 0)
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
PublicKeyAccount creator = getCreator();
@@ -144,13 +143,13 @@ public class RewardShareTransaction extends Transaction {
if (existingRewardShareData != null && !this.doesRewardShareMatch(existingRewardShareData))
return ValidationResult.INVALID_PUBLIC_KEY;
final boolean isSharePercentZero = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) == 0;
final boolean isSharePercentNegative = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0;
if (existingRewardShareData == null) {
// This is a new reward-share
// No point starting a new reward-share with 0% share (i.e. delete reward-share)
if (isSharePercentZero)
// No point starting a new reward-share with negative share (i.e. delete reward-share)
if (isSharePercentNegative)
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
// Check the minting account hasn't reach maximum number of reward-shares
@@ -161,7 +160,7 @@ public class RewardShareTransaction extends Transaction {
// This transaction intends to modify/terminate an existing reward-share
// Modifying an existing self-share is pointless and forbidden (due to 0 fee). Deleting self-share is OK though.
if (isRecipientAlsoMinter && !isSharePercentZero)
if (isRecipientAlsoMinter && !isSharePercentNegative)
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
}
@@ -188,8 +187,10 @@ public class RewardShareTransaction extends Transaction {
// Save this transaction, with previous share info
this.repository.getTransactionRepository().save(rewardShareTransactionData);
// 0% share is actually a request to delete existing reward-share
if (rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) == 0) {
final boolean isSharePercentNegative = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0;
// Negative share is actually a request to delete existing reward-share
if (isSharePercentNegative) {
this.repository.getAccountRepository().delete(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient());
} else {
// Save reward-share info