diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip index e19f3664..443e483f 100755 --- a/WindowsInstaller/Qortal.aip +++ b/WindowsInstaller/Qortal.aip @@ -1,10 +1,8 @@ - - - - - + + + @@ -19,10 +17,10 @@ - + - + @@ -35,8 +33,8 @@ - - + + @@ -48,10 +46,12 @@ - + + + @@ -76,226 +76,296 @@ + + + + + + + + + + + + + + + + + + + - + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + @@ -305,335 +375,506 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -648,7 +889,7 @@ - + @@ -659,9 +900,9 @@ - - - + + + @@ -680,10 +921,14 @@ + + + + @@ -882,13 +1127,17 @@ + + + - + + - + @@ -904,9 +1153,8 @@ - - + @@ -949,8 +1197,8 @@ - + @@ -982,6 +1230,7 @@ + @@ -990,12 +1239,14 @@ + - + + @@ -1005,21 +1256,43 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -1032,10 +1305,21 @@ + - + + + + + + + + + + + @@ -1051,74 +1335,88 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1142,7 +1440,7 @@ - + @@ -1154,9 +1452,6 @@ - - - @@ -1175,6 +1470,8 @@ + + @@ -1197,13 +1494,13 @@ + - - + @@ -1229,7 +1526,7 @@ - + @@ -1241,8 +1538,8 @@ - - + + @@ -1266,7 +1563,7 @@ - - + + diff --git a/pom.xml b/pom.xml index 6697cc81..43e9eae1 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 1.5.0 + 1.5.1 jar true diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 8069a0d5..c295b90b 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -547,7 +547,7 @@ public class AdminResource { blockchainLock.lockInterruptibly(); try { - repository.exportNodeLocalData(); + repository.exportNodeLocalData(true); return "true"; } finally { blockchainLock.unlock(); diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index e2560ac2..384ca193 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -176,19 +176,26 @@ public class Block { * * @return account-level share "bin" from blockchain config, or null if founder / none found */ - public AccountLevelShareBin getShareBin() { + public AccountLevelShareBin getShareBin(int blockHeight) { if (this.isMinterFounder) return null; final int accountLevel = this.mintingAccountData.getLevel(); if (accountLevel <= 0) - return null; + return null; // level 0 isn't included in any share bins - final AccountLevelShareBin[] shareBinsByLevel = BlockChain.getInstance().getShareBinsByAccountLevel(); + final BlockChain blockChain = BlockChain.getInstance(); + final AccountLevelShareBin[] shareBinsByLevel = blockChain.getShareBinsByAccountLevel(); if (accountLevel > shareBinsByLevel.length) return null; - return shareBinsByLevel[accountLevel]; + if (blockHeight < blockChain.getShareBinFixHeight()) + // Off-by-one bug still in effect + return shareBinsByLevel[accountLevel]; + + // level 1 stored at index 0, level 2 stored at index 1, etc. + return shareBinsByLevel[accountLevel-1]; + } public long distribute(long accountAmount, Map balanceChanges) { @@ -801,7 +808,9 @@ public class Block { NumberFormat formatter = new DecimalFormat("0.###E0"); boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE); + int blockCount = 0; for (BlockSummaryData blockSummaryData : blockSummaries) { + blockCount++; StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null; if (isLogging) @@ -830,11 +839,11 @@ public class Block { parentHeight = blockSummaryData.getHeight(); parentBlockSignature = blockSummaryData.getSignature(); - /* Potential future consensus change: only comparing the same number of blocks. - if (parentHeight >= maxHeight) + // After this timestamp, we only compare the same number of blocks + if (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp() && parentHeight >= maxHeight) break; - */ } + LOGGER.debug(String.format("Chain weight calculation was based on %d blocks", blockCount)); return cumulativeWeight; } @@ -1340,6 +1349,9 @@ public class Block { // Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts); + + // Log some debugging info relating to the block weight calculation + this.logDebugInfo(); } protected void increaseAccountLevels() throws DataException { @@ -1521,6 +1533,9 @@ public class Block { public void orphan() throws DataException { LOGGER.trace(() -> String.format("Orphaning block %d", this.blockData.getHeight())); + // Log some debugging info relating to the block weight calculation + this.logDebugInfo(); + // Return AT fees and delete AT states from repository orphanAtFeesAndStates(); @@ -1795,7 +1810,7 @@ public class Block { // 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 binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == accountLevelShareBin).collect(Collectors.toList()); + List binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList()); // No online accounts in this bin? Skip to next one if (binnedAccounts.isEmpty()) @@ -1993,4 +2008,33 @@ public class Block { this.repository.getAccountRepository().tidy(); } + private void logDebugInfo() { + try { + if (this.repository == null || this.getMinter() == null || this.getBlockData() == null) + return; + + int minterLevel = Account.getRewardShareEffectiveMintingLevel(this.repository, this.getMinter().getPublicKey()); + + LOGGER.debug(String.format("======= BLOCK %d (%.8s) =======", this.getBlockData().getHeight(), Base58.encode(this.getSignature()))); + LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp())); + LOGGER.debug(String.format("Minter level: %d", minterLevel)); + LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount())); + + BlockSummaryData blockSummaryData = new BlockSummaryData(this.getBlockData()); + if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null) + return; + + blockSummaryData.setMinterLevel(minterLevel); + BigInteger blockWeight = calcBlockWeight(this.getParent().getHeight(), this.getParent().getSignature(), blockSummaryData); + BigInteger keyDistance = calcKeyDistance(this.getParent().getHeight(), this.getParent().getSignature(), blockSummaryData.getMinterPublicKey(), blockSummaryData.getMinterLevel()); + NumberFormat formatter = new DecimalFormat("0.###E0"); + + LOGGER.debug(String.format("Key distance: %s", formatter.format(keyDistance))); + LOGGER.debug(String.format("Weight: %s", formatter.format(blockWeight))); + + } catch (DataException e) { + LOGGER.info(() -> String.format("Unable to log block debugging info: %s", e.getMessage())); + } + } + } diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index b3221619..e6b8db4e 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -71,7 +71,9 @@ public class BlockChain { public enum FeatureTrigger { atFindNextTransactionFix, - newBlockSigHeight; + newBlockSigHeight, + shareBinFix, + calcChainWeightTimestamp; } /** Map of which blockchain features are enabled when (height/timestamp) */ @@ -381,6 +383,14 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.newBlockSigHeight.name()).intValue(); } + public int getShareBinFixHeight() { + return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue(); + } + + public long getCalcChainWeightTimestamp() { + return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue(); + } + // More complex getters for aspects that change by height or timestamp public long getRewardAtHeight(int ourHeight) { diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index f804456f..8b6563f2 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -135,16 +135,19 @@ public class BlockMinter extends Thread { // Disregard peers that have "misbehaved" recently peers.removeIf(Controller.hasMisbehaved); - // Disregard peers that don't have a recent block - peers.removeIf(Controller.hasNoRecentBlock); + // Disregard peers that don't have a recent block, but only if we're not in recovery mode. + // In that mode, we want to allow minting on top of older blocks, to recover stalled networks. + if (Controller.getInstance().getRecoveryMode() == false) + peers.removeIf(Controller.hasNoRecentBlock); // Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from? if (peers.size() < Settings.getInstance().getMinBlockchainPeers()) continue; - // If our latest block isn't recent then we need to synchronize instead of minting. + // If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode. if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp) - continue; + if (Controller.getInstance().getRecoveryMode() == false) + continue; // There are enough peers with a recent block and our latest block is recent // so go ahead and mint a block if possible. @@ -165,6 +168,14 @@ public class BlockMinter extends Thread { // Do we need to build any potential new blocks? List newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList()); + // We might need to sit the next block out, if one of our minting accounts signed the previous one + final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey(); + final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter)); + if (mintedLastBlock) { + LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one")); + continue; + } + for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) { // First block does the AT heavy-lifting if (newBlocks.isEmpty()) { @@ -282,15 +293,17 @@ public class BlockMinter extends Thread { RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey()); if (rewardShareData != null) { - LOGGER.info(String.format("Minted block %d, sig %.8s by %s on behalf of %s", + LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature()), + Base58.encode(newBlock.getParent().getSignature()), rewardShareData.getMinter(), rewardShareData.getRecipient())); } else { - LOGGER.info(String.format("Minted block %d, sig %.8s by %s", + LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature()), + Base58.encode(newBlock.getParent().getSignature()), newBlock.getMinter().getAddress())); } diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index d988f24d..4fcabe55 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -123,6 +123,7 @@ public class Controller extends Thread { private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000L; // ms private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms + private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms // To do with online accounts list private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms @@ -177,6 +178,11 @@ public class Controller extends Thread { /** Latest block signatures from other peers that we know are on inferior chains. */ List inferiorChainSignatures = new ArrayList<>(); + /** Recovery mode, which is used to bring back a stalled network */ + private boolean recoveryMode = false; + private boolean peersAvailable = true; // peersAvailable must default to true + private long timePeersLastAvailable = 0; + /** * Map of recent requests for ARBITRARY transaction data payloads. *

@@ -372,6 +378,10 @@ public class Controller extends Thread { } } + public boolean getRecoveryMode() { + return this.recoveryMode; + } + // Entry point public static void main(String[] args) { @@ -643,6 +653,13 @@ public class Controller extends Thread { // Disregard peers that don't have a recent block peers.removeIf(hasNoRecentBlock); + checkRecoveryModeForPeers(peers); + if (recoveryMode) { + peers = Network.getInstance().getHandshakedPeers(); + peers.removeIf(hasOnlyGenesisBlock); + peers.removeIf(hasMisbehaved); + } + // Check we have enough peers to potentially synchronize if (peers.size() < Settings.getInstance().getMinBlockchainPeers()) return; @@ -653,9 +670,31 @@ public class Controller extends Thread { // Disregard peers that are on the same block as last sync attempt and we didn't like their chain peers.removeIf(hasInferiorChainTip); + final int peersBeforeComparison = peers.size(); + + // Request recent block summaries from the remaining peers, and locate our common block with each + Synchronizer.getInstance().findCommonBlocksWithPeers(peers); + + // Compare the peers against each other, and against our chain, which will return an updated list excluding those without common blocks + peers = Synchronizer.getInstance().comparePeers(peers); + + // We may have added more inferior chain tips when comparing peers, so remove any peers that are currently on those chains + peers.removeIf(hasInferiorChainTip); + + final int peersRemoved = peersBeforeComparison - peers.size(); + if (peersRemoved > 0) + LOGGER.info(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size())); + if (peers.isEmpty()) return; + if (peers.size() > 1) { + StringBuilder finalPeersString = new StringBuilder(); + for (Peer peer : peers) + finalPeersString = finalPeersString.length() > 0 ? finalPeersString.append(", ").append(peer) : finalPeersString.append(peer); + LOGGER.info(String.format("Choosing random peer from: [%s]", finalPeersString.toString())); + } + // Pick random peer to sync with int index = new SecureRandom().nextInt(peers.size()); Peer peer = peers.get(index); @@ -758,6 +797,46 @@ public class Controller extends Thread { } } + private boolean checkRecoveryModeForPeers(List qualifiedPeers) { + List handshakedPeers = Network.getInstance().getHandshakedPeers(); + + if (handshakedPeers.size() > 0) { + // There is at least one handshaked peer + if (qualifiedPeers.isEmpty()) { + // There are no 'qualified' peers - i.e. peers that have a recent block we can sync to + boolean werePeersAvailable = peersAvailable; + peersAvailable = false; + + // If peers only just became unavailable, update our record of the time they were last available + if (werePeersAvailable) + timePeersLastAvailable = NTP.getTime(); + + // If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint + if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) { + if (recoveryMode == false) { + LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000)); + recoveryMode = true; + } + } + } else { + // We now have at least one peer with a recent block, so we can exit recovery mode and sync normally + peersAvailable = true; + if (recoveryMode) { + LOGGER.info("Peers have become available again. Exiting recovery mode..."); + recoveryMode = false; + } + } + } + return recoveryMode; + } + + public void addInferiorChainSignature(byte[] inferiorSignature) { + // Update our list of inferior chain tips + ByteArray inferiorChainSignature = new ByteArray(inferiorSignature); + if (!inferiorChainSignatures.contains(inferiorChainSignature)) + inferiorChainSignatures.add(inferiorChainSignature); + } + public static class StatusChangeEvent implements Event { public StatusChangeEvent() { } @@ -789,7 +868,7 @@ public class Controller extends Thread { actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED"); } - String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height); + String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("Build version: %s", this.buildVersion); SysTray.getInstance().setToolTipText(tooltip); this.callbackExecutor.execute(() -> { diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index c72d18da..4f3833b3 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import java.util.Iterator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -15,8 +16,10 @@ import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.block.Block; import org.qortal.block.Block.ValidationResult; +import org.qortal.block.BlockChain; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; +import org.qortal.data.block.CommonBlockData; import org.qortal.data.network.PeerChainTipData; import org.qortal.data.transaction.RewardShareTransactionData; import org.qortal.data.transaction.TransactionData; @@ -37,6 +40,7 @@ import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.transaction.Transaction; import org.qortal.utils.Base58; +import org.qortal.utils.NTP; public class Synchronizer { @@ -56,9 +60,6 @@ public class Synchronizer { /** Maximum number of block signatures we ask from peer in one go */ private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings? - /** Number of retry attempts if a peer fails to respond with the requested data */ - private static final int MAXIMUM_RETRIES = 1; // XXX move to Settings? - /* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */ private static final long PEER_VERSION_150 = 0x0100050000L; @@ -81,6 +82,381 @@ public class Synchronizer { return instance; } + + /** + * Iterate through a list of supplied peers, and attempt to find our common block with each. + * If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later. + *

+ * Will return SynchronizationResult.OK on success. + *

+ * @param peers + * @return SynchronizationResult.OK if the process completed successfully, or a different SynchronizationResult if something went wrong. + * @throws InterruptedException + */ + public SynchronizationResult findCommonBlocksWithPeers(List peers) throws InterruptedException { + try (final Repository repository = RepositoryManager.getRepository()) { + try { + + if (peers.size() == 0) + return SynchronizationResult.NOTHING_TO_DO; + + // If our latest block is very old, it's best that we don't try and determine the best peers to sync to. + // This is because it can involve very large chain comparisons, which is too intensive. + // In reality, most forking problems occur near the chain tips, so we will reserve this functionality for those situations. + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (minLatestBlockTimestamp == null) + return SynchronizationResult.REPOSITORY_ISSUE; + + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) { + LOGGER.debug(String.format("Our latest block is very old, so we won't collect common block info from peers")); + return SynchronizationResult.NOTHING_TO_DO; + } + + LOGGER.debug(String.format("Searching for common blocks with %d peers...", peers.size())); + final long startTime = System.currentTimeMillis(); + int commonBlocksFound = 0; + + for (Peer peer : peers) { + // Are we shutting down? + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + + // Check if we can use the cached common block data, by comparing the peer's current chain tip against the peer's chain tip when we last found our common block + if (peer.canUseCachedCommonBlockData()) { + LOGGER.debug(String.format("Skipping peer %s because we already have the latest common block data in our cache. Cached common block sig is %.08s", peer, Base58.encode(peer.getCommonBlockData().getCommonBlockSummary().getSignature()))); + commonBlocksFound++; + continue; + } + + // Cached data is stale, so clear it and repopulate + peer.setCommonBlockData(null); + + // Search for the common block + Synchronizer.getInstance().findCommonBlockWithPeer(peer, repository); + if (peer.getCommonBlockData() != null) + commonBlocksFound++; + } + + final long totalTimeTaken = System.currentTimeMillis() - startTime; + LOGGER.info(String.format("Finished searching for common blocks with %d peer%s. Found: %d. Total time taken: %d ms", peers.size(), (peers.size() != 1 ? "s" : ""), commonBlocksFound, totalTimeTaken)); + + return SynchronizationResult.OK; + } finally { + repository.discardChanges(); // Free repository locks, if any, also in case anything went wrong + } + } catch (DataException e) { + LOGGER.error("Repository issue during synchronization with peer", e); + return SynchronizationResult.REPOSITORY_ISSUE; + } + } + + /** + * Attempt to find the find our common block with supplied peer. + * If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later. + *

+ * Will return SynchronizationResult.OK on success. + *

+ * @param peer + * @param repository + * @return SynchronizationResult.OK if the process completed successfully, or a different SynchronizationResult if something went wrong. + * @throws InterruptedException + */ + public SynchronizationResult findCommonBlockWithPeer(Peer peer, Repository repository) throws InterruptedException { + try { + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + final int ourInitialHeight = ourLatestBlockData.getHeight(); + + PeerChainTipData peerChainTipData = peer.getChainTipData(); + int peerHeight = peerChainTipData.getLastHeight(); + byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature(); + + byte[] ourLastBlockSignature = ourLatestBlockData.getSignature(); + LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer, + peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(), + ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp())); + + List peerBlockSummaries = new ArrayList<>(); + SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, false, peerBlockSummaries); + if (findCommonBlockResult != SynchronizationResult.OK) { + // Logging performed by fetchSummariesFromCommonBlock() above + peer.setCommonBlockData(null); + return findCommonBlockResult; + } + + // First summary is common block + final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature()); + final BlockSummaryData commonBlockSummary = new BlockSummaryData(commonBlockData); + final int commonBlockHeight = commonBlockData.getHeight(); + final byte[] commonBlockSig = commonBlockData.getSignature(); + final String commonBlockSig58 = Base58.encode(commonBlockSig); + LOGGER.debug(String.format("Common block with peer %s is at height %d, sig %.8s, ts %d", peer, + commonBlockHeight, commonBlockSig58, commonBlockData.getTimestamp())); + peerBlockSummaries.remove(0); + + // Store the common block summary against the peer, and the current chain tip (for caching) + peer.setCommonBlockData(new CommonBlockData(commonBlockSummary, peerChainTipData)); + + return SynchronizationResult.OK; + } catch (DataException e) { + LOGGER.error("Repository issue during synchronization with peer", e); + return SynchronizationResult.REPOSITORY_ISSUE; + } + } + + + /** + * Compare a list of peers to determine the best peer(s) to sync to next. + *

+ * Will return a filtered list of peers on success, or an identical list of peers on failure. + * This allows us to fall back to legacy behaviour (random selection from the entire list of peers), if we are unable to make the comparison. + *

+ * @param peers + * @return a list of peers, possibly filtered. + * @throws InterruptedException + */ + public List comparePeers(List peers) throws InterruptedException { + try (final Repository repository = RepositoryManager.getRepository()) { + try { + + // If our latest block is very old, it's best that we don't try and determine the best peers to sync to. + // This is because it can involve very large chain comparisons, which is too intensive. + // In reality, most forking problems occur near the chain tips, so we will reserve this functionality for those situations. + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (minLatestBlockTimestamp == null) + return peers; + + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) { + LOGGER.debug(String.format("Our latest block is very old, so we won't filter the peers list")); + return peers; + } + + // We will switch to a new chain weight consensus algorithm at a hard fork, so determine if this has happened yet + boolean usingSameLengthChainWeight = (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp()); + LOGGER.debug(String.format("Using %s chain weight consensus algorithm", (usingSameLengthChainWeight ? "same-length" : "variable-length"))); + + // Retrieve a list of unique common blocks from this list of peers + List commonBlocks = this.uniqueCommonBlocks(peers); + + // Order common blocks by height, in ascending order + // This is essential for the logic below to make the correct decisions when discarding chains - do not remove + commonBlocks.sort((b1, b2) -> Integer.valueOf(b1.getHeight()).compareTo(Integer.valueOf(b2.getHeight()))); + + // Get our latest height + final int ourHeight = ourLatestBlockData.getHeight(); + + // Create a placeholder to track of common blocks that we can discard due to being inferior chains + int dropPeersAfterCommonBlockHeight = 0; + + // Remove peers with no common block data + Iterator iterator = peers.iterator(); + while (iterator.hasNext()) { + Peer peer = (Peer) iterator.next(); + if (peer.getCommonBlockData() == null) { + LOGGER.debug(String.format("Removed peer %s because it has no common block data", peer)); + iterator.remove(); + } + } + + // Loop through each group of common blocks + for (BlockSummaryData commonBlockSummary : commonBlocks) { + List peersSharingCommonBlock = peers.stream().filter(peer -> peer.getCommonBlockData().getCommonBlockSummary().equals(commonBlockSummary)).collect(Collectors.toList()); + + // Check if we need to discard this group of peers + if (dropPeersAfterCommonBlockHeight > 0) { + if (commonBlockSummary.getHeight() > dropPeersAfterCommonBlockHeight) { + // We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers. + for (Peer peer : peersSharingCommonBlock) { + LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight)); + Controller.getInstance().addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature()); + } + continue; + } + } + + // Calculate the length of the shortest peer chain sharing this common block, including our chain + final int ourAdditionalBlocksAfterCommonBlock = ourHeight - commonBlockSummary.getHeight(); + int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary); + + // Fetch block summaries from each peer + for (Peer peer : peersSharingCommonBlock) { + + // If we're shutting down, just return the latest peer list + if (Controller.isStopping()) + return peers; + + // Count the number of blocks this peer has beyond our common block + final int peerHeight = peer.getChainTipData().getLastHeight(); + final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight(); + // Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed + int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE); + + // Check if we can use the cached common block summaries, by comparing the peer's current chain tip against the peer's chain tip when we last found our common block + boolean useCachedSummaries = false; + if (peer.canUseCachedCommonBlockData()) { + if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) { + if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock().size() == summariesRequired) { + LOGGER.debug(String.format("Using cached block summaries for peer %s", peer)); + useCachedSummaries = true; + } + } + } + + if (useCachedSummaries == false) { + if (summariesRequired > 0) { + LOGGER.trace(String.format("Requesting %d block summar%s from peer %s after common block %.8s. Peer height: %d", summariesRequired, (summariesRequired != 1 ? "ies" : "y"), peer, Base58.encode(commonBlockSummary.getSignature()), peerHeight)); + + List blockSummaries = this.getBlockSummaries(peer, commonBlockSummary.getSignature(), summariesRequired); + peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(blockSummaries); + + if (blockSummaries != null) { + LOGGER.trace(String.format("Peer %s returned %d block summar%s", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y"))); + + if (blockSummaries.size() < summariesRequired) + // This could mean that the peer has re-orged. But we still have the same common block, so it's safe to proceed with this set of signatures instead. + LOGGER.debug(String.format("Peer %s returned %d block summar%s instead of expected %d", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y"), summariesRequired)); + } + } else { + // There are no block summaries after this common block + peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(null); + } + } + + // Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength + List peerBlockSummaries = peer.getCommonBlockData().getBlockSummariesAfterCommonBlock(); + if (peerBlockSummaries != null && peerBlockSummaries.size() > 0) + if (peerBlockSummaries.size() < minChainLength) + minChainLength = peerBlockSummaries.size(); + } + + // Fetch our corresponding block summaries. Limit to MAXIMUM_REQUEST_SIZE, in order to make the comparison fairer, as peers have been limited too + final int ourSummariesRequired = Math.min(ourAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE); + LOGGER.trace(String.format("About to fetch our block summaries from %d to %d. Our height: %d", commonBlockSummary.getHeight() + 1, commonBlockSummary.getHeight() + ourSummariesRequired, ourHeight)); + List ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockSummary.getHeight() + 1, commonBlockSummary.getHeight() + ourSummariesRequired); + if (ourBlockSummaries.isEmpty()) { + LOGGER.debug(String.format("We don't have any block summaries so can't compare our chain against peers with this common block. We can still compare them against each other.")); + } + else { + populateBlockSummariesMinterLevels(repository, ourBlockSummaries); + // Reduce minChainLength if we have less summaries + if (ourBlockSummaries.size() < minChainLength) + minChainLength = ourBlockSummaries.size(); + } + + // Create array to hold peers for comparison + List superiorPeersForComparison = new ArrayList<>(); + + // Calculate max height for chain weight comparisons + int maxHeightForChainWeightComparisons = commonBlockSummary.getHeight() + minChainLength; + + // Calculate our chain weight + BigInteger ourChainWeight = BigInteger.valueOf(0); + if (ourBlockSummaries.size() > 0) + ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, maxHeightForChainWeightComparisons); + + NumberFormat formatter = new DecimalFormat("0.###E0"); + NumberFormat accurateFormatter = new DecimalFormat("0.################E0"); + LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), formatter.format(ourChainWeight))); + + LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature()))); + for (Peer peer : peersSharingCommonBlock) { + final int peerHeight = peer.getChainTipData().getLastHeight(); + final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight(); + final CommonBlockData peerCommonBlockData = peer.getCommonBlockData(); + + if (peerCommonBlockData == null || peerCommonBlockData.getBlockSummariesAfterCommonBlock() == null || peerCommonBlockData.getBlockSummariesAfterCommonBlock().isEmpty()) { + // No response - remove this peer for now + LOGGER.debug(String.format("Peer %s doesn't have any block summaries - removing it from this round", peer)); + peers.remove(peer); + continue; + } + + final List peerBlockSummariesAfterCommonBlock = peerCommonBlockData.getBlockSummariesAfterCommonBlock(); + populateBlockSummariesMinterLevels(repository, peerBlockSummariesAfterCommonBlock); + + // Calculate cumulative chain weight of this blockchain subset, from common block to highest mutual block held by all peers in this group. + LOGGER.debug(String.format("About to calculate chain weight based on %d blocks for peer %s with common block %.8s (peer has %d blocks after common block)", (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peer, Base58.encode(commonBlockSummary.getSignature()), peerAdditionalBlocksAfterCommonBlock)); + BigInteger peerChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), peerBlockSummariesAfterCommonBlock, maxHeightForChainWeightComparisons); + peer.getCommonBlockData().setChainWeight(peerChainWeight); + LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), formatter.format(peerChainWeight))); + + // Compare against our chain - if our blockchain has greater weight then don't synchronize with peer (or any others in this group) + if (ourChainWeight.compareTo(peerChainWeight) > 0) { + // This peer is on an inferior chain - remove it + LOGGER.debug(String.format("Peer %s is on an inferior chain to us - removing it from this round", peer)); + peers.remove(peer); + } + else { + // Our chain is inferior + LOGGER.debug(String.format("Peer %s is on a better chain to us. We will compare the other peers sharing this common block against each other, and drop all peers sharing higher common blocks.", peer)); + dropPeersAfterCommonBlockHeight = commonBlockSummary.getHeight(); + superiorPeersForComparison.add(peer); + } + } + + // Now that we have selected the best peers, compare them against each other and remove any with lower weights + if (superiorPeersForComparison.size() > 0) { + BigInteger bestChainWeight = null; + for (Peer peer : superiorPeersForComparison) { + // Increase bestChainWeight if needed + if (bestChainWeight == null || peer.getCommonBlockData().getChainWeight().compareTo(bestChainWeight) >= 0) + bestChainWeight = peer.getCommonBlockData().getChainWeight(); + } + for (Peer peer : superiorPeersForComparison) { + // Check if we should discard an inferior peer + if (peer.getCommonBlockData().getChainWeight().compareTo(bestChainWeight) < 0) { + BigInteger difference = bestChainWeight.subtract(peer.getCommonBlockData().getChainWeight()); + LOGGER.debug(String.format("Peer %s has a lower chain weight (difference: %s) than other peer(s) in this group - removing it from this round.", peer, accurateFormatter.format(difference))); + peers.remove(peer); + } + } + } + } + + return peers; + } finally { + repository.discardChanges(); // Free repository locks, if any, also in case anything went wrong + } + } catch (DataException e) { + LOGGER.error("Repository issue during peer comparison", e); + return peers; + } + } + + private List uniqueCommonBlocks(List peers) { + List commonBlocks = new ArrayList<>(); + + for (Peer peer : peers) { + if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) { + LOGGER.debug(String.format("Peer %s has common block %.8s", peer, Base58.encode(peer.getCommonBlockData().getCommonBlockSummary().getSignature()))); + + BlockSummaryData commonBlockSummary = peer.getCommonBlockData().getCommonBlockSummary(); + if (!commonBlocks.contains(commonBlockSummary)) + commonBlocks.add(commonBlockSummary); + } + else { + LOGGER.debug(String.format("Peer %s has no common block data. Skipping...", peer)); + } + } + + return commonBlocks; + } + + private int calculateMinChainLengthOfPeers(List peersSharingCommonBlock, BlockSummaryData commonBlockSummary) { + // Calculate the length of the shortest peer chain sharing this common block + int minChainLength = 0; + for (Peer peer : peersSharingCommonBlock) { + final int peerHeight = peer.getChainTipData().getLastHeight(); + final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight(); + + if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0) + minChainLength = peerAdditionalBlocksAfterCommonBlock; + } + return minChainLength; + } + + /** * Attempt to synchronize blockchain with peer. *

@@ -116,9 +492,12 @@ public class Synchronizer { List peerBlockSummaries = new ArrayList<>(); SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries); - if (findCommonBlockResult != SynchronizationResult.OK) + if (findCommonBlockResult != SynchronizationResult.OK) { // Logging performed by fetchSummariesFromCommonBlock() above + // Clear our common block cache for this peer + peer.setCommonBlockData(null); return findCommonBlockResult; + } // First summary is common block final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature()); @@ -263,9 +642,13 @@ public class Synchronizer { // Currently we work forward from common block until we hit a block we don't have // TODO: rewrite as modified binary search! int i; - for (i = 1; i < blockSummariesFromCommon.size(); ++i) + for (i = 1; i < blockSummariesFromCommon.size(); ++i) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + if (!repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) break; + } // Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive blockSummariesFromCommon.subList(0, i - 1).clear(); @@ -314,6 +697,9 @@ public class Synchronizer { // Check peer sent valid heights for (int i = 0; i < moreBlockSummaries.size(); ++i) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + ++lastSummaryHeight; BlockSummaryData blockSummary = moreBlockSummaries.get(i); @@ -335,7 +721,7 @@ public class Synchronizer { populateBlockSummariesMinterLevels(repository, ourBlockSummaries); populateBlockSummariesMinterLevels(repository, peerBlockSummaries); - final int mutualHeight = commonBlockHeight - 1 + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size()); + final int mutualHeight = commonBlockHeight + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size()); // Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block. BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight); @@ -355,181 +741,180 @@ public class Synchronizer { } private SynchronizationResult syncToPeerChain(Repository repository, BlockData commonBlockData, int ourInitialHeight, - Peer peer, final int peerHeight, List peerBlockSummaries) throws DataException, InterruptedException { + Peer peer, final int peerHeight, List peerBlockSummaries) throws DataException, InterruptedException { final int commonBlockHeight = commonBlockData.getHeight(); final byte[] commonBlockSig = commonBlockData.getSignature(); String commonBlockSig58 = Base58.encode(commonBlockSig); + byte[] latestPeerSignature = commonBlockSig; + int height = commonBlockHeight; + LOGGER.debug(() -> String.format("Fetching peer %s chain from height %d, sig %.8s", peer, commonBlockHeight, commonBlockSig58)); - int ourHeight = ourInitialHeight; + final int maxRetries = Settings.getInstance().getMaxRetries(); // Overall plan: fetch peer's blocks first, then orphan, then apply + // Convert any leftover (post-common) block summaries into signatures to request from peer + List peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList()); + + // Keep a list of blocks received so far + List peerBlocks = new ArrayList<>(); // Calculate the total number of additional blocks this peer has beyond the common block int additionalPeerBlocksAfterCommonBlock = peerHeight - commonBlockHeight; + // Subtract the number of signatures that we already have, as we don't need to request them again + int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); + int retryCount = 0; + while (height < peerHeight) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; - // Firstly, attempt to retrieve the blocks themselves, rather than signatures. This is supported by newer peers (version 1.5.0 and above). - List peerBlocks = new ArrayList<>(); - - if (Settings.getInstance().isFastSyncEnabledWhenResolvingFork() && peer.getPeersVersion() >= PEER_VERSION_150) { - // This peer supports syncing multiple blocks at once via GetBlocksMessage, and it is enabled in the settings - int numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); - - // Ensure that we don't request more blocks than specified in the settings - int maxBlocksPerRequest = Settings.getInstance().getMaxBlocksPerRequest(); - - while (numberBlocksRequired > 0) { - if (Controller.isStopping()) - return SynchronizationResult.SHUTTING_DOWN; - - byte[] latestPeerSignature = peerBlocks.isEmpty() ? commonBlockSig : peerBlocks.get(peerBlocks.size() - 1).getSignature(); - int lastPeerHeight = commonBlockHeight + peerBlocks.size(); - int numberOfBlocksToRequest = Math.min(numberBlocksRequired, maxBlocksPerRequest); - - LOGGER.trace(String.format("Requesting %d block%s after height %d, sig %.8s", - numberOfBlocksToRequest, (numberOfBlocksToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); - - List blocks = this.fetchBlocks(repository, peer, latestPeerSignature, numberOfBlocksToRequest); - if (blocks == null || blocks.isEmpty()) { - LOGGER.info(String.format("Peer %s failed to respond with more blocks after height %d, sig %.8s", peer, - lastPeerHeight, Base58.encode(latestPeerSignature))); - - if (peerBlocks.isEmpty()) { - return SynchronizationResult.NO_REPLY; - } - break; - } - - LOGGER.debug(String.format("Received %d blocks from peer %s", blocks.size(), peer)); - - try { - for (Block block : blocks) { - - // Set the repository, because we couldn't do that when originally constructing the Block - block.setRepository(repository); - - // Transactions are transmitted without approval status so determine that now - for (Transaction transaction : block.getTransactions()) { - transaction.setInitialApprovalStatus(); - } - - peerBlocks.add(block); - } - } catch (IllegalStateException e) { - LOGGER.error("Error processing transactions in block", e); - return SynchronizationResult.REPOSITORY_ISSUE; - } - - numberBlocksRequired = additionalPeerBlocksAfterCommonBlock - peerBlocks.size(); - } - } - else { - // Older peer version, or fast sync is disabled in the settings - use slow sync - - // Convert any leftover (post-common) block summaries into signatures to request from peer - List peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList()); - - // Subtract the number of signatures that we already have, as we don't need to request them again - int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); - - - // Fetch remaining block signatures, if needed - int retryCount = 0; - while (numberSignaturesRequired > 0) { - if (Controller.isStopping()) - return SynchronizationResult.SHUTTING_DOWN; - - byte[] latestPeerSignature = peerBlockSignatures.isEmpty() ? commonBlockSig : peerBlockSignatures.get(peerBlockSignatures.size() - 1); - int lastPeerHeight = commonBlockHeight + peerBlockSignatures.size(); - int numberOfSignaturesToRequest = Math.min(numberSignaturesRequired, MAXIMUM_REQUEST_SIZE); + // Ensure we don't request more than MAXIMUM_REQUEST_SIZE + int numberRequested = Math.min(numberSignaturesRequired, MAXIMUM_REQUEST_SIZE); + // Do we need more signatures? + if (peerBlockSignatures.isEmpty() && numberRequested > 0) { LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s", - numberOfSignaturesToRequest, (numberOfSignaturesToRequest != 1 ? "s" : ""), lastPeerHeight, Base58.encode(latestPeerSignature))); + numberRequested, (numberRequested != 1 ? "s" : ""), height, Base58.encode(latestPeerSignature))); - List moreBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberOfSignaturesToRequest); + peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested); - if (moreBlockSignatures == null || moreBlockSignatures.isEmpty()) { + if (peerBlockSignatures == null || peerBlockSignatures.isEmpty()) { LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer, - lastPeerHeight, Base58.encode(latestPeerSignature))); + height, Base58.encode(latestPeerSignature))); - if (retryCount >= MAXIMUM_RETRIES) { - // Give up with this peer - return SynchronizationResult.NO_REPLY; - } else { - // Retry until retryCount reaches MAXIMUM_RETRIES - retryCount++; - int triesRemaining = MAXIMUM_RETRIES - retryCount; - LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : ""))); - continue; + // Clear our cache of common block summaries for this peer, as they are likely to be invalid + CommonBlockData cachedCommonBlockData = peer.getCommonBlockData(); + if (cachedCommonBlockData != null) + cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null); + + // If we have already received recent or newer blocks from this peer, go ahead and apply them + if (peerBlocks.size() > 0) { + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1); + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (ourLatestBlockData != null && peerLatestBlock != null && minLatestBlockTimestamp != null) { + + // If we have received at least one recent block, we can apply them + if (peerLatestBlock.getBlockData().getTimestamp() > minLatestBlockTimestamp) { + LOGGER.debug("Newly received blocks are recent, so we will apply them"); + break; + } + + // If our latest block is very old.... + if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) { + // ... and we have received a block that is more recent than our latest block ... + if (peerLatestBlock.getBlockData().getTimestamp() > ourLatestBlockData.getTimestamp()) { + // ... then apply the blocks, as it takes us a step forward. + // This is particularly useful when starting up a node that was on a small fork when it was last shut down. + // In these cases, we now allow the node to sync forward, and get onto the main chain again. + // Without this, we would require that the node syncs ENTIRELY with this peer, + // and any problems downloading a block would cause all progress to be lost. + LOGGER.debug(String.format("Newly received blocks are %d ms newer than our latest block - so we will apply them", peerLatestBlock.getBlockData().getTimestamp() - ourLatestBlockData.getTimestamp())); + break; + } + } + } } + // Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state + return SynchronizationResult.NO_REPLY; } - // Reset retryCount because the last request succeeded - retryCount = 0; - + numberSignaturesRequired = peerHeight - height - peerBlockSignatures.size(); LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : ""))); - - peerBlockSignatures.addAll(moreBlockSignatures); - numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size(); } - // Fetch blocks using signatures - LOGGER.debug(String.format("Fetching new blocks from peer %s after height %d", peer, commonBlockHeight)); + if (peerBlockSignatures.isEmpty()) { + LOGGER.trace(String.format("No more signatures or blocks to request from peer %s", peer)); + break; + } - retryCount = 0; - while (peerBlocks.size() < peerBlockSignatures.size()) { - if (Controller.isStopping()) - return SynchronizationResult.SHUTTING_DOWN; + byte[] nextPeerSignature = peerBlockSignatures.get(0); + int nextHeight = height + 1; - byte[] blockSignature = peerBlockSignatures.get(peerBlocks.size()); + LOGGER.trace(String.format("Fetching block %d, sig %.8s from %s", nextHeight, Base58.encode(nextPeerSignature), peer)); + Block newBlock = this.fetchBlock(repository, peer, nextPeerSignature); - LOGGER.debug(String.format("Fetching block with signature %.8s", Base58.encode(blockSignature))); - int blockHeightToRequest = commonBlockHeight + peerBlocks.size() + 1; // +1 because we are requesting the next block, beyond what we already have in the peerBlocks array - Block newBlock = this.fetchBlock(repository, peer, blockSignature); + if (newBlock == null) { + LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, + nextHeight, Base58.encode(nextPeerSignature))); - if (newBlock == null) { - LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, blockHeightToRequest, Base58.encode(blockSignature))); + if (retryCount >= maxRetries) { - if (retryCount >= MAXIMUM_RETRIES) { - // Give up with this peer - return SynchronizationResult.NO_REPLY; - } else { - // Retry until retryCount reaches MAXIMUM_RETRIES - retryCount++; - int triesRemaining = MAXIMUM_RETRIES - retryCount; - LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : ""))); - continue; + // If we have already received recent or newer blocks from this peer, go ahead and apply them + if (peerBlocks.size() > 0) { + final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock(); + final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1); + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (ourLatestBlockData != null && peerLatestBlock != null && minLatestBlockTimestamp != null) { + + // If we have received at least one recent block, we can apply them + if (peerLatestBlock.getBlockData().getTimestamp() > minLatestBlockTimestamp) { + LOGGER.debug("Newly received blocks are recent, so we will apply them"); + break; + } + + // If our latest block is very old.... + if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) { + // ... and we have received a block that is more recent than our latest block ... + if (peerLatestBlock.getBlockData().getTimestamp() > ourLatestBlockData.getTimestamp()) { + // ... then apply the blocks, as it takes us a step forward. + // This is particularly useful when starting up a node that was on a small fork when it was last shut down. + // In these cases, we now allow the node to sync forward, and get onto the main chain again. + // Without this, we would require that the node syncs ENTIRELY with this peer, + // and any problems downloading a block would cause all progress to be lost. + LOGGER.debug(String.format("Newly received blocks are %d ms newer than our latest block - so we will apply them", peerLatestBlock.getBlockData().getTimestamp() - ourLatestBlockData.getTimestamp())); + break; + } + } + } } + // Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state + return SynchronizationResult.NO_REPLY; + + } else { + // Re-fetch signatures, in case the peer is now on a different fork + peerBlockSignatures.clear(); + numberSignaturesRequired = peerHeight - height; + + // Retry until retryCount reaches maxRetries + retryCount++; + int triesRemaining = maxRetries - retryCount; + LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : ""))); + continue; } - - if (!newBlock.isSignatureValid()) { - LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer, - blockHeightToRequest, Base58.encode(blockSignature))); - return SynchronizationResult.INVALID_DATA; - } - - // Reset retryCount because the last request succeeded - retryCount = 0; - - LOGGER.debug(String.format("Received block with height %d, sig: %.8s", newBlock.getBlockData().getHeight(), Base58.encode(blockSignature))); - - // Transactions are transmitted without approval status so determine that now - for (Transaction transaction : newBlock.getTransactions()) - transaction.setInitialApprovalStatus(); - - peerBlocks.add(newBlock); } - } + // Reset retryCount because the last request succeeded + retryCount = 0; + LOGGER.trace(String.format("Fetched block %d, sig %.8s from %s", nextHeight, Base58.encode(latestPeerSignature), peer)); + + if (!newBlock.isSignatureValid()) { + LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer, + nextHeight, Base58.encode(latestPeerSignature))); + return SynchronizationResult.INVALID_DATA; + } + + // Transactions are transmitted without approval status so determine that now + for (Transaction transaction : newBlock.getTransactions()) + transaction.setInitialApprovalStatus(); + + peerBlocks.add(newBlock); + + // Now that we've received this block, we can increase our height and move on to the next one + latestPeerSignature = nextPeerSignature; + peerBlockSignatures.remove(0); + ++height; + } // Unwind to common block (unless common block is our latest block) - LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58)); + int ourHeight = ourInitialHeight; + LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s. Our height: %d", commonBlockHeight, commonBlockSig58, ourHeight)); - BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight); + BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourInitialHeight); while (ourHeight > commonBlockHeight) { if (Controller.isStopping()) return SynchronizationResult.SHUTTING_DOWN; @@ -551,6 +936,9 @@ public class Synchronizer { LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer)); for (Block newBlock : peerBlocks) { + if (Controller.isStopping()) + return SynchronizationResult.SHUTTING_DOWN; + ValidationResult blockResult = newBlock.isValid(); if (blockResult != ValidationResult.OK) { LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, @@ -818,6 +1206,9 @@ public class Synchronizer { final int firstBlockHeight = blockSummaries.get(0).getHeight(); for (int i = 0; i < blockSummaries.size(); ++i) { + if (Controller.isStopping()) + return; + BlockSummaryData blockSummary = blockSummaries.get(i); // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java index 15764199..286cbf74 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java @@ -211,6 +211,9 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress)); + // Attempt to backup the trade bot data + TradeBot.backupTradeBotData(repository); + // Return to user for signing and broadcast as we don't have their Qortal private key try { return DeployAtTransactionTransformer.toBytes(deployAtTransactionData); @@ -283,6 +286,9 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash); + // Attempt to backup the trade bot data + TradeBot.backupTradeBotData(repository); + // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount long p2shFee; try { diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index 84e32125..94c7cefb 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.concurrent.locks.ReentrantLock; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -267,6 +268,22 @@ public class TradeBot implements Listener { return secret; } + /*package*/ static void backupTradeBotData(Repository repository) { + // Attempt to backup the trade bot data. This an optional step and doesn't impact trading, so don't throw an exception on failure + try { + LOGGER.info("About to backup trade bot data..."); + ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); + blockchainLock.lockInterruptibly(); + try { + repository.exportNodeLocalData(true); + } finally { + blockchainLock.unlock(); + } + } catch (InterruptedException | DataException e) { + LOGGER.info(String.format("Failed to obtain blockchain lock when exporting trade bot data: %s", e.getMessage())); + } + } + /** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */ /*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData, String newState, int newStateValue, Supplier logMessageSupplier) throws DataException { diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java index a8c6469a..28275d6a 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoin.java +++ b/src/main/java/org/qortal/crosschain/Bitcoin.java @@ -42,35 +42,32 @@ public class Bitcoin extends Bitcoiny { public Collection getServers() { return Arrays.asList( // Servers chosen on NO BASIS WHATSOEVER from various sources! - new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002), - new Server("electrumx.ml", Server.ConnectionType.SSL, 50002), - new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512), - new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), - new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002), - new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001), - new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001), - new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001), - new Server("xtrum.com", Server.ConnectionType.TCP, 50001), - new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001), - new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001), - new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001), - new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001), + new Server("128.0.190.26", Server.ConnectionType.SSL, 50002), + new Server("hodlers.beer", Server.ConnectionType.SSL, 50002), + new Server("electrumx.erbium.eu", Server.ConnectionType.TCP, 50001), + new Server("electrumx.erbium.eu", Server.ConnectionType.SSL, 50002), + new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002), + new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002), + new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002), + new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022), + new Server("185.64.116.15", Server.ConnectionType.SSL, 50002), + new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002), + new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002), + new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002), new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001), - new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081), - new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001), - new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003), - new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001), - new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001), - new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001), - new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001), - new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001), - new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001), - new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001), - new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)); + new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002), + new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002), + new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002), + new Server("xtrum.com", Server.ConnectionType.SSL, 50002), + new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002), + new Server("electrum2.taborsky.cz", Server.ConnectionType.SSL, 50002), + new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002), + new Server("electrum2.privateservers.network", Server.ConnectionType.SSL, 50002), + new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002), + new Server("192.166.219.200", Server.ConnectionType.SSL, 50002), + new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002), + new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002), + new Server("caleb.vegas", Server.ConnectionType.SSL, 50002)); } @Override @@ -96,10 +93,8 @@ public class Bitcoin extends Bitcoiny { @Override public Collection getServers() { return Arrays.asList( - new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001), - new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002), + new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002), new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002), - new Server("testnet.qtornado.com", Server.ConnectionType.TCP, 51001), new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002), new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001), new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002), diff --git a/src/main/java/org/qortal/data/block/BlockSummaryData.java b/src/main/java/org/qortal/data/block/BlockSummaryData.java index a0c39f75..2167f0f0 100644 --- a/src/main/java/org/qortal/data/block/BlockSummaryData.java +++ b/src/main/java/org/qortal/data/block/BlockSummaryData.java @@ -2,6 +2,7 @@ package org.qortal.data.block; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Arrays; @XmlAccessorType(XmlAccessType.FIELD) public class BlockSummaryData { @@ -84,4 +85,21 @@ public class BlockSummaryData { this.minterLevel = minterLevel; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + + if (o == null || getClass() != o.getClass()) + return false; + + BlockSummaryData otherBlockSummary = (BlockSummaryData) o; + if (this.getSignature() == null || otherBlockSummary.getSignature() == null) + return false; + + // Treat two block summaries as equal if they have matching signatures + return Arrays.equals(this.getSignature(), otherBlockSummary.getSignature()); + } + } diff --git a/src/main/java/org/qortal/data/block/CommonBlockData.java b/src/main/java/org/qortal/data/block/CommonBlockData.java new file mode 100644 index 00000000..dd502df7 --- /dev/null +++ b/src/main/java/org/qortal/data/block/CommonBlockData.java @@ -0,0 +1,56 @@ +package org.qortal.data.block; + +import org.qortal.data.network.PeerChainTipData; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.math.BigInteger; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CommonBlockData { + + // Properties + private BlockSummaryData commonBlockSummary = null; + private List blockSummariesAfterCommonBlock = null; + private BigInteger chainWeight = null; + private PeerChainTipData chainTipData = null; + + // Constructors + + protected CommonBlockData() { + } + + public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) { + this.commonBlockSummary = commonBlockSummary; + this.chainTipData = chainTipData; + } + + + // Getters / setters + + public BlockSummaryData getCommonBlockSummary() { + return this.commonBlockSummary; + } + + public List getBlockSummariesAfterCommonBlock() { + return this.blockSummariesAfterCommonBlock; + } + + public void setBlockSummariesAfterCommonBlock(List blockSummariesAfterCommonBlock) { + this.blockSummariesAfterCommonBlock = blockSummariesAfterCommonBlock; + } + + public BigInteger getChainWeight() { + return this.chainWeight; + } + + public void setChainWeight(BigInteger chainWeight) { + this.chainWeight = chainWeight; + } + + public PeerChainTipData getChainTipData() { + return this.chainTipData; + } + +} diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index 9212fb56..cc4ad918 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Random; +import java.util.Arrays; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -22,6 +23,7 @@ import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.qortal.data.block.CommonBlockData; import org.qortal.data.network.PeerChainTipData; import org.qortal.data.network.PeerData; import org.qortal.network.message.ChallengeMessage; @@ -106,6 +108,9 @@ public class Peer { /** Latest block info as reported by peer. */ private PeerChainTipData peersChainTipData; + /** Our common block with this peer */ + private CommonBlockData commonBlockData; + // Constructors /** Construct unconnected, outbound Peer using socket address in peer data */ @@ -272,6 +277,18 @@ public class Peer { } } + public CommonBlockData getCommonBlockData() { + synchronized (this.peerInfoLock) { + return this.commonBlockData; + } + } + + public void setCommonBlockData(CommonBlockData commonBlockData) { + synchronized (this.peerInfoLock) { + this.commonBlockData = commonBlockData; + } + } + /*package*/ void queueMessage(Message message) { if (!this.pendingMessages.offer(message)) LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this)); @@ -616,6 +633,25 @@ public class Peer { } } + + // Common block data + + public boolean canUseCachedCommonBlockData() { + PeerChainTipData peerChainTipData = this.getChainTipData(); + CommonBlockData commonBlockData = this.getCommonBlockData(); + + if (peerChainTipData != null && commonBlockData != null) { + PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData(); + if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null && commonBlockChainTipData.getLastBlockSignature() != null) { + if (Arrays.equals(peerChainTipData.getLastBlockSignature(), commonBlockChainTipData.getLastBlockSignature())) { + return true; + } + } + } + return false; + } + + // Utility methods /** Returns true if ports and addresses (or hostnames) match */ diff --git a/src/main/java/org/qortal/repository/Repository.java b/src/main/java/org/qortal/repository/Repository.java index 656e6e1e..5438f1d9 100644 --- a/src/main/java/org/qortal/repository/Repository.java +++ b/src/main/java/org/qortal/repository/Repository.java @@ -49,7 +49,7 @@ public interface Repository extends AutoCloseable { public void performPeriodicMaintenance() throws DataException; - public void exportNodeLocalData() throws DataException; + public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException; public void importDataFromFile(String filename) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index 7c514d73..5557c13e 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -52,6 +52,7 @@ import org.qortal.repository.TransactionRepository; import org.qortal.repository.VotingRepository; import org.qortal.repository.hsqldb.transaction.HSQLDBTransactionRepository; import org.qortal.settings.Settings; +import org.qortal.utils.NTP; public class HSQLDBRepository implements Repository { @@ -459,10 +460,44 @@ public class HSQLDBRepository implements Repository { } @Override - public void exportNodeLocalData() throws DataException { + public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException { + + // Create the qortal-backup folder if it doesn't exist + Path backupPath = Paths.get("qortal-backup"); + try { + Files.createDirectories(backupPath); + } catch (IOException e) { + LOGGER.info("Unable to create backup folder"); + throw new DataException("Unable to create backup folder"); + } + + // We need to rename or delete an existing TradeBotStates backup before creating a new one + File tradeBotStatesBackupFile = new File("qortal-backup/TradeBotStates.script"); + if (tradeBotStatesBackupFile.exists()) { + if (keepArchivedCopy) { + // Rename existing TradeBotStates backup, to make sure that we're not overwriting any keys + File archivedBackupFile = new File(String.format("qortal-backup/TradeBotStates-archive-%d.script", NTP.getTime())); + if (tradeBotStatesBackupFile.renameTo(archivedBackupFile)) + LOGGER.info(String.format("Moved existing TradeBotStates backup file to %s", archivedBackupFile.getPath())); + else + throw new DataException("Unable to rename existing TradeBotStates backup"); + } else { + // Delete existing copy + LOGGER.info("Deleting existing TradeBotStates backup because it is being replaced with a new one"); + tradeBotStatesBackupFile.delete(); + } + } + + // There's currently no need to take an archived copy of the MintingAccounts data - just delete the old one if it exists + File mintingAccountsBackupFile = new File("qortal-backup/MintingAccounts.script"); + if (mintingAccountsBackupFile.exists()) { + LOGGER.info("Deleting existing MintingAccounts backup because it is being replaced with a new one"); + mintingAccountsBackupFile.delete(); + } + try (Statement stmt = this.connection.createStatement()) { - stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'"); - stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'"); + stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'qortal-backup/MintingAccounts.script'"); + stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'qortal-backup/TradeBotStates.script'"); LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states"); } catch (SQLException e) { throw new DataException("Unable to export sensitive/node-local data from repository"); @@ -475,12 +510,12 @@ public class HSQLDBRepository implements Repository { LOGGER.info(() -> String.format("Importing data into repository from %s", filename)); String escapedFilename = stmt.enquoteLiteral(filename); - stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " STOP ON ERROR"); + stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " CONTINUE ON ERROR"); LOGGER.info(() -> String.format("Imported data into repository from %s", filename)); } catch (SQLException e) { LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage())); - throw new DataException("Unable to export sensitive/node-local data from repository: " + e.getMessage()); + throw new DataException("Unable to import sensitive/node-local data to repository: " + e.getMessage()); } } @@ -681,7 +716,7 @@ public class HSQLDBRepository implements Repository { /** * Execute PreparedStatement and return changed row count. * - * @param preparedStatement + * @param sql * @param objects * @return number of changed rows * @throws SQLException @@ -693,8 +728,8 @@ public class HSQLDBRepository implements Repository { /** * Execute batched PreparedStatement * - * @param preparedStatement - * @param objects + * @param sql + * @param batchedObjects * @return number of changed rows * @throws SQLException */ @@ -818,7 +853,7 @@ public class HSQLDBRepository implements Repository { * * @param tableName * @param whereClause - * @param objects + * @param batchedObjects * @throws SQLException */ public int deleteBatch(String tableName, String whereClause, List batchedObjects) throws SQLException { diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 3070bf96..8b0eefcc 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -52,7 +52,7 @@ public class Settings { // UI servers private int uiPort = 12388; private String[] uiLocalServers = new String[] { - "localhost", "127.0.0.1", "172.24.1.1", "qor.tal" + "localhost", "127.0.0.1" }; private String[] uiRemoteServers = new String[] { "node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org", @@ -122,6 +122,8 @@ public class Settings { private int maxNetworkThreadPoolSize = 20; /** Maximum number of threads for network proof-of-work compute, used during handshaking. */ private int networkPoWComputePoolSize = 2; + /** Maximum number of retry attempts if a peer fails to respond with the requested data */ + private int maxRetries = 2; /** Whether to sync multiple blocks at once in normal operation */ private boolean fastSyncEnabled = false; @@ -417,6 +419,8 @@ public class Settings { return this.networkPoWComputePoolSize; } + public int getMaxRetries() { return this.maxRetries; } + public String getBlockchainConfig() { return this.blockchainConfig; } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 92d4ad86..d0ac9ffb 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -49,7 +49,9 @@ }, "featureTriggers": { "atFindNextTransactionFix": 275000, - "newBlockSigHeight": 320000 + "newBlockSigHeight": 320000, + "shareBinFix": 399000, + "calcChainWeightTimestamp": 1620579600000 }, "genesisInfo": { "version": 4, diff --git a/src/test/java/org/qortal/test/ChainWeightTests.java b/src/test/java/org/qortal/test/ChainWeightTests.java index b02c155e..e53c4c8e 100644 --- a/src/test/java/org/qortal/test/ChainWeightTests.java +++ b/src/test/java/org/qortal/test/ChainWeightTests.java @@ -3,12 +3,15 @@ package org.qortal.test; import static org.junit.Assert.*; import java.math.BigInteger; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.Random; import org.qortal.account.Account; import org.qortal.block.Block; +import org.qortal.block.BlockChain; import org.qortal.data.block.BlockSummaryData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -17,12 +20,21 @@ import org.qortal.test.common.Common; import org.qortal.test.common.TestAccount; import org.qortal.transform.Transformer; import org.qortal.transform.block.BlockTransformer; +import org.qortal.utils.NTP; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; public class ChainWeightTests extends Common { private static final Random RANDOM = new Random(); + private static final NumberFormat FORMATTER = new DecimalFormat("0.###E0"); + + @BeforeClass + public static void beforeClass() { + // We need this so that NTP.getTime() in Block.calcChainWeight() doesn't return null, causing NPE + NTP.setFixedOffset(0L); + } @Before public void beforeTest() throws DataException { @@ -89,7 +101,97 @@ public class ChainWeightTests extends Common { } } - // Check that a longer chain beats a shorter chain + // Demonstrates that typical key distance ranges from roughly 1E75 to 1E77 + @Test + public void testKeyDistances() { + byte[] parentMinterKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + byte[] testKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + + for (int i = 0; i < 50; ++i) { + int parentHeight = RANDOM.nextInt(50000); + RANDOM.nextBytes(parentMinterKey); + RANDOM.nextBytes(testKey); + int minterLevel = RANDOM.nextInt(10) + 1; + + BigInteger keyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, testKey, minterLevel); + + System.out.println(String.format("Parent height: %d, minter level: %d, distance: %s", + parentHeight, + minterLevel, + FORMATTER.format(keyDistance))); + } + } + + // If typical key distance ranges from 1E75 to 1E77 + // then we want lots of online accounts to push a 1E75 distance + // towards 1E77 so that it competes with a 1E77 key that has hardly any online accounts + // 1E75 is approx. 2**249 so maybe that's a good value for Block.ACCOUNTS_COUNT_SHIFT + @Test + public void testMoreAccountsVersusKeyDistance() throws DataException { + BigInteger minimumBetterKeyDistance = BigInteger.TEN.pow(77); + BigInteger maximumWorseKeyDistance = BigInteger.TEN.pow(75); + + try (final Repository repository = RepositoryManager.getRepository()) { + final byte[] parentMinterKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + + TestAccount betterAccount = Common.getTestAccount(repository, "bob-reward-share"); + byte[] betterKey = betterAccount.getPublicKey(); + int betterMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, betterKey); + + TestAccount worseAccount = Common.getTestAccount(repository, "dilbert-reward-share"); + byte[] worseKey = worseAccount.getPublicKey(); + int worseMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, worseKey); + + // This is to check that the hard-coded keys ARE actually better/worse as expected, before moving on testing more online accounts + BigInteger betterKeyDistance; + BigInteger worseKeyDistance; + + int parentHeight = 0; + do { + ++parentHeight; + betterKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, betterKey, betterMinterLevel); + worseKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, worseKey, worseMinterLevel); + } while (betterKeyDistance.compareTo(minimumBetterKeyDistance) < 0 || worseKeyDistance.compareTo(maximumWorseKeyDistance) > 0); + + System.out.println(String.format("Parent height: %d, better key distance: %s, worse key distance: %s", + parentHeight, + FORMATTER.format(betterKeyDistance), + FORMATTER.format(worseKeyDistance))); + + for (int accountsCountShift = 244; accountsCountShift <= 256; accountsCountShift += 2) { + for (int worseAccountsCount = 1; worseAccountsCount <= 101; worseAccountsCount += 25) { + for (int betterAccountsCount = 1; betterAccountsCount <= 1001; betterAccountsCount += 250) { + BlockSummaryData worseKeyBlockSummary = new BlockSummaryData(parentHeight + 1, null, worseKey, betterAccountsCount); + BlockSummaryData betterKeyBlockSummary = new BlockSummaryData(parentHeight + 1, null, betterKey, worseAccountsCount); + + populateBlockSummaryMinterLevel(repository, worseKeyBlockSummary); + populateBlockSummaryMinterLevel(repository, betterKeyBlockSummary); + + BigInteger worseKeyBlockWeight = calcBlockWeight(parentHeight, parentMinterKey, worseKeyBlockSummary, accountsCountShift); + BigInteger betterKeyBlockWeight = calcBlockWeight(parentHeight, parentMinterKey, betterKeyBlockSummary, accountsCountShift); + + System.out.println(String.format("Shift: %d, worse key: %d accounts, %s diff; better key: %d accounts: %s diff; winner: %s", + accountsCountShift, + betterAccountsCount, // used with worseKey + FORMATTER.format(worseKeyBlockWeight), + worseAccountsCount, // used with betterKey + FORMATTER.format(betterKeyBlockWeight), + worseKeyBlockWeight.compareTo(betterKeyBlockWeight) > 0 ? "worse key/better accounts" : "better key/worse accounts" + )); + } + } + + System.out.println(); + } + } + } + + private static BigInteger calcBlockWeight(int parentHeight, byte[] parentBlockSignature, BlockSummaryData blockSummaryData, int accountsCountShift) { + BigInteger keyDistance = Block.calcKeyDistance(parentHeight, parentBlockSignature, blockSummaryData.getMinterPublicKey(), blockSummaryData.getMinterLevel()); + return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(accountsCountShift).add(keyDistance); + } + + // Check that a longer chain has same weight as shorter/truncated chain @Test public void testLongerChain() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { @@ -97,18 +199,20 @@ public class ChainWeightTests extends Common { BlockSummaryData commonBlockSummary = genBlockSummary(repository, commonBlockHeight); byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey(); - List shorterChain = genBlockSummaries(repository, 3, commonBlockSummary); - List longerChain = genBlockSummaries(repository, shorterChain.size() + 1, commonBlockSummary); - - populateBlockSummariesMinterLevels(repository, shorterChain); + List longerChain = genBlockSummaries(repository, 6, commonBlockSummary); populateBlockSummariesMinterLevels(repository, longerChain); + List shorterChain = longerChain.subList(0, longerChain.size() / 2); + final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size()); BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight); BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, mutualHeight); - assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight)); + if (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp()) + assertEquals("longer chain should have same weight", 0, longerChainWeight.compareTo(shorterChainWeight)); + else + assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight)); } } diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 4d098f67..6c03662c 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -336,4 +336,457 @@ public class RewardTests extends Common { } } + /** Test rewards for level 1 and 2 accounts both pre and post the shareBinFix, including orphaning back through the feature trigger block */ + @Test + public void testLevel1And2Rewards() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share NOT online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint a couple of blocks so that we are able to orphan them later + for (int i=0; i<2; i++) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(1, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(2, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Ensure that only Alice is a founder + assertEquals(1, getFlags(repository, "alice")); + assertEquals(0, getFlags(repository, "bob")); + assertEquals(0, getFlags(repository, "chloe")); + assertEquals(0, getFlags(repository, "dilbert")); + + // Now that everyone is at level 1 or 2, we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are at the correct height and block reward value + assertEquals(6, (int) repository.getBlockRepository().getLastBlock().getHeight()); + assertEquals(10000000000L, blockReward); + + /* + * Alice, Chloe, and Dilbert are 'online'. Bob is offline. + * Chloe is level 1, Dilbert is level 2. + * One founder online (Alice, who is also level 1). + * No legacy QORA holders. + * + * Chloe and Dilbert should receive equal shares of the 5% block reward for Level 1 and 2 + * Alice should receive the remainder (95%) + */ + + // We are after the shareBinFix feature trigger, so we expect level 1 and 2 to share the same reward (5%) + final int level1And2SharePercent = 5_00; // 5% + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long expectedReward = level1And2ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level1And2ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + assertEquals(500000000, level1And2ShareAmount); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedReward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedReward); + + // Now orphan the latest block. This brings us to the threshold of the shareBinFix feature trigger. + BlockUtils.orphanBlocks(repository, 1); + assertEquals(5, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Ensure the latest post-fix block rewards have been subtracted and they have returned to their initial values + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + + // Orphan another block. This time, the block that was orphaned was prior to the shareBinFix feature trigger. + BlockUtils.orphanBlocks(repository, 1); + assertEquals(4, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Prior to the fix, the levels were incorrectly grouped + // Chloe should receive 100% of the level 1 reward, and Dilbert should receive 100% of the level 2+3 reward + final int level1SharePercent = 5_00; // 5% + final int level2And3SharePercent = 10_00; // 10% + final long level1ShareAmountBeforeFix = (blockReward * level1SharePercent) / 100L / 100L; + final long level2And3ShareAmountBeforeFix = (blockReward * level2And3SharePercent) / 100L / 100L; + final long expectedFounderRewardBeforeFix = blockReward - level1ShareAmountBeforeFix - level2And3ShareAmountBeforeFix; // Alice should receive the remainder + + // Validate the share amounts and balances + assertEquals(500000000, level1ShareAmountBeforeFix); + assertEquals(1000000000, level2And3ShareAmountBeforeFix); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-expectedFounderRewardBeforeFix); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-level1ShareAmountBeforeFix); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-level2And3ShareAmountBeforeFix); + + // Orphan the latest block one last time + BlockUtils.orphanBlocks(repository, 1); + assertEquals(3, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Validate balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-(expectedFounderRewardBeforeFix*2)); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-(level1ShareAmountBeforeFix*2)); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-(level2And3ShareAmountBeforeFix*2)); + + } + } + + /** Test rewards for level 3 and 4 accounts */ + @Test + public void testLevel3And4Rewards() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share online + byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0); + PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey); + mintingAndOnlineAccounts.add(bobRewardShareAccount); + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 3 and 4 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(4) - 20; // 20 blocks before level 4, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(3, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(3, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(3, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(4, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 3 or 4, we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Bob, Chloe, and Dilbert are 'online'. + * Bob and Chloe are level 3; Dilbert is level 4. + * One founder online (Alice, who is also level 3). + * No legacy QORA holders. + * + * Chloe, Bob and Dilbert should receive equal shares of the 10% block reward for level 3 and 4 + * Alice should receive the remainder (90%) + */ + + // We are after the shareBinFix feature trigger, so we expect level 3 and 4 to share the same reward (10%) + final int level3And4SharePercent = 10_00; // 10% + final long level3And4ShareAmount = (blockReward * level3And4SharePercent) / 100L / 100L; + final long expectedReward = level3And4ShareAmount / 3; // The reward is split between Bob, Chloe, and Dilbert + final long expectedFounderReward = blockReward - level3And4ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedReward); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedReward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedReward); + + } + } + + /** Test rewards for level 5 and 6 accounts */ + @Test + public void testLevel5And6Rewards() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share not initially online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 5 and 6 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(6) - 20; // 20 blocks before level 6, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Bob self-share now comes online + byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0); + PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey); + mintingAndOnlineAccounts.add(bobRewardShareAccount); + + // Ensure that the levels are as we expect + assertEquals(5, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(5, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(6, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 5 or 6 (except Bob who has only just started minting, so is at level 1), we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Bob, Chloe, and Dilbert are 'online'. + * Bob is level 1; Chloe is level 5; Dilbert is level 6. + * One founder online (Alice, who is also level 5). + * No legacy QORA holders. + * + * Chloe and Dilbert should receive equal shares of the 15% block reward for level 5 and 6 + * Bob should receive all of the level 1 and 2 reward (5%) + * Alice should receive the remainder (80%) + */ + + // We are after the shareBinFix feature trigger, so we expect level 5 and 6 to share the same reward (15%) + final int level1And2SharePercent = 5_00; // 5% + final int level5And6SharePercent = 15_00; // 10% + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long level5And6ShareAmount = (blockReward * level5And6SharePercent) / 100L / 100L; + final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob + final long expectedLevel5And6Reward = level5And6ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level1And2ShareAmount - level5And6ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5And6Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5And6Reward); + + } + } + + /** Test rewards for level 7 and 8 accounts */ + @Test + public void testLevel7And8Rewards() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share NOT online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 7 and 8 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(8) - 20; // 20 blocks before level 8, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(8, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Chloe, and Dilbert are 'online'. + * Chloe is level 7; Dilbert is level 8. + * One founder online (Alice, who is also level 7). + * No legacy QORA holders. + * + * Chloe and Dilbert should receive equal shares of the 20% block reward for level 7 and 8 + * Alice should receive the remainder (80%) + */ + + // We are after the shareBinFix feature trigger, so we expect level 7 and 8 to share the same reward (20%) + final int level7And8SharePercent = 20_00; // 20% + final long level7And8ShareAmount = (blockReward * level7And8SharePercent) / 100L / 100L; + final long expectedLevel7And8Reward = level7And8ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level7And8ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel7And8Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel7And8Reward); + + } + } + + /** Test rewards for level 9 and 10 accounts */ + @Test + public void testLevel9And10Rewards() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + + // Bob self-share not initially online + + // Chloe self share online + byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint enough blocks to bump testAccount levels to 9 and 10 + final int minterBlocksNeeded = cumulativeBlocksByLevel.get(10) - 20; // 20 blocks before level 10, so that the test accounts reach the correct levels + for (int bc = 0; bc < minterBlocksNeeded; ++bc) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Bob self-share now comes online + byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0); + PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey); + mintingAndOnlineAccounts.add(bobRewardShareAccount); + + // Ensure that the levels are as we expect + assertEquals(9, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(9, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(10, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are using the correct block reward value + assertEquals(100000000L, blockReward); + + /* + * Alice, Bob, Chloe, and Dilbert are 'online'. + * Bob is level 1; Chloe is level 9; Dilbert is level 10. + * One founder online (Alice, who is also level 9). + * No legacy QORA holders. + * + * Chloe and Dilbert should receive equal shares of the 25% block reward for level 9 and 10 + * Bob should receive all of the level 1 and 2 reward (5%) + * Alice should receive the remainder (70%) + */ + + // We are after the shareBinFix feature trigger, so we expect level 9 and 10 to share the same reward (25%) + final int level1And2SharePercent = 5_00; // 5% + final int level9And10SharePercent = 25_00; // 25% + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long level9And10ShareAmount = (blockReward * level9And10SharePercent) / 100L / 100L; + final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob + final long expectedLevel9And10Reward = level9And10ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderReward = blockReward - level1And2ShareAmount - level9And10ShareAmount; // Alice should receive the remainder + + // Validate the balances to ensure that the correct post-shareBinFix distribution is being applied + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward); + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel9And10Reward); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel9And10Reward); + + } + } + + + private int getFlags(Repository repository, String name) throws DataException { + TestAccount testAccount = Common.getTestAccount(repository, name); + return repository.getAccountRepository().getAccount(testAccount.getAddress()).getFlags(); + } + } \ No newline at end of file diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 6f3c5fff..2b96da55 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 53e13915..3ff0c8e7 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index a5c841a0..94014868 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 883e36fe..308461c1 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index a06dda7f..99adf1be 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json new file mode 100644 index 00000000..a078119a --- /dev/null +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -0,0 +1,75 @@ +{ + "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, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 6, + "calcChainWeightTimestamp": 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": "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": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 2 } + ] + } +} diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 91d6a36b..e0faeec2 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index dd8377be..e7347246 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -46,7 +46,9 @@ "newAssetPricingTimestamp": 0, "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, - "newBlockSigHeight": 999999 + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "calcChainWeightTimestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-settings-v2-reward-levels.json b/src/test/resources/test-settings-v2-reward-levels.json new file mode 100644 index 00000000..1c6862ad --- /dev/null +++ b/src/test/resources/test-settings-v2-reward-levels.json @@ -0,0 +1,7 @@ +{ + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-reward-levels.json", + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0 +} diff --git a/tools/publish-auto-update.pl b/tools/publish-auto-update.pl index 0c7a47d6..ad43b2f4 100755 --- a/tools/publish-auto-update.pl +++ b/tools/publish-auto-update.pl @@ -58,6 +58,7 @@ $timestamp *= 1000; # Convert to milliseconds # locate sha256 utility my $SHA256 = `which sha256sum || which sha256`; chomp $SHA256; +die("Can't find sha256sum or sha256\n") unless length($SHA256) > 0; # SHA256 of actual update file my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`;