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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>1.5.0</version>
|
<version>1.5.1</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
|
@ -547,7 +547,7 @@ public class AdminResource {
|
|||||||
blockchainLock.lockInterruptibly();
|
blockchainLock.lockInterruptibly();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
repository.exportNodeLocalData();
|
repository.exportNodeLocalData(true);
|
||||||
return "true";
|
return "true";
|
||||||
} finally {
|
} finally {
|
||||||
blockchainLock.unlock();
|
blockchainLock.unlock();
|
||||||
|
@ -176,19 +176,26 @@ public class Block {
|
|||||||
*
|
*
|
||||||
* @return account-level share "bin" from blockchain config, or null if founder / none found
|
* @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)
|
if (this.isMinterFounder)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
final int accountLevel = this.mintingAccountData.getLevel();
|
final int accountLevel = this.mintingAccountData.getLevel();
|
||||||
if (accountLevel <= 0)
|
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)
|
if (accountLevel > shareBinsByLevel.length)
|
||||||
return null;
|
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) {
|
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
|
||||||
@ -801,7 +808,9 @@ public class Block {
|
|||||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||||
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
|
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
|
||||||
|
|
||||||
|
int blockCount = 0;
|
||||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
||||||
|
blockCount++;
|
||||||
StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
|
StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
|
||||||
|
|
||||||
if (isLogging)
|
if (isLogging)
|
||||||
@ -830,11 +839,11 @@ public class Block {
|
|||||||
parentHeight = blockSummaryData.getHeight();
|
parentHeight = blockSummaryData.getHeight();
|
||||||
parentBlockSignature = blockSummaryData.getSignature();
|
parentBlockSignature = blockSummaryData.getSignature();
|
||||||
|
|
||||||
/* Potential future consensus change: only comparing the same number of blocks.
|
// After this timestamp, we only compare the same number of blocks
|
||||||
if (parentHeight >= maxHeight)
|
if (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp() && parentHeight >= maxHeight)
|
||||||
break;
|
break;
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
LOGGER.debug(String.format("Chain weight calculation was based on %d blocks", blockCount));
|
||||||
|
|
||||||
return cumulativeWeight;
|
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
|
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
|
||||||
Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
||||||
|
|
||||||
|
// Log some debugging info relating to the block weight calculation
|
||||||
|
this.logDebugInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void increaseAccountLevels() throws DataException {
|
protected void increaseAccountLevels() throws DataException {
|
||||||
@ -1521,6 +1533,9 @@ public class Block {
|
|||||||
public void orphan() throws DataException {
|
public void orphan() throws DataException {
|
||||||
LOGGER.trace(() -> String.format("Orphaning block %d", this.blockData.getHeight()));
|
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
|
// Return AT fees and delete AT states from repository
|
||||||
orphanAtFeesAndStates();
|
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.
|
// 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);
|
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||||
// Object reference compare is OK as all references are read-only from blockchain config.
|
// 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
|
// No online accounts in this bin? Skip to next one
|
||||||
if (binnedAccounts.isEmpty())
|
if (binnedAccounts.isEmpty())
|
||||||
@ -1993,4 +2008,33 @@ public class Block {
|
|||||||
this.repository.getAccountRepository().tidy();
|
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 {
|
public enum FeatureTrigger {
|
||||||
atFindNextTransactionFix,
|
atFindNextTransactionFix,
|
||||||
newBlockSigHeight;
|
newBlockSigHeight,
|
||||||
|
shareBinFix,
|
||||||
|
calcChainWeightTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
/** 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();
|
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
|
// More complex getters for aspects that change by height or timestamp
|
||||||
|
|
||||||
public long getRewardAtHeight(int ourHeight) {
|
public long getRewardAtHeight(int ourHeight) {
|
||||||
|
@ -135,16 +135,19 @@ public class BlockMinter extends Thread {
|
|||||||
// Disregard peers that have "misbehaved" recently
|
// Disregard peers that have "misbehaved" recently
|
||||||
peers.removeIf(Controller.hasMisbehaved);
|
peers.removeIf(Controller.hasMisbehaved);
|
||||||
|
|
||||||
// Disregard peers that don't have a recent block
|
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||||
peers.removeIf(Controller.hasNoRecentBlock);
|
// 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?
|
// 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())
|
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||||
continue;
|
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)
|
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
|
// There are enough peers with a recent block and our latest block is recent
|
||||||
// so go ahead and mint a block if possible.
|
// 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?
|
// Do we need to build any potential new blocks?
|
||||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
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) {
|
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||||
// First block does the AT heavy-lifting
|
// First block does the AT heavy-lifting
|
||||||
if (newBlocks.isEmpty()) {
|
if (newBlocks.isEmpty()) {
|
||||||
@ -282,15 +293,17 @@ public class BlockMinter extends Thread {
|
|||||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||||
|
|
||||||
if (rewardShareData != null) {
|
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(),
|
newBlock.getBlockData().getHeight(),
|
||||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||||
|
Base58.encode(newBlock.getParent().getSignature()),
|
||||||
rewardShareData.getMinter(),
|
rewardShareData.getMinter(),
|
||||||
rewardShareData.getRecipient()));
|
rewardShareData.getRecipient()));
|
||||||
} else {
|
} 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(),
|
newBlock.getBlockData().getHeight(),
|
||||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||||
|
Base58.encode(newBlock.getParent().getSignature()),
|
||||||
newBlock.getMinter().getAddress()));
|
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_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
|
||||||
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 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 DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms
|
||||||
|
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||||
|
|
||||||
// To do with online accounts list
|
// To do with online accounts list
|
||||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
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. */
|
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
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.
|
* Map of recent requests for ARBITRARY transaction data payloads.
|
||||||
* <p>
|
* <p>
|
||||||
@ -372,6 +378,10 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getRecoveryMode() {
|
||||||
|
return this.recoveryMode;
|
||||||
|
}
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@ -643,6 +653,13 @@ public class Controller extends Thread {
|
|||||||
// Disregard peers that don't have a recent block
|
// Disregard peers that don't have a recent block
|
||||||
peers.removeIf(hasNoRecentBlock);
|
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
|
// Check we have enough peers to potentially synchronize
|
||||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||||
return;
|
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
|
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||||
peers.removeIf(hasInferiorChainTip);
|
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())
|
if (peers.isEmpty())
|
||||||
return;
|
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
|
// Pick random peer to sync with
|
||||||
int index = new SecureRandom().nextInt(peers.size());
|
int index = new SecureRandom().nextInt(peers.size());
|
||||||
Peer peer = peers.get(index);
|
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 static class StatusChangeEvent implements Event {
|
||||||
public StatusChangeEvent() {
|
public StatusChangeEvent() {
|
||||||
}
|
}
|
||||||
@ -789,7 +868,7 @@ public class Controller extends Thread {
|
|||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
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);
|
SysTray.getInstance().setToolTipText(tooltip);
|
||||||
|
|
||||||
this.callbackExecutor.execute(() -> {
|
this.callbackExecutor.execute(() -> {
|
||||||
|
@ -8,6 +8,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -15,8 +16,10 @@ import org.qortal.account.Account;
|
|||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
import org.qortal.block.Block.ValidationResult;
|
import org.qortal.block.Block.ValidationResult;
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
|
import org.qortal.data.block.CommonBlockData;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
import org.qortal.data.network.PeerChainTipData;
|
||||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
@ -37,6 +40,7 @@ import org.qortal.repository.RepositoryManager;
|
|||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class Synchronizer {
|
public class Synchronizer {
|
||||||
|
|
||||||
@ -56,9 +60,6 @@ public class Synchronizer {
|
|||||||
/** Maximum number of block signatures we ask from peer in one go */
|
/** Maximum number of block signatures we ask from peer in one go */
|
||||||
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
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 */
|
/* Minimum peer version that supports syncing multiple blocks at once via GetBlocksMessage */
|
||||||
private static final long PEER_VERSION_150 = 0x0100050000L;
|
private static final long PEER_VERSION_150 = 0x0100050000L;
|
||||||
|
|
||||||
@ -81,6 +82,381 @@ public class Synchronizer {
|
|||||||
return instance;
|
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.
|
* Attempt to synchronize blockchain with peer.
|
||||||
* <p>
|
* <p>
|
||||||
@ -116,9 +492,12 @@ public class Synchronizer {
|
|||||||
|
|
||||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries);
|
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries);
|
||||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
if (findCommonBlockResult != SynchronizationResult.OK) {
|
||||||
// Logging performed by fetchSummariesFromCommonBlock() above
|
// Logging performed by fetchSummariesFromCommonBlock() above
|
||||||
|
// Clear our common block cache for this peer
|
||||||
|
peer.setCommonBlockData(null);
|
||||||
return findCommonBlockResult;
|
return findCommonBlockResult;
|
||||||
|
}
|
||||||
|
|
||||||
// First summary is common block
|
// First summary is common block
|
||||||
final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
|
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
|
// Currently we work forward from common block until we hit a block we don't have
|
||||||
// TODO: rewrite as modified binary search!
|
// TODO: rewrite as modified binary search!
|
||||||
int i;
|
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()))
|
if (!repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature()))
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
// Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
||||||
blockSummariesFromCommon.subList(0, i - 1).clear();
|
blockSummariesFromCommon.subList(0, i - 1).clear();
|
||||||
@ -314,6 +697,9 @@ public class Synchronizer {
|
|||||||
|
|
||||||
// Check peer sent valid heights
|
// Check peer sent valid heights
|
||||||
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
|
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
|
||||||
|
if (Controller.isStopping())
|
||||||
|
return SynchronizationResult.SHUTTING_DOWN;
|
||||||
|
|
||||||
++lastSummaryHeight;
|
++lastSummaryHeight;
|
||||||
|
|
||||||
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
|
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
|
||||||
@ -335,7 +721,7 @@ public class Synchronizer {
|
|||||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
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.
|
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||||
@ -355,181 +741,180 @@ public class Synchronizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SynchronizationResult syncToPeerChain(Repository repository, BlockData commonBlockData, int ourInitialHeight,
|
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 int commonBlockHeight = commonBlockData.getHeight();
|
||||||
final byte[] commonBlockSig = commonBlockData.getSignature();
|
final byte[] commonBlockSig = commonBlockData.getSignature();
|
||||||
String commonBlockSig58 = Base58.encode(commonBlockSig);
|
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));
|
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
|
// 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
|
// Calculate the total number of additional blocks this peer has beyond the common block
|
||||||
int additionalPeerBlocksAfterCommonBlock = peerHeight - commonBlockHeight;
|
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).
|
// Ensure we don't request more than MAXIMUM_REQUEST_SIZE
|
||||||
List<Block> peerBlocks = new ArrayList<>();
|
int numberRequested = Math.min(numberSignaturesRequired, MAXIMUM_REQUEST_SIZE);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
// Do we need more signatures?
|
||||||
|
if (peerBlockSignatures.isEmpty() && numberRequested > 0) {
|
||||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
|
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,
|
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) {
|
// Clear our cache of common block summaries for this peer, as they are likely to be invalid
|
||||||
// Give up with this peer
|
CommonBlockData cachedCommonBlockData = peer.getCommonBlockData();
|
||||||
return SynchronizationResult.NO_REPLY;
|
if (cachedCommonBlockData != null)
|
||||||
} else {
|
cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null);
|
||||||
// Retry until retryCount reaches MAXIMUM_RETRIES
|
|
||||||
retryCount++;
|
// If we have already received recent or newer blocks from this peer, go ahead and apply them
|
||||||
int triesRemaining = MAXIMUM_RETRIES - retryCount;
|
if (peerBlocks.size() > 0) {
|
||||||
LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : "")));
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
continue;
|
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
|
numberSignaturesRequired = peerHeight - height - peerBlockSignatures.size();
|
||||||
retryCount = 0;
|
|
||||||
|
|
||||||
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
|
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
|
if (peerBlockSignatures.isEmpty()) {
|
||||||
LOGGER.debug(String.format("Fetching new blocks from peer %s after height %d", peer, commonBlockHeight));
|
LOGGER.trace(String.format("No more signatures or blocks to request from peer %s", peer));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
retryCount = 0;
|
byte[] nextPeerSignature = peerBlockSignatures.get(0);
|
||||||
while (peerBlocks.size() < peerBlockSignatures.size()) {
|
int nextHeight = height + 1;
|
||||||
if (Controller.isStopping())
|
|
||||||
return SynchronizationResult.SHUTTING_DOWN;
|
|
||||||
|
|
||||||
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)));
|
if (newBlock == null) {
|
||||||
int blockHeightToRequest = commonBlockHeight + peerBlocks.size() + 1; // +1 because we are requesting the next block, beyond what we already have in the peerBlocks array
|
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||||
Block newBlock = this.fetchBlock(repository, peer, blockSignature);
|
nextHeight, Base58.encode(nextPeerSignature)));
|
||||||
|
|
||||||
if (newBlock == null) {
|
if (retryCount >= maxRetries) {
|
||||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer, blockHeightToRequest, Base58.encode(blockSignature)));
|
|
||||||
|
|
||||||
if (retryCount >= MAXIMUM_RETRIES) {
|
// If we have already received recent or newer blocks from this peer, go ahead and apply them
|
||||||
// Give up with this peer
|
if (peerBlocks.size() > 0) {
|
||||||
return SynchronizationResult.NO_REPLY;
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
} else {
|
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||||
// Retry until retryCount reaches MAXIMUM_RETRIES
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
retryCount++;
|
if (ourLatestBlockData != null && peerLatestBlock != null && minLatestBlockTimestamp != null) {
|
||||||
int triesRemaining = MAXIMUM_RETRIES - retryCount;
|
|
||||||
LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : "")));
|
// If we have received at least one recent block, we can apply them
|
||||||
continue;
|
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)
|
// 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) {
|
while (ourHeight > commonBlockHeight) {
|
||||||
if (Controller.isStopping())
|
if (Controller.isStopping())
|
||||||
return SynchronizationResult.SHUTTING_DOWN;
|
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));
|
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) {
|
for (Block newBlock : peerBlocks) {
|
||||||
|
if (Controller.isStopping())
|
||||||
|
return SynchronizationResult.SHUTTING_DOWN;
|
||||||
|
|
||||||
ValidationResult blockResult = newBlock.isValid();
|
ValidationResult blockResult = newBlock.isValid();
|
||||||
if (blockResult != ValidationResult.OK) {
|
if (blockResult != ValidationResult.OK) {
|
||||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
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();
|
final int firstBlockHeight = blockSummaries.get(0).getHeight();
|
||||||
|
|
||||||
for (int i = 0; i < blockSummaries.size(); ++i) {
|
for (int i = 0; i < blockSummaries.size(); ++i) {
|
||||||
|
if (Controller.isStopping())
|
||||||
|
return;
|
||||||
|
|
||||||
BlockSummaryData blockSummary = blockSummaries.get(i);
|
BlockSummaryData blockSummary = blockSummaries.get(i);
|
||||||
|
|
||||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
// 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));
|
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
|
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||||
try {
|
try {
|
||||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
@ -283,6 +286,9 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
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
|
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||||
long p2shFee;
|
long p2shFee;
|
||||||
try {
|
try {
|
||||||
|
@ -7,6 +7,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -267,6 +268,22 @@ public class TradeBot implements Listener {
|
|||||||
return secret;
|
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. */
|
/** 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,
|
/*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData,
|
||||||
String newState, int newStateValue, Supplier<String> logMessageSupplier) throws DataException {
|
String newState, int newStateValue, Supplier<String> logMessageSupplier) throws DataException {
|
||||||
|
@ -42,35 +42,32 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
public Collection<ElectrumX.Server> getServers() {
|
public Collection<ElectrumX.Server> getServers() {
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
|
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
|
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
|
new Server("electrumx.erbium.eu", Server.ConnectionType.TCP, 50001),
|
||||||
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
|
new Server("electrumx.erbium.eu", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002),
|
new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001),
|
new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001),
|
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
|
||||||
new Server("xtrum.com", Server.ConnectionType.TCP, 50001),
|
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001),
|
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001),
|
new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001),
|
|
||||||
new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001),
|
|
||||||
new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
|
||||||
new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081),
|
new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001),
|
new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003),
|
new Server("xtrum.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001),
|
new Server("electrum2.taborsky.cz", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001),
|
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrum2.privateservers.network", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001),
|
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001),
|
new Server("192.166.219.200", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001),
|
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001),
|
new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001),
|
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002));
|
||||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -96,10 +93,8 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
@Override
|
@Override
|
||||||
public Collection<ElectrumX.Server> getServers() {
|
public Collection<ElectrumX.Server> getServers() {
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001),
|
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
|
||||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
|
|
||||||
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
|
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.qtornado.com", Server.ConnectionType.SSL, 51002),
|
||||||
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
|
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
|
||||||
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
|
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.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class BlockSummaryData {
|
public class BlockSummaryData {
|
||||||
@ -84,4 +85,21 @@ public class BlockSummaryData {
|
|||||||
this.minterLevel = minterLevel;
|
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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.data.block.CommonBlockData;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
import org.qortal.data.network.PeerChainTipData;
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.network.message.ChallengeMessage;
|
import org.qortal.network.message.ChallengeMessage;
|
||||||
@ -106,6 +108,9 @@ public class Peer {
|
|||||||
/** Latest block info as reported by peer. */
|
/** Latest block info as reported by peer. */
|
||||||
private PeerChainTipData peersChainTipData;
|
private PeerChainTipData peersChainTipData;
|
||||||
|
|
||||||
|
/** Our common block with this peer */
|
||||||
|
private CommonBlockData commonBlockData;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/** Construct unconnected, outbound Peer using socket address in peer data */
|
/** 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) {
|
/*package*/ void queueMessage(Message message) {
|
||||||
if (!this.pendingMessages.offer(message))
|
if (!this.pendingMessages.offer(message))
|
||||||
LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
|
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
|
// Utility methods
|
||||||
|
|
||||||
/** Returns true if ports and addresses (or hostnames) match */
|
/** 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 performPeriodicMaintenance() throws DataException;
|
||||||
|
|
||||||
public void exportNodeLocalData() throws DataException;
|
public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException;
|
||||||
|
|
||||||
public void importDataFromFile(String filename) 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.VotingRepository;
|
||||||
import org.qortal.repository.hsqldb.transaction.HSQLDBTransactionRepository;
|
import org.qortal.repository.hsqldb.transaction.HSQLDBTransactionRepository;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class HSQLDBRepository implements Repository {
|
public class HSQLDBRepository implements Repository {
|
||||||
|
|
||||||
@ -459,10 +460,44 @@ public class HSQLDBRepository implements Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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()) {
|
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 MintingAccounts DATA TO 'qortal-backup/MintingAccounts.script'");
|
||||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.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");
|
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to export sensitive/node-local data from repository");
|
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));
|
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
||||||
|
|
||||||
String escapedFilename = stmt.enquoteLiteral(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));
|
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
|
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.
|
* Execute PreparedStatement and return changed row count.
|
||||||
*
|
*
|
||||||
* @param preparedStatement
|
* @param sql
|
||||||
* @param objects
|
* @param objects
|
||||||
* @return number of changed rows
|
* @return number of changed rows
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
@ -693,8 +728,8 @@ public class HSQLDBRepository implements Repository {
|
|||||||
/**
|
/**
|
||||||
* Execute batched PreparedStatement
|
* Execute batched PreparedStatement
|
||||||
*
|
*
|
||||||
* @param preparedStatement
|
* @param sql
|
||||||
* @param objects
|
* @param batchedObjects
|
||||||
* @return number of changed rows
|
* @return number of changed rows
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
@ -818,7 +853,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
*
|
*
|
||||||
* @param tableName
|
* @param tableName
|
||||||
* @param whereClause
|
* @param whereClause
|
||||||
* @param objects
|
* @param batchedObjects
|
||||||
* @throws SQLException
|
* @throws SQLException
|
||||||
*/
|
*/
|
||||||
public int deleteBatch(String tableName, String whereClause, List<Object[]> batchedObjects) throws SQLException {
|
public int deleteBatch(String tableName, String whereClause, List<Object[]> batchedObjects) throws SQLException {
|
||||||
|
@ -52,7 +52,7 @@ public class Settings {
|
|||||||
// UI servers
|
// UI servers
|
||||||
private int uiPort = 12388;
|
private int uiPort = 12388;
|
||||||
private String[] uiLocalServers = new String[] {
|
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[] {
|
private String[] uiRemoteServers = new String[] {
|
||||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
"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;
|
private int maxNetworkThreadPoolSize = 20;
|
||||||
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
|
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
|
||||||
private int networkPoWComputePoolSize = 2;
|
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 */
|
/** Whether to sync multiple blocks at once in normal operation */
|
||||||
private boolean fastSyncEnabled = false;
|
private boolean fastSyncEnabled = false;
|
||||||
@ -417,6 +419,8 @@ public class Settings {
|
|||||||
return this.networkPoWComputePoolSize;
|
return this.networkPoWComputePoolSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxRetries() { return this.maxRetries; }
|
||||||
|
|
||||||
public String getBlockchainConfig() {
|
public String getBlockchainConfig() {
|
||||||
return this.blockchainConfig;
|
return this.blockchainConfig;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,9 @@
|
|||||||
},
|
},
|
||||||
"featureTriggers": {
|
"featureTriggers": {
|
||||||
"atFindNextTransactionFix": 275000,
|
"atFindNextTransactionFix": 275000,
|
||||||
"newBlockSigHeight": 320000
|
"newBlockSigHeight": 320000,
|
||||||
|
"shareBinFix": 399000,
|
||||||
|
"calcChainWeightTimestamp": 1620579600000
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -3,12 +3,15 @@ package org.qortal.test;
|
|||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -17,12 +20,21 @@ import org.qortal.test.common.Common;
|
|||||||
import org.qortal.test.common.TestAccount;
|
import org.qortal.test.common.TestAccount;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
import org.qortal.transform.block.BlockTransformer;
|
import org.qortal.transform.block.BlockTransformer;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
public class ChainWeightTests extends Common {
|
public class ChainWeightTests extends Common {
|
||||||
|
|
||||||
private static final Random RANDOM = new Random();
|
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
|
@Before
|
||||||
public void beforeTest() throws DataException {
|
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
|
@Test
|
||||||
public void testLongerChain() throws DataException {
|
public void testLongerChain() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -97,18 +199,20 @@ public class ChainWeightTests extends Common {
|
|||||||
BlockSummaryData commonBlockSummary = genBlockSummary(repository, commonBlockHeight);
|
BlockSummaryData commonBlockSummary = genBlockSummary(repository, commonBlockHeight);
|
||||||
byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey();
|
byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey();
|
||||||
|
|
||||||
List<BlockSummaryData> shorterChain = genBlockSummaries(repository, 3, commonBlockSummary);
|
List<BlockSummaryData> longerChain = genBlockSummaries(repository, 6, commonBlockSummary);
|
||||||
List<BlockSummaryData> longerChain = genBlockSummaries(repository, shorterChain.size() + 1, commonBlockSummary);
|
|
||||||
|
|
||||||
populateBlockSummariesMinterLevels(repository, shorterChain);
|
|
||||||
populateBlockSummariesMinterLevels(repository, longerChain);
|
populateBlockSummariesMinterLevels(repository, longerChain);
|
||||||
|
|
||||||
|
List<BlockSummaryData> shorterChain = longerChain.subList(0, longerChain.size() / 2);
|
||||||
|
|
||||||
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
|
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
|
||||||
|
|
||||||
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
|
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
|
||||||
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, 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,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
"newAssetPricingTimestamp": 0,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
"newAssetPricingTimestamp": 0,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
"newAssetPricingTimestamp": 0,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
"newAssetPricingTimestamp": 0,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"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,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
"newAssetPricingTimestamp": 0,
|
"newAssetPricingTimestamp": 0,
|
||||||
"groupApprovalTimestamp": 0,
|
"groupApprovalTimestamp": 0,
|
||||||
"atFindNextTransactionFix": 0,
|
"atFindNextTransactionFix": 0,
|
||||||
"newBlockSigHeight": 999999
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"calcChainWeightTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"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
|
# locate sha256 utility
|
||||||
my $SHA256 = `which sha256sum || which sha256`;
|
my $SHA256 = `which sha256sum || which sha256`;
|
||||||
chomp $SHA256;
|
chomp $SHA256;
|
||||||
|
die("Can't find sha256sum or sha256\n") unless length($SHA256) > 0;
|
||||||
|
|
||||||
# SHA256 of actual update file
|
# SHA256 of actual update file
|
||||||
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`;
|
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user