From 1bca152d9ce7a05da9d46734b80e1071e638540a Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 4 May 2020 14:32:42 +0100 Subject: [PATCH 1/5] Reduce minBlockchainPeers for now --- src/main/java/org/qortal/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 83d46d8b..70762296 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -81,7 +81,7 @@ public class Settings { /** Port number for inbound peer-to-peer connections. */ private Integer listenPort; /** Minimum number of peers to allow block minting / synchronization. */ - private int minBlockchainPeers = 8; + private int minBlockchainPeers = 5; /** Target number of outbound connections to peers we should make. */ private int minOutboundPeers = 16; /** Maximum number of peer connections we allow. */ From 44e8b3e6e7bf4e7aadd9b174ba084645bde9c7fe Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 4 May 2020 14:33:10 +0100 Subject: [PATCH 2/5] Log when BlockMinter fails to acquire blockchain lock after waiting --- src/main/java/org/qortal/block/BlockMinter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/block/BlockMinter.java b/src/main/java/org/qortal/block/BlockMinter.java index 3adbef3d..48bcb967 100644 --- a/src/main/java/org/qortal/block/BlockMinter.java +++ b/src/main/java/org/qortal/block/BlockMinter.java @@ -177,8 +177,10 @@ public class BlockMinter extends Thread { // Make sure we're the only thread modifying the blockchain ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); - if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) + if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) { + LOGGER.warn("Couldn't acquire blockchain lock even after waiting 30 seconds"); continue; + } boolean newBlockMinted = false; From e5cf76f3e0fe6321f9db740e2cf68496b3a942b2 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 4 May 2020 08:18:15 +0100 Subject: [PATCH 3/5] Replace throwing IllegalStateException with more defensive log & null in Block/BlockMinter --- src/main/java/org/qortal/block/Block.java | 18 ++++++++++++------ .../java/org/qortal/block/BlockMinter.java | 8 ++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 4614c81c..4ad7894f 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -304,8 +304,10 @@ public class Block { // Fetch our list of online accounts List onlineAccounts = Controller.getInstance().getOnlineAccounts(); - if (onlineAccounts.isEmpty()) - throw new IllegalStateException("No online accounts - not even our own?"); + if (onlineAccounts.isEmpty()) { + LOGGER.error("No online accounts - not even our own?"); + return null; + } // Find newest online accounts timestamp long onlineAccountsTimestamp = 0; @@ -350,8 +352,10 @@ public class Block { // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey()); - if (minterLevel == 0) - throw new IllegalStateException("Minter effective level returned zero?"); + if (minterLevel == 0) { + LOGGER.error("Minter effective level returned zero?"); + return null; + } long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel); @@ -419,8 +423,10 @@ public class Block { // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey()); - if (minterLevel == 0) - throw new IllegalStateException("Minter effective level returned zero?"); + if (minterLevel == 0){ + LOGGER.error("Minter effective level returned zero?"); + return null; + } long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel); diff --git a/src/main/java/org/qortal/block/BlockMinter.java b/src/main/java/org/qortal/block/BlockMinter.java index 48bcb967..c5e64728 100644 --- a/src/main/java/org/qortal/block/BlockMinter.java +++ b/src/main/java/org/qortal/block/BlockMinter.java @@ -163,10 +163,18 @@ public class BlockMinter extends Thread { // First block does the AT heavy-lifting if (newBlocks.isEmpty()) { Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount); + if (newBlock == null) + // For some reason we can't mint right now + continue; + newBlocks.add(newBlock); } else { // The blocks for other minters require less effort... Block newBlock = newBlocks.get(0); + if (newBlock == null) + // For some reason we can't mint right now + continue; + newBlocks.add(newBlock.remint(mintingAccount)); } } From 0cc9cd728e6205505fcf0a97c41a0b35e271c45f Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 5 May 2020 11:09:46 +0100 Subject: [PATCH 4/5] Fix for chain-stall relating to freshly cancelled reward-shares. In some cases, a freshly cancelled reward-share could still have an associated signed timestamp. Block.mint() failed to spot this and used an incorrect "online account" index when building the to-be-minted block. Block.mint() now checks that AccountRepository.getRewardShareIndex() doesn't return null, i.e. indicating that the associated reward-share for that "online account" no longer exists. In turn, AccountRepository.getRewardShareIndex() didn't fulfill its contract of returning null when the passed public key wasn't present in the repository. So this method has been corrected also. AccountRepository.rewardShareExists(byte[] publicKey) : boolean added. BlockMinter had another bug where it didn't check the return from Block.remint() for null properly. This has been fixed. BlockMinter now has additional logging, with cool-off to prevent log spam, for situations where minting could not happen. Unit test (DisagreementTests) added to cover cancelled reward-share case above. BlockMinter testing support slightly modified to help. --- src/main/java/org/qortal/block/Block.java | 6 +- .../java/org/qortal/block/BlockMinter.java | 49 +++++-- .../qortal/repository/AccountRepository.java | 2 + .../hsqldb/HSQLDBAccountRepository.java | 19 ++- .../test/minting/DisagreementTests.java | 125 ++++++++++++++++++ 5 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 src/test/java/org/qortal/test/minting/DisagreementTests.java diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 4ad7894f..f0835d2d 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -323,7 +323,11 @@ public class Block { if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp) continue; - int accountIndex = repository.getAccountRepository().getRewardShareIndex(onlineAccountData.getPublicKey()); + Integer accountIndex = repository.getAccountRepository().getRewardShareIndex(onlineAccountData.getPublicKey()); + if (accountIndex == null) + // Online account (reward-share) with current timestamp but reward-share cancelled + continue; + indexedOnlineAccounts.put(accountIndex, onlineAccountData); } List accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet()); diff --git a/src/main/java/org/qortal/block/BlockMinter.java b/src/main/java/org/qortal/block/BlockMinter.java index c5e64728..a9246d27 100644 --- a/src/main/java/org/qortal/block/BlockMinter.java +++ b/src/main/java/org/qortal/block/BlockMinter.java @@ -40,6 +40,8 @@ public class BlockMinter extends Thread { // Other properties private static final Logger LOGGER = LogManager.getLogger(BlockMinter.class); + private static Long lastLogTimestamp; + private static Long logTimeout; // Constructors @@ -151,6 +153,9 @@ public class BlockMinter extends Thread { if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) { previousBlock = new Block(repository, lastBlockData); newBlocks.clear(); + + // Reduce log timeout + logTimeout = 10 * 1000L; } // Discard accounts we have already built blocks with @@ -163,19 +168,23 @@ public class BlockMinter extends Thread { // First block does the AT heavy-lifting if (newBlocks.isEmpty()) { Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount); - if (newBlock == null) + if (newBlock == null) { // For some reason we can't mint right now + moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block")); continue; + } newBlocks.add(newBlock); } else { // The blocks for other minters require less effort... - Block newBlock = newBlocks.get(0); - if (newBlock == null) + Block newBlock = newBlocks.get(0).remint(mintingAccount); + if (newBlock == null) { // For some reason we can't mint right now + moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block")); continue; + } - newBlocks.add(newBlock.remint(mintingAccount)); + newBlocks.add(newBlock); } } @@ -193,9 +202,14 @@ public class BlockMinter extends Thread { boolean newBlockMinted = false; try { - // Clear repository's "in transaction" state so we don't cause a repository deadlock + // Clear repository session state so we have latest view of data repository.discardChanges(); + // Now that we have blockchain lock, do final check that chain hasn't changed + BlockData latestBlockData = blockRepository.getLastBlock(); + if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature())) + continue; + List goodBlocks = new ArrayList<>(); for (Block testBlock : newBlocks) { // Is new block's timestamp valid yet? @@ -204,8 +218,12 @@ public class BlockMinter extends Thread { continue; // Is new block valid yet? (Before adding unconfirmed transactions) - if (testBlock.isValid() != ValidationResult.OK) + ValidationResult result = testBlock.isValid(); + if (result != ValidationResult.OK) { + moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name()))); + continue; + } goodBlocks.add(testBlock); } @@ -356,10 +374,14 @@ public class BlockMinter extends Thread { // Ensure mintingAccount is 'online' so blocks can be minted Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts); - BlockData previousBlockData = repository.getBlockRepository().getLastBlock(); - PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0]; + mintTestingBlockRetainingTimestamps(repository, mintingAccount); + } + + public static void mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException { + BlockData previousBlockData = repository.getBlockRepository().getLastBlock(); + Block newBlock = Block.mint(repository, previousBlockData, mintingAccount); // Make sure we're the only thread modifying the blockchain @@ -387,4 +409,15 @@ public class BlockMinter extends Thread { } } + private static void moderatedLog(Runnable logFunction) { + // We only log if logging at TRACE or previous log timeout has expired + if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis()) + return; + + lastLogTimestamp = System.currentTimeMillis(); + logTimeout = 2 * 60 * 1000L; // initial timeout, can be reduced if new block appears + + logFunction.run(); + } + } diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index 15aba42a..805b5ece 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -139,6 +139,8 @@ public interface AccountRepository { */ public RewardShareData getRewardShareByIndex(int index) throws DataException; + public boolean rewardShareExists(byte[] rewardSharePublicKey) throws DataException; + public void save(RewardShareData rewardShareData) throws DataException; /** Delete reward-share from repository using passed minting account's public key and recipient's address. */ diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index c38fa438..6bcaeb3b 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -689,13 +689,13 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public Integer getRewardShareIndex(byte[] publicKey) throws DataException { + public Integer getRewardShareIndex(byte[] rewardSharePublicKey) throws DataException { + if (!this.rewardShareExists(rewardSharePublicKey)) + return null; + String sql = "SELECT COUNT(*) FROM RewardShares WHERE reward_share_public_key < ?"; - try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) { - if (resultSet == null) - return null; - + try (ResultSet resultSet = this.repository.checkedExecute(sql, rewardSharePublicKey)) { return resultSet.getInt(1); } catch (SQLException e) { throw new DataException("Unable to determine reward-share index in repository", e); @@ -724,6 +724,15 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public boolean rewardShareExists(byte[] rewardSharePublicKey) throws DataException { + try { + return this.repository.exists("RewardShares", "reward_share_public_key = ?", rewardSharePublicKey); + } catch (SQLException e) { + throw new DataException("Unable to check reward-share exists in repository", e); + } + } + @Override public void save(RewardShareData rewardShareData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("RewardShares"); diff --git a/src/test/java/org/qortal/test/minting/DisagreementTests.java b/src/test/java/org/qortal/test/minting/DisagreementTests.java new file mode 100644 index 00000000..e71eb9f6 --- /dev/null +++ b/src/test/java/org/qortal/test/minting/DisagreementTests.java @@ -0,0 +1,125 @@ +package org.qortal.test.minting; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.block.BlockMinter; +import org.qortal.controller.Controller; +import org.qortal.data.account.RewardShareData; +import org.qortal.data.block.BlockData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.AccountUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TestAccount; +import org.qortal.test.common.TransactionUtils; +import org.qortal.transform.block.BlockTransformer; +import org.roaringbitmap.IntIterator; + +import io.druid.extendedset.intset.ConciseSet; + +public class DisagreementTests extends Common { + + private static final BigDecimal CANCEL_SHARE_PERCENT = BigDecimal.ONE.negate(); + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + /** + * Testing minting a block when there is a signed online account timestamp present + * that no longer has a corresponding reward-share in DB. + *

+ * Something like: + *

    + *
  • Mint block, with tx to create reward-share R
  • + *
  • Sign current timestamp with R
  • + *
  • Mint block including R as online account
  • + *
  • Mint block, with tx to cancel reward-share R
  • + *
  • Mint another block: R's timestamp should be excluded
  • + *
+ * + * @throws DataException + */ + @Test + public void testOnlineAccounts() throws DataException { + final BigDecimal sharePercent = new BigDecimal("12.8"); + + try (final Repository repository = RepositoryManager.getRepository()) { + TestAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share"); + TestAccount signingAccount = Common.getTestAccount(repository, "alice"); + + // Create reward-share + byte[] testRewardSharePrivateKey = AccountUtils.rewardShare(repository, "alice", "bob", sharePercent); + PrivateKeyAccount testRewardShareAccount = new PrivateKeyAccount(repository, testRewardSharePrivateKey); + + // Confirm reward-share info set correctly + RewardShareData testRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey()); + assertNotNull(testRewardShareData); + + // Create signed timestamps + Controller.getInstance().ensureTestingAccountsOnline(mintingAccount, testRewardShareAccount); + + // Mint another block + BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount); + + // Confirm reward-share's signed timestamp is included + BlockData blockData = repository.getBlockRepository().getLastBlock(); + List rewardSharesData = fetchRewardSharesForBlock(repository, blockData); + boolean doesContainRewardShare = rewardSharesData.stream().anyMatch(rewardShareData -> Arrays.equals(rewardShareData.getRewardSharePublicKey(), testRewardShareData.getRewardSharePublicKey())); + assertTrue(doesContainRewardShare); + + // Cancel reward-share + TransactionData cancelRewardShareTransactionData = AccountUtils.createRewardShare(repository, "alice", "bob", CANCEL_SHARE_PERCENT); + TransactionUtils.signAsUnconfirmed(repository, cancelRewardShareTransactionData, signingAccount); + BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount); + + // Confirm reward-share no longer exists in repository + RewardShareData cancelledRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey()); + assertNull("Reward-share shouldn't exist", cancelledRewardShareData); + + // Attempt to mint with cancelled reward-share + BlockMinter.mintTestingBlockRetainingTimestamps(repository, mintingAccount); + + // Confirm reward-share's signed timestamp is NOT included + blockData = repository.getBlockRepository().getLastBlock(); + rewardSharesData = fetchRewardSharesForBlock(repository, blockData); + doesContainRewardShare = rewardSharesData.stream().anyMatch(rewardShareData -> Arrays.equals(rewardShareData.getRewardSharePublicKey(), testRewardShareData.getRewardSharePublicKey())); + assertFalse(doesContainRewardShare); + } + } + + private List fetchRewardSharesForBlock(Repository repository, BlockData blockData) throws DataException { + byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts(); + ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(encodedOnlineAccounts); + + List rewardSharesData = new ArrayList<>(); + + IntIterator iterator = accountIndexes.iterator(); + while (iterator.hasNext()) { + int accountIndex = iterator.next(); + + RewardShareData rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex); + rewardSharesData.add(rewardShareData); + } + + return rewardSharesData; + } + +} From d2559f36ce459afdc1be8972cfcd3e5e6b57c408 Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 5 May 2020 16:10:54 +0100 Subject: [PATCH 5/5] Fix for Block not correctly adjusting accounts' blocksMinted values. Added BlocksMintedCountTests to cover above. --- src/main/java/org/qortal/block/Block.java | 18 ++-- .../test/minting/BlocksMintedCountTests.java | 96 +++++++++++++++++++ 2 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/qortal/test/minting/BlocksMintedCountTests.java diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index f0835d2d..d3de162b 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -139,7 +138,6 @@ public class Block { private final Account recipientAccount; private final AccountData recipientAccountData; - private final boolean isRecipientFounder; ExpandedAccount(Repository repository, int accountIndex) throws DataException { this.repository = repository; @@ -155,12 +153,10 @@ public class Block { // Self-share: minter is also recipient this.recipientAccount = this.mintingAccount; this.recipientAccountData = this.mintingAccountData; - this.isRecipientFounder = this.isMinterFounder; } else { // Recipient differs from minter this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient()); this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress()); - this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags()); } } @@ -1273,14 +1269,13 @@ public class Block { protected void increaseAccountLevels() throws DataException { // We need to do this for both minters and recipients - this.increaseAccountLevels(expandedAccount -> expandedAccount.isMinterFounder, expandedAccount -> expandedAccount.mintingAccountData); - this.increaseAccountLevels(expandedAccount -> expandedAccount.isRecipientFounder, expandedAccount -> expandedAccount.recipientAccountData); + this.increaseAccountLevels(false, expandedAccount -> expandedAccount.mintingAccountData); + this.increaseAccountLevels(true, expandedAccount -> expandedAccount.recipientAccountData); } - private void increaseAccountLevels(Predicate isFounder, Function getAccountData) throws DataException { + private void increaseAccountLevels(boolean isProcessingRecipients, Function getAccountData) throws DataException { final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); final List expandedAccounts = this.getExpandedAccounts(); - final boolean isProcessingRecipients = getAccountData.apply(expandedAccounts.get(0)) == expandedAccounts.get(0).recipientAccountData; // Increase blocks-minted count for all accounts for (int a = 0; a < expandedAccounts.size(); ++a) { @@ -1591,14 +1586,13 @@ public class Block { protected void decreaseAccountLevels() throws DataException { // We need to do this for both minters and recipients - this.decreaseAccountLevels(expandedAccount -> expandedAccount.isMinterFounder, expandedAccount -> expandedAccount.mintingAccountData); - this.decreaseAccountLevels(expandedAccount -> expandedAccount.isRecipientFounder, expandedAccount -> expandedAccount.recipientAccountData); + this.decreaseAccountLevels(false, expandedAccount -> expandedAccount.mintingAccountData); + this.decreaseAccountLevels(true, expandedAccount -> expandedAccount.recipientAccountData); } - private void decreaseAccountLevels(Predicate isFounder, Function getAccountData) throws DataException { + private void decreaseAccountLevels(boolean isProcessingRecipients, Function getAccountData) throws DataException { final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); final List expandedAccounts = this.getExpandedAccounts(); - final boolean isProcessingRecipients = getAccountData.apply(expandedAccounts.get(0)) == expandedAccounts.get(0).recipientAccountData; // Decrease blocks minted count for all accounts for (int a = 0; a < expandedAccounts.size(); ++a) { diff --git a/src/test/java/org/qortal/test/minting/BlocksMintedCountTests.java b/src/test/java/org/qortal/test/minting/BlocksMintedCountTests.java new file mode 100644 index 00000000..d4d9e81e --- /dev/null +++ b/src/test/java/org/qortal/test/minting/BlocksMintedCountTests.java @@ -0,0 +1,96 @@ +package org.qortal.test.minting; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.block.BlockMinter; +import org.qortal.data.account.RewardShareData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.AccountUtils; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TestAccount; + +public class BlocksMintedCountTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + @Test + public void testNonSelfShare() throws DataException { + final BigDecimal sharePercent = new BigDecimal("12.8"); + + try (final Repository repository = RepositoryManager.getRepository()) { + // Create reward-share + byte[] testRewardSharePrivateKey = AccountUtils.rewardShare(repository, "alice", "bob", sharePercent); + PrivateKeyAccount testRewardShareAccount = new PrivateKeyAccount(repository, testRewardSharePrivateKey); + + // Confirm reward-share info set correctly + RewardShareData testRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey()); + assertNotNull(testRewardShareData); + + testRewardShare(repository, testRewardShareAccount, +1, +1); + } + } + + @Test + public void testSelfShare() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount testRewardShareAccount = Common.getTestAccount(repository, "alice-reward-share"); + + // Confirm reward-share exists + RewardShareData testRewardShareData = repository.getAccountRepository().getRewardShare(testRewardShareAccount.getPublicKey()); + assertNotNull(testRewardShareData); + + testRewardShare(repository, testRewardShareAccount, +1, 0); + } + } + + private void testRewardShare(Repository repository, PrivateKeyAccount mintingAccount, int aliceDelta, int bobDelta) throws DataException { + // Fetch pre-mint blocks minted counts + int alicePreMintCount = getBlocksMinted(repository, "alice"); + int bobPreMintCount = getBlocksMinted(repository, "bob"); + + // Mint another block + BlockMinter.mintTestingBlock(repository, mintingAccount); + + // Fetch post-mint blocks minted counts + int alicePostMintCount = getBlocksMinted(repository, "alice"); + int bobPostMintCount = getBlocksMinted(repository, "bob"); + + // Check both accounts + assertEquals("Alice's post-mint blocks-minted count incorrect", alicePreMintCount + aliceDelta, alicePostMintCount); + assertEquals("Bob's post-mint blocks-minted count incorrect", bobPreMintCount + bobDelta, bobPostMintCount); + + // Orphan latest block + BlockUtils.orphanLastBlock(repository); + + // Fetch post-orphan blocks minted counts + int alicePostOrphanCount = getBlocksMinted(repository, "alice"); + int bobPostOrphanCount = getBlocksMinted(repository, "bob"); + + // Check blocks minted counts reverted correctly + assertEquals("Alice's post-orphan blocks-minted count incorrect", alicePreMintCount, alicePostOrphanCount); + assertEquals("Bob's post-orphan blocks-minted count incorrect", bobPreMintCount, bobPostOrphanCount); + } + + private int getBlocksMinted(Repository repository, String name) throws DataException { + TestAccount testAccount = Common.getTestAccount(repository, name); + return repository.getAccountRepository().getAccount(testAccount.getAddress()).getBlocksMinted(); + } + +}