forked from Qortal/qortal
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:
parent
d01504a541
commit
42bd68230b
@ -153,14 +153,23 @@ public class Block {
|
|||||||
this.isRecipientAlsoMinter = this.mintingAccountData.getAddress().equals(this.recipientAccountData.getAddress());
|
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() {
|
int getShareBin() {
|
||||||
if (this.isMinterFounder)
|
if (this.isMinterFounder)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
||||||
|
final int accountLevel = this.mintingAccountData.getLevel();
|
||||||
|
|
||||||
for (int s = 0; s < sharesByLevel.size(); ++s)
|
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 s;
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
@ -1603,8 +1612,8 @@ public class Block {
|
|||||||
BigDecimal binAmount = sharesByLevel.get(binIndex).share.multiply(totalAmount).setScale(8, RoundingMode.DOWN);
|
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()));
|
LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, totalAmount.toPlainString(), binAmount.toPlainString()));
|
||||||
|
|
||||||
// Spread across all accounts in bin
|
// 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.isMinterFounder && accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
|
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == binIndex).collect(Collectors.toList());
|
||||||
if (binnedAccounts.isEmpty())
|
if (binnedAccounts.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -1731,8 +1740,20 @@ public class Block {
|
|||||||
perFounderAmount.toPlainString()));
|
perFounderAmount.toPlainString()));
|
||||||
|
|
||||||
for (int a = 0; a < founderAccounts.size(); ++a) {
|
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.
|
||||||
founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import com.google.common.primitives.Bytes;
|
|||||||
|
|
||||||
public class MemoryPoW {
|
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 WORK_BUFFER_LENGTH_MASK = WORK_BUFFER_LENGTH - 1;
|
||||||
|
|
||||||
private static final int HASH_LENGTH = 32;
|
private static final int HASH_LENGTH = 32;
|
||||||
|
@ -27,6 +27,7 @@ public class RewardShareTransactionData extends TransactionData {
|
|||||||
@Schema(example = "reward_share_public_key")
|
@Schema(example = "reward_share_public_key")
|
||||||
private byte[] rewardSharePublicKey;
|
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;
|
private BigDecimal sharePercent;
|
||||||
|
|
||||||
// No need to ever expose this via API
|
// No need to ever expose this via API
|
||||||
|
@ -474,10 +474,11 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
public void save(AccountBalanceData accountBalanceData) throws DataException {
|
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 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) {
|
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;
|
boolean hasPriorBalances;
|
||||||
try {
|
try {
|
||||||
hasPriorBalances = this.repository.exists("HistoricAccountBalances", "account = ? AND asset_id = ? AND height < (SELECT IFNULL(MAX(height), 1) FROM Blocks)",
|
hasPriorBalances = this.repository.exists("HistoricAccountBalances", existsSql, accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
||||||
accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to check for historic account balances in repository", e);
|
throw new DataException("Unable to check for historic account balances in repository", e);
|
||||||
}
|
}
|
||||||
|
@ -876,6 +876,30 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
|
+ "ON DUPLICATE KEY UPDATE balance = new_row.balance");
|
||||||
break;
|
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:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -112,9 +112,8 @@ public class RewardShareTransaction extends Transaction {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult isValid() throws DataException {
|
public ValidationResult isValid() throws DataException {
|
||||||
// Check reward share given to recipient
|
// Check reward share given to recipient. Negative is potentially OK to end a current reward-share. Zero also fine.
|
||||||
if (this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0
|
if (this.rewardShareTransactionData.getSharePercent().compareTo(MAX_SHARE) > 0)
|
||||||
|| this.rewardShareTransactionData.getSharePercent().compareTo(MAX_SHARE) > 0)
|
|
||||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||||
|
|
||||||
PublicKeyAccount creator = getCreator();
|
PublicKeyAccount creator = getCreator();
|
||||||
@ -144,13 +143,13 @@ public class RewardShareTransaction extends Transaction {
|
|||||||
if (existingRewardShareData != null && !this.doesRewardShareMatch(existingRewardShareData))
|
if (existingRewardShareData != null && !this.doesRewardShareMatch(existingRewardShareData))
|
||||||
return ValidationResult.INVALID_PUBLIC_KEY;
|
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) {
|
if (existingRewardShareData == null) {
|
||||||
// This is a new reward-share
|
// This is a new reward-share
|
||||||
|
|
||||||
// No point starting a new reward-share with 0% share (i.e. delete reward-share)
|
// No point starting a new reward-share with negative share (i.e. delete reward-share)
|
||||||
if (isSharePercentZero)
|
if (isSharePercentNegative)
|
||||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||||
|
|
||||||
// Check the minting account hasn't reach maximum number of reward-shares
|
// 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
|
// 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.
|
// 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;
|
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,8 +187,10 @@ public class RewardShareTransaction extends Transaction {
|
|||||||
// Save this transaction, with previous share info
|
// Save this transaction, with previous share info
|
||||||
this.repository.getTransactionRepository().save(rewardShareTransactionData);
|
this.repository.getTransactionRepository().save(rewardShareTransactionData);
|
||||||
|
|
||||||
// 0% share is actually a request to delete existing reward-share
|
final boolean isSharePercentNegative = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0;
|
||||||
if (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());
|
this.repository.getAccountRepository().delete(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient());
|
||||||
} else {
|
} else {
|
||||||
// Save reward-share info
|
// Save reward-share info
|
||||||
|
@ -96,6 +96,11 @@ public class AccountBalanceTests extends Common {
|
|||||||
|
|
||||||
BigDecimal initialBalance = testNewerBalance(repository, alice);
|
BigDecimal initialBalance = testNewerBalance(repository, alice);
|
||||||
|
|
||||||
|
// Fetch all historic balances
|
||||||
|
List<AccountBalanceData> historicBalances = repository.getAccountRepository().getHistoricBalances(alice.getAddress(), Asset.QORT);
|
||||||
|
for (AccountBalanceData historicBalance : historicBalances)
|
||||||
|
System.out.println(String.format("Balance at height %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
||||||
|
|
||||||
// Fetch balance at height 1, even though newer balance exists
|
// Fetch balance at height 1, even though newer balance exists
|
||||||
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(alice.getAddress(), Asset.QORT, 1);
|
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(alice.getAddress(), Asset.QORT, 1);
|
||||||
BigDecimal genesisBalance = accountBalanceData.getBalance();
|
BigDecimal genesisBalance = accountBalanceData.getBalance();
|
||||||
@ -115,6 +120,7 @@ public class AccountBalanceTests extends Common {
|
|||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, publicKey);
|
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, publicKey);
|
||||||
|
System.out.println(String.format("Test recipient: %s", recipientAccount.getAddress()));
|
||||||
|
|
||||||
// Mint a few blocks
|
// Mint a few blocks
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
@ -124,6 +130,12 @@ public class AccountBalanceTests extends Common {
|
|||||||
BigDecimal balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
BigDecimal balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||||
assertEqualBigDecimals("recipient's balance should be zero", BigDecimal.ZERO, balance);
|
assertEqualBigDecimals("recipient's balance should be zero", BigDecimal.ZERO, balance);
|
||||||
|
|
||||||
|
// Confirm recipient has no historic balances
|
||||||
|
List<AccountBalanceData> historicBalances = repository.getAccountRepository().getHistoricBalances(recipientAccount.getAddress(), Asset.QORT);
|
||||||
|
for (AccountBalanceData historicBalance : historicBalances)
|
||||||
|
System.err.println(String.format("Block %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
||||||
|
assertTrue("recipient should not have historic balances yet", historicBalances.isEmpty());
|
||||||
|
|
||||||
// Send 1 QORT to recipient
|
// Send 1 QORT to recipient
|
||||||
TestAccount sendingAccount = Common.getTestAccount(repository, "alice");
|
TestAccount sendingAccount = Common.getTestAccount(repository, "alice");
|
||||||
pay(repository, sendingAccount, recipientAccount, BigDecimal.ONE);
|
pay(repository, sendingAccount, recipientAccount, BigDecimal.ONE);
|
||||||
@ -145,7 +157,7 @@ public class AccountBalanceTests extends Common {
|
|||||||
balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
balance = recipientAccount.getConfirmedBalance(Asset.QORT);
|
||||||
assertEqualBigDecimals("recipient's balance incorrect", totalAmount, balance);
|
assertEqualBigDecimals("recipient's balance incorrect", totalAmount, balance);
|
||||||
|
|
||||||
List<AccountBalanceData> historicBalances = repository.getAccountRepository().getHistoricBalances(recipientAccount.getAddress(), Asset.QORT);
|
historicBalances = repository.getAccountRepository().getHistoricBalances(recipientAccount.getAddress(), Asset.QORT);
|
||||||
for (AccountBalanceData historicBalance : historicBalances)
|
for (AccountBalanceData historicBalance : historicBalances)
|
||||||
System.out.println(String.format("Block %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
System.out.println(String.format("Block %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
||||||
|
|
||||||
@ -172,6 +184,12 @@ public class AccountBalanceTests extends Common {
|
|||||||
// Re-check balance from (now) invalid height
|
// Re-check balance from (now) invalid height
|
||||||
accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2);
|
accountBalanceData = repository.getAccountRepository().getBalance(recipientAccount.getAddress(), Asset.QORT, height - 2);
|
||||||
assertNull("recipient's invalid-height balance data should be null", accountBalanceData);
|
assertNull("recipient's invalid-height balance data should be null", accountBalanceData);
|
||||||
|
|
||||||
|
// Confirm recipient has no historic balances
|
||||||
|
historicBalances = repository.getAccountRepository().getHistoricBalances(recipientAccount.getAddress(), Asset.QORT);
|
||||||
|
for (AccountBalanceData historicBalance : historicBalances)
|
||||||
|
System.err.println(String.format("Block %d: %s", historicBalance.getHeight(), historicBalance.getBalance().toPlainString()));
|
||||||
|
assertTrue("recipient should have no remaining historic balances", historicBalances.isEmpty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +209,7 @@ public class AccountBalanceTests extends Common {
|
|||||||
@Test
|
@Test
|
||||||
public void testRepositorySpeed() throws DataException, SQLException {
|
public void testRepositorySpeed() throws DataException, SQLException {
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
final long MAX_QUERY_TIME = 100L; // ms
|
final long MAX_QUERY_TIME = 80L; // ms
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
System.out.println("Creating random accounts...");
|
System.out.println("Creating random accounts...");
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
package org.qora.test;
|
|
||||||
|
|
||||||
import org.bitcoinj.script.Script;
|
|
||||||
import org.bitcoinj.script.ScriptBuilder;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.qora.crosschain.BTC;
|
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
|
||||||
|
|
||||||
public class BTCTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWatchAddress() throws Exception {
|
|
||||||
// String testAddress = "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M";
|
|
||||||
String testAddress = "1GRENT17xMQe2ukPhwAeZU1TaUUon1Qc65";
|
|
||||||
|
|
||||||
long testStartTime = 1539000000L;
|
|
||||||
|
|
||||||
BTC btc = BTC.getInstance();
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
// btc.watch(testAddress, testStartTime);
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
// Thread.sleep(5000);
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
// btc.watch(testAddress, testStartTime);
|
|
||||||
|
|
||||||
btc.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWatchScript() throws Exception {
|
|
||||||
long testStartTime = 1539000000L;
|
|
||||||
|
|
||||||
BTC btc = BTC.getInstance();
|
|
||||||
|
|
||||||
byte[] redeemScriptHash = HashCode.fromString("3dbcc35e69ebc449f616fa3eb3723dfad9cbb5b3").asBytes();
|
|
||||||
Script redeemScript = ScriptBuilder.createP2SHOutputScript(redeemScriptHash);
|
|
||||||
redeemScript.setCreationTimeSeconds(testStartTime);
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
// btc.watch(redeemScript);
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
Thread.sleep(5000);
|
|
||||||
|
|
||||||
// Disabled for now, pending further work
|
|
||||||
// btc.watch(redeemScript);
|
|
||||||
|
|
||||||
btc.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void updateCheckpoints() throws Exception {
|
|
||||||
BTC btc = BTC.getInstance();
|
|
||||||
|
|
||||||
btc.updateCheckpoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -50,6 +50,9 @@ public class EPCTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
|
private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
|
||||||
|
final int runTime = 60; // seconds
|
||||||
|
System.out.println(String.format("Testing EPC for %s seconds:", runTime));
|
||||||
|
|
||||||
final long start = System.currentTimeMillis();
|
final long start = System.currentTimeMillis();
|
||||||
testEPC.start();
|
testEPC.start();
|
||||||
|
|
||||||
@ -67,7 +70,7 @@ public class EPCTests {
|
|||||||
}, 1L, 1L, TimeUnit.SECONDS);
|
}, 1L, 1L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
// Let it run for a minute
|
// Let it run for a minute
|
||||||
Thread.sleep(60_000L);
|
Thread.sleep(runTime * 1000L);
|
||||||
statusExecutor.shutdownNow();
|
statusExecutor.shutdownNow();
|
||||||
|
|
||||||
final long before = System.currentTimeMillis();
|
final long before = System.currentTimeMillis();
|
||||||
|
@ -24,7 +24,7 @@ public class MemoryPoWTests {
|
|||||||
Integer nonce = MemoryPoW.compute(data, start, range, difficulty);
|
Integer nonce = MemoryPoW.compute(data, start, range, difficulty);
|
||||||
long finishTime = System.currentTimeMillis();
|
long finishTime = System.currentTimeMillis();
|
||||||
|
|
||||||
System.out.println(String.format("Memory-hard PoW took %dms", finishTime - startTime));
|
System.out.println(String.format("Memory-hard PoW (buffer size: %dKB, range: %d, leading zeros: %d) took %dms", MemoryPoW.WORK_BUFFER_LENGTH / 1024, range, difficulty, finishTime - startTime));
|
||||||
|
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
|
|
||||||
|
@ -7,11 +7,14 @@ import org.qora.asset.Asset;
|
|||||||
import org.qora.repository.DataException;
|
import org.qora.repository.DataException;
|
||||||
import org.qora.repository.Repository;
|
import org.qora.repository.Repository;
|
||||||
import org.qora.repository.RepositoryManager;
|
import org.qora.repository.RepositoryManager;
|
||||||
|
import org.qora.repository.hsqldb.HSQLDBRepository;
|
||||||
|
import org.qora.test.common.BlockUtils;
|
||||||
import org.qora.test.common.Common;
|
import org.qora.test.common.Common;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -90,4 +93,52 @@ public class RepositoryTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
||||||
|
@Test
|
||||||
|
public void testBlockHeightSpeed() throws DataException, SQLException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
// Mint some blocks
|
||||||
|
System.out.println("Minting test blocks - should take approx. 30 seconds...");
|
||||||
|
for (int i = 0; i < 30000; ++i)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
final HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
|
||||||
|
|
||||||
|
// Too slow:
|
||||||
|
testSql(hsqldb, "SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks", false);
|
||||||
|
|
||||||
|
// Fast but if there are no rows, then no result is returned, which causes some triggers to fail:
|
||||||
|
testSql(hsqldb, "SELECT IFNULL(height, 0) + 1 FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1)", true);
|
||||||
|
|
||||||
|
// Too slow:
|
||||||
|
testSql(hsqldb, "SELECT COUNT(*) + 1 FROM Blocks", false);
|
||||||
|
|
||||||
|
// 2-stage, using cached value:
|
||||||
|
hsqldb.prepareStatement("DROP TABLE IF EXISTS TestNextBlockHeight").execute();
|
||||||
|
hsqldb.prepareStatement("CREATE TABLE TestNextBlockHeight (height INT NOT NULL)").execute();
|
||||||
|
hsqldb.prepareStatement("INSERT INTO TestNextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)").execute();
|
||||||
|
|
||||||
|
// 1: Check fetching cached next block height is fast:
|
||||||
|
testSql(hsqldb, "SELECT height from TestNextBlockHeight", true);
|
||||||
|
|
||||||
|
// 2: Check updating NextBlockHeight (typically called via trigger) is fast:
|
||||||
|
testSql(hsqldb, "UPDATE TestNextBlockHeight SET height = (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1)", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSql(HSQLDBRepository hsqldb, String sql, boolean isFast) throws DataException, SQLException {
|
||||||
|
// Execute query to prime caches
|
||||||
|
hsqldb.prepareStatement(sql).execute();
|
||||||
|
|
||||||
|
// Execute again for a slightly more accurate timing
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
|
hsqldb.prepareStatement(sql).execute();
|
||||||
|
|
||||||
|
final long executionTime = System.currentTimeMillis() - start;
|
||||||
|
System.out.println(String.format("%s: [%d ms] SQL: %s", (isFast ? "fast": "slow"), executionTime, sql));
|
||||||
|
|
||||||
|
final long threshold = 3; // ms
|
||||||
|
assertTrue( isFast ? executionTime < threshold : executionTime > threshold);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -117,16 +117,16 @@ public class SerializationTests extends Common {
|
|||||||
public void benchmarkBitSetCompression() {
|
public void benchmarkBitSetCompression() {
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
|
||||||
System.out.println(String.format("Known Online UncompressedBitSet UncompressedIntList Compressed"));
|
System.out.println(String.format("Known Online UncompressedBitSet UncompressedIntList Compressed"));
|
||||||
|
|
||||||
for (int run = 0; run < 1000; ++run) {
|
for (int run = 0; run < 100; ++run) {
|
||||||
final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1;
|
final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1;
|
||||||
|
|
||||||
// 5% to 25%
|
// 3% to 23%
|
||||||
final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 20) + numberOfKnownAccounts / 5);
|
final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 20) + numberOfKnownAccounts / 3);
|
||||||
|
|
||||||
// Enough uncompressed bytes to fit one bit per known account
|
// Enough uncompressed bytes to fit one bit per known account
|
||||||
final int uncompressedBitSetSize = ((numberOfKnownAccounts - 1) >> 3) + 1;
|
final int uncompressedBitSetSize = ((numberOfKnownAccounts - 1) >> 3) + 1; // the >> 3 is to scale size from 8 bits to 1 byte
|
||||||
|
|
||||||
// Size of a simple list of ints
|
// Size of a simple list of ints
|
||||||
final int uncompressedIntListSize = numberOfAccountsToEncode * 4;
|
final int uncompressedIntListSize = numberOfAccountsToEncode * 4;
|
||||||
@ -138,7 +138,7 @@ public class SerializationTests extends Common {
|
|||||||
|
|
||||||
int compressedSize = compressedSet.toByteBuffer().remaining();
|
int compressedSize = compressedSet.toByteBuffer().remaining();
|
||||||
|
|
||||||
System.out.println(String.format("%d %d %d %d %d", numberOfKnownAccounts, numberOfAccountsToEncode, uncompressedBitSetSize, uncompressedIntListSize, compressedSize));
|
System.out.println(String.format("%6d %6d %18d %19d %10d", numberOfKnownAccounts, numberOfAccountsToEncode, uncompressedBitSetSize, uncompressedIntListSize, compressedSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ import org.qora.utils.Base58;
|
|||||||
|
|
||||||
public class RewardShareTests extends Common {
|
public class RewardShareTests extends Common {
|
||||||
|
|
||||||
|
private static final BigDecimal CANCEL_SHARE_PERCENT = BigDecimal.ONE.negate();
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void beforeTest() throws DataException {
|
public void beforeTest() throws DataException {
|
||||||
Common.useDefaultSettings();
|
Common.useDefaultSettings();
|
||||||
@ -60,7 +62,7 @@ public class RewardShareTests extends Common {
|
|||||||
assertEqualBigDecimals("Incorrect share percentage", sharePercent, rewardShareData.getSharePercent());
|
assertEqualBigDecimals("Incorrect share percentage", sharePercent, rewardShareData.getSharePercent());
|
||||||
|
|
||||||
// Delete reward-share
|
// Delete reward-share
|
||||||
byte[] newRewardSharePrivateKey = AccountUtils.rewardShare(repository, "alice", "bob", BigDecimal.ZERO);
|
byte[] newRewardSharePrivateKey = AccountUtils.rewardShare(repository, "alice", "bob", CANCEL_SHARE_PERCENT);
|
||||||
PrivateKeyAccount newRewardShareAccount = new PrivateKeyAccount(repository, newRewardSharePrivateKey);
|
PrivateKeyAccount newRewardShareAccount = new PrivateKeyAccount(repository, newRewardSharePrivateKey);
|
||||||
|
|
||||||
// Confirm reward-share keys match
|
// Confirm reward-share keys match
|
||||||
@ -112,10 +114,10 @@ public class RewardShareTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testZeroInitialShareInvalid() throws DataException {
|
public void testNegativeInitialShareInvalid() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Create invalid REWARD_SHARE transaction with initial 0% reward share
|
// Create invalid REWARD_SHARE transaction with initial negative reward share
|
||||||
TransactionData transactionData = AccountUtils.createRewardShare(repository, "alice", "bob", BigDecimal.ZERO);
|
TransactionData transactionData = AccountUtils.createRewardShare(repository, "alice", "bob", CANCEL_SHARE_PERCENT);
|
||||||
|
|
||||||
// Confirm transaction is invalid
|
// Confirm transaction is invalid
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
@ -162,8 +164,8 @@ public class RewardShareTests extends Common {
|
|||||||
validationResult = newTransaction.isValidUnconfirmed();
|
validationResult = newTransaction.isValidUnconfirmed();
|
||||||
assertNotSame("Subsequent zero-fee self-share should be invalid", ValidationResult.OK, validationResult);
|
assertNotSame("Subsequent zero-fee self-share should be invalid", ValidationResult.OK, validationResult);
|
||||||
|
|
||||||
// Subsequent terminating (0% share) self-reward-share should be OK
|
// Subsequent terminating (negative share) self-reward-share should be OK
|
||||||
newTransactionData = AccountUtils.createRewardShare(repository, testAccountName, testAccountName, BigDecimal.ZERO);
|
newTransactionData = AccountUtils.createRewardShare(repository, testAccountName, testAccountName, CANCEL_SHARE_PERCENT);
|
||||||
newTransaction = Transaction.fromData(repository, newTransactionData);
|
newTransaction = Transaction.fromData(repository, newTransactionData);
|
||||||
|
|
||||||
// Confirm terminating reward-share is valid
|
// Confirm terminating reward-share is valid
|
||||||
|
Loading…
Reference in New Issue
Block a user