forked from Qortal/qortal
Merge branch 'master' into sync-multiple-blocks
# Conflicts: # pom.xml # src/main/java/org/qortal/controller/Synchronizer.java Removed all fast sync code from Controller.syncToPeerChain(), so it is now the same as `master`.
This commit is contained in:
commit
3b3dc5032b
File diff suppressed because it is too large
Load Diff
2
pom.xml
2
pom.xml
@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.5.0</version>
|
||||
<version>1.5.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
|
@ -547,7 +547,7 @@ public class AdminResource {
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.exportNodeLocalData();
|
||||
repository.exportNodeLocalData(true);
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
|
@ -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<String, Long> 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<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin() == accountLevelShareBin).collect(Collectors.toList());
|
||||
List<ExpandedAccount> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<PrivateKeyAccount> 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()));
|
||||
}
|
||||
|
||||
|
@ -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<ByteArray> 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.
|
||||
* <p>
|
||||
@ -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<Peer> qualifiedPeers) {
|
||||
List<Peer> 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(() -> {
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* Will return <tt>SynchronizationResult.OK</tt> on success.
|
||||
* <p>
|
||||
* @param peers
|
||||
* @return SynchronizationResult.OK if the process completed successfully, or a different SynchronizationResult if something went wrong.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public SynchronizationResult findCommonBlocksWithPeers(List<Peer> 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.
|
||||
* <p>
|
||||
* Will return <tt>SynchronizationResult.OK</tt> on success.
|
||||
* <p>
|
||||
* @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<BlockSummaryData> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* @param peers
|
||||
* @return a list of peers, possibly filtered.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public List<Peer> comparePeers(List<Peer> 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<BlockSummaryData> 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<Peer> 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<BlockSummaryData> 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 <BlockSummaryData> 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<BlockSummaryData> 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<Peer> 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<BlockSummaryData> 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<BlockSummaryData> uniqueCommonBlocks(List<Peer> peers) {
|
||||
List<BlockSummaryData> 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<Peer> 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.
|
||||
* <p>
|
||||
@ -116,9 +492,12 @@ public class Synchronizer {
|
||||
|
||||
List<BlockSummaryData> 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<BlockSummaryData> peerBlockSummaries) throws DataException, InterruptedException {
|
||||
Peer peer, final int peerHeight, List<BlockSummaryData> 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<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
|
||||
|
||||
// Keep a list of blocks received so far
|
||||
List<Block> 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<Block> 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<Block> 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<byte[]> 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<byte[]> 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
|
||||
|
@ -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 {
|
||||
|
@ -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<String> logMessageSupplier) throws DataException {
|
||||
|
@ -42,35 +42,32 @@ public class Bitcoin extends Bitcoiny {
|
||||
public Collection<ElectrumX.Server> 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<ElectrumX.Server> 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),
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
56
src/main/java/org/qortal/data/block/CommonBlockData.java
Normal file
56
src/main/java/org/qortal/data/block/CommonBlockData.java
Normal file
@ -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<BlockSummaryData> 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<BlockSummaryData> getBlockSummariesAfterCommonBlock() {
|
||||
return this.blockSummariesAfterCommonBlock;
|
||||
}
|
||||
|
||||
public void setBlockSummariesAfterCommonBlock(List<BlockSummaryData> blockSummariesAfterCommonBlock) {
|
||||
this.blockSummariesAfterCommonBlock = blockSummariesAfterCommonBlock;
|
||||
}
|
||||
|
||||
public BigInteger getChainWeight() {
|
||||
return this.chainWeight;
|
||||
}
|
||||
|
||||
public void setChainWeight(BigInteger chainWeight) {
|
||||
this.chainWeight = chainWeight;
|
||||
}
|
||||
|
||||
public PeerChainTipData getChainTipData() {
|
||||
return this.chainTipData;
|
||||
}
|
||||
|
||||
}
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<Object[]> batchedObjects) throws SQLException {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -49,7 +49,9 @@
|
||||
},
|
||||
"featureTriggers": {
|
||||
"atFindNextTransactionFix": 275000,
|
||||
"newBlockSigHeight": 320000
|
||||
"newBlockSigHeight": 320000,
|
||||
"shareBinFix": 399000,
|
||||
"calcChainWeightTimestamp": 1620579600000
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -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<BlockSummaryData> shorterChain = genBlockSummaries(repository, 3, commonBlockSummary);
|
||||
List<BlockSummaryData> longerChain = genBlockSummaries(repository, shorterChain.size() + 1, commonBlockSummary);
|
||||
|
||||
populateBlockSummariesMinterLevels(repository, shorterChain);
|
||||
List<BlockSummaryData> longerChain = genBlockSummaries(repository, 6, commonBlockSummary);
|
||||
populateBlockSummariesMinterLevels(repository, longerChain);
|
||||
|
||||
List<BlockSummaryData> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<PrivateKeyAccount> 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<String, Map<Long, Long>> 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<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
List<PrivateKeyAccount> 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<String, Map<Long, Long>> 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<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
List<PrivateKeyAccount> 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<String, Map<Long, Long>> 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<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
List<PrivateKeyAccount> 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<String, Map<Long, Long>> 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<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
List<PrivateKeyAccount> 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<String, Map<Long, Long>> 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();
|
||||
}
|
||||
|
||||
}
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
75
src/test/resources/test-chain-v2-reward-levels.json
Normal file
75
src/test/resources/test-chain-v2-reward-levels.json
Normal file
@ -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 }
|
||||
]
|
||||
}
|
||||
}
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@ -46,7 +46,9 @@
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"calcChainWeightTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
7
src/test/resources/test-settings-v2-reward-levels.json
Normal file
7
src/test/resources/test-settings-v2-reward-levels.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-reward-levels.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
@ -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`;
|
||||
|
Loading…
x
Reference in New Issue
Block a user