forked from Qortal-Forker/qortal
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
654dc5bff3 | ||
|
13dcf7f72a | ||
|
65c26f17df | ||
|
3bedba71d5 | ||
|
1ba64d9745 | ||
|
84bf570243 | ||
|
28d50bccf9 | ||
|
66711c2e9d | ||
|
92d8c37d7d | ||
|
5824f75669 | ||
|
deb8adafc9 | ||
|
d2649b237c | ||
|
6532c258f6 | ||
|
83e2b10904 | ||
|
26c1793d85 | ||
|
23a9eea26b | ||
|
af9b536dd9 | ||
|
e4874f86f9 | ||
|
e300a957e4 | ||
|
1c38afcd25 | ||
|
a06faa7685 | ||
|
019ab2b21d | ||
|
f6ba5f5d51 | ||
|
c4cbb64643 | ||
|
8260cec713 | ||
|
f4520e2752 | ||
|
475802afbc | ||
|
a170668d9d | ||
|
f8dac39076 | ||
|
fe4ae61552 | ||
|
45efe7cd56 | ||
|
78cac7f0e6 | ||
|
a1a1b8e94a | ||
|
641a658059 |
File diff suppressed because it is too large
Load Diff
9
pom.xml
9
pom.xml
@@ -3,12 +3,12 @@
|
|||||||
<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.1</version>
|
<version>1.5.2</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
<altcoinj.version>bf9fb80</altcoinj.version>
|
<altcoinj.version>bf9fb80</altcoinj.version>
|
||||||
<bitcoinj.version>0.15.6</bitcoinj.version>
|
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||||
<bouncycastle.version>1.64</bouncycastle.version>
|
<bouncycastle.version>1.64</bouncycastle.version>
|
||||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||||
@@ -439,6 +439,11 @@
|
|||||||
<artifactId>json-simple</artifactId>
|
<artifactId>json-simple</artifactId>
|
||||||
<version>1.1.1</version>
|
<version>1.1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
<version>20210307</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-text</artifactId>
|
<artifactId>commons-text</artifactId>
|
||||||
|
23
src/main/java/org/qortal/api/model/BlockMintingInfo.java
Normal file
23
src/main/java/org/qortal/api/model/BlockMintingInfo.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package org.qortal.api.model;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class BlockMintingInfo {
|
||||||
|
|
||||||
|
public byte[] minterPublicKey;
|
||||||
|
public int minterLevel;
|
||||||
|
public int onlineAccountsCount;
|
||||||
|
public BigDecimal maxDistance;
|
||||||
|
public BigInteger keyDistance;
|
||||||
|
public double keyDistanceRatio;
|
||||||
|
public long timestamp;
|
||||||
|
public long timeDelta;
|
||||||
|
|
||||||
|
public BlockMintingInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -542,19 +542,8 @@ public class AdminResource {
|
|||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
repository.exportNodeLocalData();
|
||||||
|
return "true";
|
||||||
blockchainLock.lockInterruptibly();
|
|
||||||
|
|
||||||
try {
|
|
||||||
repository.exportNodeLocalData(true);
|
|
||||||
return "true";
|
|
||||||
} finally {
|
|
||||||
blockchainLock.unlock();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// We couldn't lock blockchain to perform export
|
|
||||||
return "false";
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
}
|
}
|
||||||
@@ -564,7 +553,7 @@ public class AdminResource {
|
|||||||
@Path("/repository/data")
|
@Path("/repository/data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Import data into repository.",
|
summary = "Import data into repository.",
|
||||||
description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
|
description = "Imports data from file on local machine. Filename is forced to 'import.json' if apiKey is not set.",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@@ -588,7 +577,7 @@ public class AdminResource {
|
|||||||
|
|
||||||
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
|
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
|
||||||
if (Settings.getInstance().getApiKey() == null)
|
if (Settings.getInstance().getApiKey() == null)
|
||||||
filename = "import.script";
|
filename = "import.json";
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
|
@@ -8,6 +8,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -20,10 +23,13 @@ import javax.ws.rs.QueryParam;
|
|||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.qortal.account.Account;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.model.BlockMintingInfo;
|
||||||
import org.qortal.api.model.BlockSignerSummary;
|
import org.qortal.api.model.BlockSignerSummary;
|
||||||
|
import org.qortal.block.Block;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
@@ -328,6 +334,59 @@ public class BlocksResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/byheight/{height}/mintinginfo")
|
||||||
|
@Operation(
|
||||||
|
summary = "Fetch block minter info using block height",
|
||||||
|
description = "Returns the minter info for the block with given height",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "the block",
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = BlockData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||||
|
})
|
||||||
|
public BlockMintingInfo getBlockMintingInfoByHeight(@PathParam("height") int height) {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||||
|
if (blockData == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||||
|
|
||||||
|
Block block = new Block(repository, blockData);
|
||||||
|
BlockData parentBlockData = repository.getBlockRepository().fromSignature(blockData.getReference());
|
||||||
|
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||||
|
if (minterLevel == 0)
|
||||||
|
// This may be unavailable when requesting a trimmed block
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
BigInteger distance = block.calcKeyDistance(parentBlockData.getHeight(), parentBlockData.getSignature(), blockData.getMinterPublicKey(), minterLevel);
|
||||||
|
double ratio = new BigDecimal(distance).divide(new BigDecimal(block.MAX_DISTANCE), 40, RoundingMode.DOWN).doubleValue();
|
||||||
|
long timestamp = block.calcTimestamp(parentBlockData, blockData.getMinterPublicKey(), minterLevel);
|
||||||
|
long timeDelta = timestamp - parentBlockData.getTimestamp();
|
||||||
|
|
||||||
|
BlockMintingInfo blockMintingInfo = new BlockMintingInfo();
|
||||||
|
blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey();
|
||||||
|
blockMintingInfo.minterLevel = minterLevel;
|
||||||
|
blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||||
|
blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE);
|
||||||
|
blockMintingInfo.keyDistance = distance;
|
||||||
|
blockMintingInfo.keyDistanceRatio = ratio;
|
||||||
|
blockMintingInfo.timestamp = timestamp;
|
||||||
|
blockMintingInfo.timeDelta = timeDelta;
|
||||||
|
|
||||||
|
return blockMintingInfo;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/timestamp/{timestamp}")
|
@Path("/timestamp/{timestamp}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@@ -255,13 +255,19 @@ public class CrossChainResource {
|
|||||||
description = "foreign blockchain",
|
description = "foreign blockchain",
|
||||||
example = "LITECOIN",
|
example = "LITECOIN",
|
||||||
schema = @Schema(implementation = SupportedBlockchain.class)
|
schema = @Schema(implementation = SupportedBlockchain.class)
|
||||||
) @PathParam("blockchain") SupportedBlockchain foreignBlockchain) {
|
) @PathParam("blockchain") SupportedBlockchain foreignBlockchain,
|
||||||
|
@Parameter(
|
||||||
|
description = "Maximum number of trades to include in price calculation",
|
||||||
|
example = "10",
|
||||||
|
schema = @Schema(type = "integer", defaultValue = "10")
|
||||||
|
) @QueryParam("maxtrades") Integer maxtrades) {
|
||||||
// foreignBlockchain is required
|
// foreignBlockchain is required
|
||||||
if (foreignBlockchain == null)
|
if (foreignBlockchain == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// We want both a minimum of 5 trades and enough trades to span at least 4 hours
|
// We want both a minimum of 5 trades and enough trades to span at least 4 hours
|
||||||
int minimumCount = 5;
|
int minimumCount = 5;
|
||||||
|
int maximumCount = maxtrades != null ? maxtrades : 10;
|
||||||
long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
|
long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
|
||||||
Boolean isFinished = Boolean.TRUE;
|
Boolean isFinished = Boolean.TRUE;
|
||||||
|
|
||||||
@@ -276,7 +282,7 @@ public class CrossChainResource {
|
|||||||
ACCT acct = acctInfo.getValue().get();
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStatesQuorum(codeHash,
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStatesQuorum(codeHash,
|
||||||
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumCount, minimumPeriod);
|
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumCount, maximumCount, minimumPeriod);
|
||||||
|
|
||||||
for (ATStateData atState : atStates) {
|
for (ATStateData atState : atStates) {
|
||||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
@@ -321,7 +321,7 @@ public class PeersResource {
|
|||||||
boolean force = true;
|
boolean force = true;
|
||||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||||
|
|
||||||
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
|
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries, true);
|
||||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
if (findCommonBlockResult != SynchronizationResult.OK)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@@ -232,7 +232,7 @@ public class Block {
|
|||||||
|
|
||||||
// Other useful constants
|
// Other useful constants
|
||||||
|
|
||||||
private static final BigInteger MAX_DISTANCE;
|
public static final BigInteger MAX_DISTANCE;
|
||||||
static {
|
static {
|
||||||
byte[] maxValue = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
byte[] maxValue = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||||
Arrays.fill(maxValue, (byte) 0xFF);
|
Arrays.fill(maxValue, (byte) 0xFF);
|
||||||
@@ -831,7 +831,7 @@ public class Block {
|
|||||||
if (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp() && 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));
|
LOGGER.trace(String.format("Chain weight calculation was based on %d blocks", blockCount));
|
||||||
|
|
||||||
return cumulativeWeight;
|
return cumulativeWeight;
|
||||||
}
|
}
|
||||||
@@ -1998,6 +1998,10 @@ public class Block {
|
|||||||
|
|
||||||
private void logDebugInfo() {
|
private void logDebugInfo() {
|
||||||
try {
|
try {
|
||||||
|
// Avoid calculations if possible. We have to check against INFO here, since Level.isMoreSpecificThan() confusingly uses <= rather than just <
|
||||||
|
if (LOGGER.getLevel().isMoreSpecificThan(Level.INFO))
|
||||||
|
return;
|
||||||
|
|
||||||
if (this.repository == null || this.getMinter() == null || this.getBlockData() == null)
|
if (this.repository == null || this.getMinter() == null || this.getBlockData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -2007,9 +2011,10 @@ public class Block {
|
|||||||
LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp()));
|
LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp()));
|
||||||
LOGGER.debug(String.format("Minter level: %d", minterLevel));
|
LOGGER.debug(String.format("Minter level: %d", minterLevel));
|
||||||
LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount()));
|
LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount()));
|
||||||
|
LOGGER.debug(String.format("AT count: %d", this.getBlockData().getATCount()));
|
||||||
|
|
||||||
BlockSummaryData blockSummaryData = new BlockSummaryData(this.getBlockData());
|
BlockSummaryData blockSummaryData = new BlockSummaryData(this.getBlockData());
|
||||||
if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null)
|
if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null || minterLevel == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
blockSummaryData.setMinterLevel(minterLevel);
|
blockSummaryData.setMinterLevel(minterLevel);
|
||||||
|
@@ -623,6 +623,11 @@ public class Controller extends Thread {
|
|||||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(new ByteArray(peerChainTipData.getLastBlockSignature()));
|
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(new ByteArray(peerChainTipData.getLastBlockSignature()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Predicate<Peer> hasOldVersion = peer -> {
|
||||||
|
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
||||||
|
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||||
|
};
|
||||||
|
|
||||||
private void potentiallySynchronize() throws InterruptedException {
|
private void potentiallySynchronize() throws InterruptedException {
|
||||||
// Already synchronizing via another thread?
|
// Already synchronizing via another thread?
|
||||||
if (this.isSynchronizing)
|
if (this.isSynchronizing)
|
||||||
@@ -639,11 +644,15 @@ 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);
|
||||||
|
|
||||||
|
// Disregard peers that are on an old version
|
||||||
|
peers.removeIf(hasOldVersion);
|
||||||
|
|
||||||
checkRecoveryModeForPeers(peers);
|
checkRecoveryModeForPeers(peers);
|
||||||
if (recoveryMode) {
|
if (recoveryMode) {
|
||||||
peers = Network.getInstance().getHandshakedPeers();
|
peers = Network.getInstance().getHandshakedPeers();
|
||||||
peers.removeIf(hasOnlyGenesisBlock);
|
peers.removeIf(hasOnlyGenesisBlock);
|
||||||
peers.removeIf(hasMisbehaved);
|
peers.removeIf(hasMisbehaved);
|
||||||
|
peers.removeIf(hasOldVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check we have enough peers to potentially synchronize
|
// Check we have enough peers to potentially synchronize
|
||||||
@@ -668,7 +677,7 @@ public class Controller extends Thread {
|
|||||||
peers.removeIf(hasInferiorChainTip);
|
peers.removeIf(hasInferiorChainTip);
|
||||||
|
|
||||||
final int peersRemoved = peersBeforeComparison - peers.size();
|
final int peersRemoved = peersBeforeComparison - peers.size();
|
||||||
if (peersRemoved > 0)
|
if (peersRemoved > 0 && peers.size() > 0)
|
||||||
LOGGER.info(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
LOGGER.info(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
||||||
|
|
||||||
if (peers.isEmpty())
|
if (peers.isEmpty())
|
||||||
|
@@ -35,6 +35,7 @@ import org.qortal.network.message.Message.MessageType;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
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;
|
import org.qortal.utils.NTP;
|
||||||
@@ -57,9 +58,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 = 2; // XXX move to Settings?
|
|
||||||
|
|
||||||
|
|
||||||
private static Synchronizer instance;
|
private static Synchronizer instance;
|
||||||
|
|
||||||
@@ -113,6 +111,7 @@ public class Synchronizer {
|
|||||||
LOGGER.debug(String.format("Searching for common blocks with %d peers...", peers.size()));
|
LOGGER.debug(String.format("Searching for common blocks with %d peers...", peers.size()));
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
int commonBlocksFound = 0;
|
int commonBlocksFound = 0;
|
||||||
|
boolean wereNewRequestsMade = false;
|
||||||
|
|
||||||
for (Peer peer : peers) {
|
for (Peer peer : peers) {
|
||||||
// Are we shutting down?
|
// Are we shutting down?
|
||||||
@@ -133,10 +132,15 @@ public class Synchronizer {
|
|||||||
Synchronizer.getInstance().findCommonBlockWithPeer(peer, repository);
|
Synchronizer.getInstance().findCommonBlockWithPeer(peer, repository);
|
||||||
if (peer.getCommonBlockData() != null)
|
if (peer.getCommonBlockData() != null)
|
||||||
commonBlocksFound++;
|
commonBlocksFound++;
|
||||||
|
|
||||||
|
// This round wasn't served entirely from the cache, so we may want to log the results
|
||||||
|
wereNewRequestsMade = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final long totalTimeTaken = System.currentTimeMillis() - startTime;
|
if (wereNewRequestsMade) {
|
||||||
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));
|
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;
|
return SynchronizationResult.OK;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -174,7 +178,7 @@ public class Synchronizer {
|
|||||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||||
|
|
||||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, false, peerBlockSummaries);
|
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, false, peerBlockSummaries, false);
|
||||||
if (findCommonBlockResult != SynchronizationResult.OK) {
|
if (findCommonBlockResult != SynchronizationResult.OK) {
|
||||||
// Logging performed by fetchSummariesFromCommonBlock() above
|
// Logging performed by fetchSummariesFromCommonBlock() above
|
||||||
peer.setCommonBlockData(null);
|
peer.setCommonBlockData(null);
|
||||||
@@ -284,7 +288,9 @@ public class Synchronizer {
|
|||||||
return peers;
|
return peers;
|
||||||
|
|
||||||
// Count the number of blocks this peer has beyond our common block
|
// Count the number of blocks this peer has beyond our common block
|
||||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||||
|
final int peerHeight = peerChainTipData.getLastHeight();
|
||||||
|
final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
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
|
// 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);
|
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
||||||
@@ -294,7 +300,7 @@ public class Synchronizer {
|
|||||||
if (peer.canUseCachedCommonBlockData()) {
|
if (peer.canUseCachedCommonBlockData()) {
|
||||||
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
|
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
|
||||||
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock().size() == summariesRequired) {
|
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock().size() == summariesRequired) {
|
||||||
LOGGER.debug(String.format("Using cached block summaries for peer %s", peer));
|
LOGGER.trace(String.format("Using cached block summaries for peer %s", peer));
|
||||||
useCachedSummaries = true;
|
useCachedSummaries = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,15 +310,23 @@ public class Synchronizer {
|
|||||||
if (summariesRequired > 0) {
|
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));
|
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);
|
// Forget any cached summaries
|
||||||
peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(blockSummaries);
|
peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(null);
|
||||||
|
|
||||||
|
// Request new block summaries
|
||||||
|
List<BlockSummaryData> blockSummaries = this.getBlockSummaries(peer, commonBlockSummary.getSignature(), summariesRequired);
|
||||||
if (blockSummaries != null) {
|
if (blockSummaries != null) {
|
||||||
LOGGER.trace(String.format("Peer %s returned %d block summar%s", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
|
LOGGER.trace(String.format("Peer %s returned %d block summar%s", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
|
||||||
|
|
||||||
if (blockSummaries.size() < summariesRequired)
|
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.
|
// This could mean that the peer has re-orged. Exclude this peer until they return the summaries we expect.
|
||||||
LOGGER.debug(String.format("Peer %s returned %d block summar%s instead of expected %d", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y"), summariesRequired));
|
LOGGER.debug(String.format("Peer %s returned %d block summar%s instead of expected %d - excluding them from this round", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y"), summariesRequired));
|
||||||
|
else if (blockSummaryWithSignature(peerLastBlockSignature, blockSummaries) == null)
|
||||||
|
// We don't have a block summary for the peer's reported chain tip, so should exclude it
|
||||||
|
LOGGER.debug(String.format("Peer %s didn't return a block summary with signature %.8s - excluding them from this round", peer, Base58.encode(peerLastBlockSignature)));
|
||||||
|
else
|
||||||
|
// All looks good, so store the retrieved block summaries in the peer's cache
|
||||||
|
peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(blockSummaries);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// There are no block summaries after this common block
|
// There are no block summaries after this common block
|
||||||
@@ -385,8 +399,8 @@ public class Synchronizer {
|
|||||||
peers.remove(peer);
|
peers.remove(peer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Our chain is inferior
|
// Our chain is inferior or equal
|
||||||
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));
|
LOGGER.debug(String.format("Peer %s is on an equal or 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();
|
dropPeersAfterCommonBlockHeight = commonBlockSummary.getHeight();
|
||||||
superiorPeersForComparison.add(peer);
|
superiorPeersForComparison.add(peer);
|
||||||
}
|
}
|
||||||
@@ -408,6 +422,9 @@ public class Synchronizer {
|
|||||||
peers.remove(peer);
|
peers.remove(peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// FUTURE: we may want to prefer peers with additional blocks, and compare the additional blocks against each other.
|
||||||
|
// This would fast track us to the best candidate for the latest block.
|
||||||
|
// Right now, peers with the exact same chain as us are treated equally to those with an additional block.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,14 +443,14 @@ public class Synchronizer {
|
|||||||
|
|
||||||
for (Peer peer : peers) {
|
for (Peer peer : peers) {
|
||||||
if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) {
|
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())));
|
LOGGER.trace(String.format("Peer %s has common block %.8s", peer, Base58.encode(peer.getCommonBlockData().getCommonBlockSummary().getSignature())));
|
||||||
|
|
||||||
BlockSummaryData commonBlockSummary = peer.getCommonBlockData().getCommonBlockSummary();
|
BlockSummaryData commonBlockSummary = peer.getCommonBlockData().getCommonBlockSummary();
|
||||||
if (!commonBlocks.contains(commonBlockSummary))
|
if (!commonBlocks.contains(commonBlockSummary))
|
||||||
commonBlocks.add(commonBlockSummary);
|
commonBlocks.add(commonBlockSummary);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LOGGER.debug(String.format("Peer %s has no common block data. Skipping...", peer));
|
LOGGER.trace(String.format("Peer %s has no common block data. Skipping...", peer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,6 +470,12 @@ public class Synchronizer {
|
|||||||
return minChainLength;
|
return minChainLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BlockSummaryData blockSummaryWithSignature(byte[] signature, List<BlockSummaryData> blockSummaries) {
|
||||||
|
if (blockSummaries != null)
|
||||||
|
return blockSummaries.stream().filter(blockSummary -> Arrays.equals(blockSummary.getSignature(), signature)).findAny().orElse(null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to synchronize blockchain with peer.
|
* Attempt to synchronize blockchain with peer.
|
||||||
@@ -488,7 +511,7 @@ public class Synchronizer {
|
|||||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||||
|
|
||||||
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, true);
|
||||||
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
|
// Clear our common block cache for this peer
|
||||||
@@ -570,7 +593,7 @@ public class Synchronizer {
|
|||||||
* @throws DataException
|
* @throws DataException
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
public SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
|
public SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon, boolean infoLogWhenNotFound) throws DataException, InterruptedException {
|
||||||
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
||||||
// Failing that, back off exponentially
|
// Failing that, back off exponentially
|
||||||
int step = INITIAL_BLOCK_STEP;
|
int step = INITIAL_BLOCK_STEP;
|
||||||
@@ -599,8 +622,12 @@ public class Synchronizer {
|
|||||||
blockSummariesBatch = this.getBlockSummaries(peer, testSignature, step);
|
blockSummariesBatch = this.getBlockSummaries(peer, testSignature, step);
|
||||||
|
|
||||||
if (blockSummariesBatch == null) {
|
if (blockSummariesBatch == null) {
|
||||||
|
if (infoLogWhenNotFound)
|
||||||
|
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
|
||||||
|
else
|
||||||
|
LOGGER.debug(String.format("Error while trying to find common block with peer %s", peer));
|
||||||
|
|
||||||
// No response - give up this time
|
// No response - give up this time
|
||||||
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
|
|
||||||
return SynchronizationResult.NO_REPLY;
|
return SynchronizationResult.NO_REPLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,6 +775,7 @@ public class Synchronizer {
|
|||||||
|
|
||||||
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));
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -786,19 +814,13 @@ public class Synchronizer {
|
|||||||
if (cachedCommonBlockData != null)
|
if (cachedCommonBlockData != null)
|
||||||
cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null);
|
cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null);
|
||||||
|
|
||||||
// If we have already received recent or newer blocks from this peer, go ahead and apply them
|
// If we have already received newer blocks from this peer that what we have already, go ahead and apply them
|
||||||
if (peerBlocks.size() > 0) {
|
if (peerBlocks.size() > 0) {
|
||||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
if (ourLatestBlockData != null && peerLatestBlock != null && minLatestBlockTimestamp != null) {
|
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 our latest block is very old....
|
||||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||||
// ... and we have received a block that is more recent than our latest block ...
|
// ... and we have received a block that is more recent than our latest block ...
|
||||||
@@ -814,7 +836,7 @@ public class Synchronizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state
|
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated or incomplete state
|
||||||
return SynchronizationResult.NO_REPLY;
|
return SynchronizationResult.NO_REPLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,21 +859,14 @@ public class Synchronizer {
|
|||||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||||
nextHeight, Base58.encode(nextPeerSignature)));
|
nextHeight, Base58.encode(nextPeerSignature)));
|
||||||
|
|
||||||
if (retryCount >= MAXIMUM_RETRIES) {
|
if (retryCount >= maxRetries) {
|
||||||
|
// If we have already received newer blocks from this peer that what we have already, go ahead and apply them
|
||||||
// If we have already received recent or newer blocks from this peer, go ahead and apply them
|
|
||||||
if (peerBlocks.size() > 0) {
|
if (peerBlocks.size() > 0) {
|
||||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
if (ourLatestBlockData != null && peerLatestBlock != null && minLatestBlockTimestamp != null) {
|
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 our latest block is very old....
|
||||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||||
// ... and we have received a block that is more recent than our latest block ...
|
// ... and we have received a block that is more recent than our latest block ...
|
||||||
@@ -867,7 +882,7 @@ public class Synchronizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state
|
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated or incomplete state
|
||||||
return SynchronizationResult.NO_REPLY;
|
return SynchronizationResult.NO_REPLY;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -875,9 +890,9 @@ public class Synchronizer {
|
|||||||
peerBlockSignatures.clear();
|
peerBlockSignatures.clear();
|
||||||
numberSignaturesRequired = peerHeight - height;
|
numberSignaturesRequired = peerHeight - height;
|
||||||
|
|
||||||
// Retry until retryCount reaches MAXIMUM_RETRIES
|
// Retry until retryCount reaches maxRetries
|
||||||
retryCount++;
|
retryCount++;
|
||||||
int triesRemaining = MAXIMUM_RETRIES - retryCount;
|
int triesRemaining = maxRetries - retryCount;
|
||||||
LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : "")));
|
LOGGER.info(String.format("Re-issuing request to peer %s (%d attempt%s remaining)", peer, triesRemaining, (triesRemaining != 1 ? "s" : "")));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@@ -272,15 +272,9 @@ public class TradeBot implements Listener {
|
|||||||
// Attempt to backup the trade bot data. This an optional step and doesn't impact trading, so don't throw an exception on failure
|
// 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 {
|
try {
|
||||||
LOGGER.info("About to backup trade bot data...");
|
LOGGER.info("About to backup trade bot data...");
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
repository.exportNodeLocalData();
|
||||||
blockchainLock.lockInterruptibly();
|
} catch (DataException e) {
|
||||||
try {
|
LOGGER.info(String.format("Repository issue when exporting trade bot data: %s", e.getMessage()));
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,6 +6,9 @@ import javax.xml.bind.annotation.XmlTransient;
|
|||||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@@ -205,6 +208,58 @@ public class TradeBotData {
|
|||||||
return this.receivingAccountInfo;
|
return this.receivingAccountInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JSONObject toJson() {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("tradePrivateKey", Base58.encode(this.getTradePrivateKey()));
|
||||||
|
jsonObject.put("acctName", this.getAcctName());
|
||||||
|
jsonObject.put("tradeState", this.getState());
|
||||||
|
jsonObject.put("tradeStateValue", this.getStateValue());
|
||||||
|
jsonObject.put("creatorAddress", this.getCreatorAddress());
|
||||||
|
jsonObject.put("atAddress", this.getAtAddress());
|
||||||
|
jsonObject.put("timestamp", this.getTimestamp());
|
||||||
|
jsonObject.put("qortAmount", this.getQortAmount());
|
||||||
|
if (this.getTradeNativePublicKey() != null) jsonObject.put("tradeNativePublicKey", Base58.encode(this.getTradeNativePublicKey()));
|
||||||
|
if (this.getTradeNativePublicKeyHash() != null) jsonObject.put("tradeNativePublicKeyHash", Base58.encode(this.getTradeNativePublicKeyHash()));
|
||||||
|
jsonObject.put("tradeNativeAddress", this.getTradeNativeAddress());
|
||||||
|
if (this.getSecret() != null) jsonObject.put("secret", Base58.encode(this.getSecret()));
|
||||||
|
if (this.getHashOfSecret() != null) jsonObject.put("hashOfSecret", Base58.encode(this.getHashOfSecret()));
|
||||||
|
jsonObject.put("foreignBlockchain", this.getForeignBlockchain());
|
||||||
|
if (this.getTradeForeignPublicKey() != null) jsonObject.put("tradeForeignPublicKey", Base58.encode(this.getTradeForeignPublicKey()));
|
||||||
|
if (this.getTradeForeignPublicKeyHash() != null) jsonObject.put("tradeForeignPublicKeyHash", Base58.encode(this.getTradeForeignPublicKeyHash()));
|
||||||
|
jsonObject.put("foreignKey", this.getForeignKey());
|
||||||
|
jsonObject.put("foreignAmount", this.getForeignAmount());
|
||||||
|
if (this.getLastTransactionSignature() != null) jsonObject.put("lastTransactionSignature", Base58.encode(this.getLastTransactionSignature()));
|
||||||
|
jsonObject.put("lockTimeA", this.getLockTimeA());
|
||||||
|
if (this.getReceivingAccountInfo() != null) jsonObject.put("receivingAccountInfo", Base58.encode(this.getReceivingAccountInfo()));
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TradeBotData fromJson(JSONObject json) {
|
||||||
|
return new TradeBotData(
|
||||||
|
json.isNull("tradePrivateKey") ? null : Base58.decode(json.getString("tradePrivateKey")),
|
||||||
|
json.isNull("acctName") ? null : json.getString("acctName"),
|
||||||
|
json.isNull("tradeState") ? null : json.getString("tradeState"),
|
||||||
|
json.isNull("tradeStateValue") ? null : json.getInt("tradeStateValue"),
|
||||||
|
json.isNull("creatorAddress") ? null : json.getString("creatorAddress"),
|
||||||
|
json.isNull("atAddress") ? null : json.getString("atAddress"),
|
||||||
|
json.isNull("timestamp") ? null : json.getLong("timestamp"),
|
||||||
|
json.isNull("qortAmount") ? null : json.getLong("qortAmount"),
|
||||||
|
json.isNull("tradeNativePublicKey") ? null : Base58.decode(json.getString("tradeNativePublicKey")),
|
||||||
|
json.isNull("tradeNativePublicKeyHash") ? null : Base58.decode(json.getString("tradeNativePublicKeyHash")),
|
||||||
|
json.isNull("tradeNativeAddress") ? null : json.getString("tradeNativeAddress"),
|
||||||
|
json.isNull("secret") ? null : Base58.decode(json.getString("secret")),
|
||||||
|
json.isNull("hashOfSecret") ? null : Base58.decode(json.getString("hashOfSecret")),
|
||||||
|
json.isNull("foreignBlockchain") ? null : json.getString("foreignBlockchain"),
|
||||||
|
json.isNull("tradeForeignPublicKey") ? null : Base58.decode(json.getString("tradeForeignPublicKey")),
|
||||||
|
json.isNull("tradeForeignPublicKeyHash") ? null : Base58.decode(json.getString("tradeForeignPublicKeyHash")),
|
||||||
|
json.isNull("foreignAmount") ? null : json.getLong("foreignAmount"),
|
||||||
|
json.isNull("foreignKey") ? null : json.getString("foreignKey"),
|
||||||
|
json.isNull("lastTransactionSignature") ? null : Base58.decode(json.getString("lastTransactionSignature")),
|
||||||
|
json.isNull("lockTimeA") ? null : json.getInt("lockTimeA"),
|
||||||
|
json.isNull("receivingAccountInfo") ? null : Base58.decode(json.getString("receivingAccountInfo"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Mostly for debugging
|
// Mostly for debugging
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("%s: %s (%d)", this.atAddress, this.tradeState, this.tradeStateValue);
|
return String.format("%s: %s (%d)", this.atAddress, this.tradeState, this.tradeStateValue);
|
||||||
|
@@ -4,7 +4,6 @@ import java.util.Arrays;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -51,7 +50,7 @@ public enum Handshake {
|
|||||||
|
|
||||||
String versionString = helloMessage.getVersionString();
|
String versionString = helloMessage.getVersionString();
|
||||||
|
|
||||||
Matcher matcher = VERSION_PATTERN.matcher(versionString);
|
Matcher matcher = peer.VERSION_PATTERN.matcher(versionString);
|
||||||
if (!matcher.lookingAt()) {
|
if (!matcher.lookingAt()) {
|
||||||
LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString));
|
LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString));
|
||||||
return null;
|
return null;
|
||||||
@@ -72,6 +71,15 @@ public enum Handshake {
|
|||||||
peer.setPeersConnectionTimestamp(peersConnectionTimestamp);
|
peer.setPeersConnectionTimestamp(peersConnectionTimestamp);
|
||||||
peer.setPeersVersion(versionString, version);
|
peer.setPeersVersion(versionString, version);
|
||||||
|
|
||||||
|
if (Settings.getInstance().getAllowConnectionsWithOlderPeerVersions() == false) {
|
||||||
|
// Ensure the peer is running at least the minimum version allowed for connections
|
||||||
|
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
||||||
|
if (peer.isAtLeastVersion(minPeerVersion) == false) {
|
||||||
|
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CHALLENGE;
|
return CHALLENGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,8 +252,6 @@ public enum Handshake {
|
|||||||
/** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
|
/** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
|
||||||
private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms
|
private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms
|
||||||
|
|
||||||
private static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX + "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
|
|
||||||
|
|
||||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||||
|
|
||||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||||
|
@@ -20,9 +20,12 @@ 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;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
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.controller.Controller;
|
||||||
import org.qortal.data.block.CommonBlockData;
|
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;
|
||||||
@@ -87,6 +90,9 @@ public class Peer {
|
|||||||
|
|
||||||
byte[] ourChallenge;
|
byte[] ourChallenge;
|
||||||
|
|
||||||
|
// Versioning
|
||||||
|
public static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX + "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
|
||||||
|
|
||||||
// Peer info
|
// Peer info
|
||||||
|
|
||||||
private final Object peerInfoLock = new Object();
|
private final Object peerInfoLock = new Object();
|
||||||
@@ -634,6 +640,35 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Minimum version
|
||||||
|
|
||||||
|
public boolean isAtLeastVersion(String minVersionString) {
|
||||||
|
if (minVersionString == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Add the version prefix
|
||||||
|
minVersionString = Controller.VERSION_PREFIX + minVersionString;
|
||||||
|
|
||||||
|
Matcher matcher = VERSION_PATTERN.matcher(minVersionString);
|
||||||
|
if (!matcher.lookingAt())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// We're expecting 3 positive shorts, so we can convert 1.2.3 into 0x0100020003
|
||||||
|
long minVersion = 0;
|
||||||
|
for (int g = 1; g <= 3; ++g) {
|
||||||
|
long value = Long.parseLong(matcher.group(g));
|
||||||
|
|
||||||
|
if (value < 0 || value > Short.MAX_VALUE)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
minVersion <<= 16;
|
||||||
|
minVersion |= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getPeersVersion() >= minVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Common block data
|
// Common block data
|
||||||
|
|
||||||
public boolean canUseCachedCommonBlockData() {
|
public boolean canUseCachedCommonBlockData() {
|
||||||
|
@@ -98,7 +98,7 @@ public interface ATRepository {
|
|||||||
*/
|
*/
|
||||||
public List<ATStateData> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
public List<ATStateData> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
||||||
Integer dataByteOffset, Long expectedValue,
|
Integer dataByteOffset, Long expectedValue,
|
||||||
int minimumCount, long minimumPeriod) throws DataException;
|
int minimumCount, int maximumCount, long minimumPeriod) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all ATStateData for a given block height.
|
* Returns all ATStateData for a given block height.
|
||||||
|
@@ -49,7 +49,7 @@ public interface Repository extends AutoCloseable {
|
|||||||
|
|
||||||
public void performPeriodicMaintenance() throws DataException;
|
public void performPeriodicMaintenance() throws DataException;
|
||||||
|
|
||||||
public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException;
|
public void exportNodeLocalData() throws DataException;
|
||||||
|
|
||||||
public void importDataFromFile(String filename) throws DataException;
|
public void importDataFromFile(String filename) throws DataException;
|
||||||
|
|
||||||
|
@@ -454,7 +454,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
@Override
|
@Override
|
||||||
public List<ATStateData> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
public List<ATStateData> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
||||||
Integer dataByteOffset, Long expectedValue,
|
Integer dataByteOffset, Long expectedValue,
|
||||||
int minimumCount, long minimumPeriod) throws DataException {
|
int minimumCount, int maximumCount, long minimumPeriod) throws DataException {
|
||||||
// We need most recent entry first so we can use its timestamp to slice further results
|
// We need most recent entry first so we can use its timestamp to slice further results
|
||||||
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, isFinished,
|
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, isFinished,
|
||||||
dataByteOffset, expectedValue, null,
|
dataByteOffset, expectedValue, null,
|
||||||
@@ -510,7 +510,8 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
bindParams.add(minimumHeight);
|
bindParams.add(minimumHeight);
|
||||||
bindParams.add(minimumCount);
|
bindParams.add(minimumCount);
|
||||||
|
|
||||||
sql.append("ORDER BY FinalATStates.height DESC");
|
sql.append("ORDER BY FinalATStates.height DESC LIMIT ?");
|
||||||
|
bindParams.add(maximumCount);
|
||||||
|
|
||||||
List<ATStateData> atStates = new ArrayList<>();
|
List<ATStateData> atStates = new ArrayList<>();
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ package org.qortal.repository.hsqldb;
|
|||||||
|
|
||||||
import java.awt.TrayIcon.MessageType;
|
import java.awt.TrayIcon.MessageType;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -15,23 +16,19 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Savepoint;
|
import java.sql.Savepoint;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
import java.util.ArrayDeque;
|
import java.util.*;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
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.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
import org.qortal.globalization.Translator;
|
import org.qortal.globalization.Translator;
|
||||||
import org.qortal.gui.SysTray;
|
import org.qortal.gui.SysTray;
|
||||||
import org.qortal.repository.ATRepository;
|
import org.qortal.repository.ATRepository;
|
||||||
@@ -52,7 +49,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;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
public class HSQLDBRepository implements Repository {
|
public class HSQLDBRepository implements Repository {
|
||||||
|
|
||||||
@@ -460,8 +457,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException {
|
public void exportNodeLocalData() throws DataException {
|
||||||
|
|
||||||
// Create the qortal-backup folder if it doesn't exist
|
// Create the qortal-backup folder if it doesn't exist
|
||||||
Path backupPath = Paths.get("qortal-backup");
|
Path backupPath = Paths.get("qortal-backup");
|
||||||
try {
|
try {
|
||||||
@@ -471,52 +467,59 @@ public class HSQLDBRepository implements Repository {
|
|||||||
throw new DataException("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
|
try {
|
||||||
File tradeBotStatesBackupFile = new File("qortal-backup/TradeBotStates.script");
|
// Load trade bot data
|
||||||
if (tradeBotStatesBackupFile.exists()) {
|
List<TradeBotData> allTradeBotData = this.getCrossChainRepository().getAllTradeBotData();
|
||||||
if (keepArchivedCopy) {
|
JSONArray allTradeBotDataJson = new JSONArray();
|
||||||
// Rename existing TradeBotStates backup, to make sure that we're not overwriting any keys
|
for (TradeBotData tradeBotData : allTradeBotData) {
|
||||||
File archivedBackupFile = new File(String.format("qortal-backup/TradeBotStates-archive-%d.script", NTP.getTime()));
|
JSONObject tradeBotDataJson = tradeBotData.toJson();
|
||||||
if (tradeBotStatesBackupFile.renameTo(archivedBackupFile))
|
allTradeBotDataJson.put(tradeBotDataJson);
|
||||||
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
|
// We need to combine existing TradeBotStates data before overwriting
|
||||||
File mintingAccountsBackupFile = new File("qortal-backup/MintingAccounts.script");
|
String fileName = "qortal-backup/TradeBotStates.json";
|
||||||
if (mintingAccountsBackupFile.exists()) {
|
File tradeBotStatesBackupFile = new File(fileName);
|
||||||
LOGGER.info("Deleting existing MintingAccounts backup because it is being replaced with a new one");
|
if (tradeBotStatesBackupFile.exists()) {
|
||||||
mintingAccountsBackupFile.delete();
|
String jsonString = new String(Files.readAllBytes(Paths.get(fileName)));
|
||||||
}
|
JSONArray allExistingTradeBotData = new JSONArray(jsonString);
|
||||||
|
Iterator<Object> iterator = allExistingTradeBotData.iterator();
|
||||||
|
while(iterator.hasNext()) {
|
||||||
|
JSONObject existingTradeBotData = (JSONObject)iterator.next();
|
||||||
|
String existingTradePrivateKey = (String) existingTradeBotData.get("tradePrivateKey");
|
||||||
|
// Check if we already have an entry for this trade
|
||||||
|
boolean found = allTradeBotData.stream().anyMatch(tradeBotData -> Base58.encode(tradeBotData.getTradePrivateKey()).equals(existingTradePrivateKey));
|
||||||
|
if (found == false)
|
||||||
|
// We need to add this to our list
|
||||||
|
allTradeBotDataJson.put(existingTradeBotData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try (Statement stmt = this.connection.createStatement()) {
|
FileWriter writer = new FileWriter(fileName);
|
||||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'qortal-backup/MintingAccounts.script'");
|
writer.write(allTradeBotDataJson.toString());
|
||||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'qortal-backup/TradeBotStates.script'");
|
writer.close();
|
||||||
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
|
LOGGER.info("Exported sensitive/node-local data: trade bot states");
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new DataException("Unable to export sensitive/node-local data from repository");
|
} catch (DataException | IOException e) {
|
||||||
|
throw new DataException("Unable to export trade bot states from repository");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void importDataFromFile(String filename) throws DataException {
|
public void importDataFromFile(String filename) throws DataException {
|
||||||
try (Statement stmt = this.connection.createStatement()) {
|
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
||||||
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
try {
|
||||||
|
String jsonString = new String(Files.readAllBytes(Paths.get(filename)));
|
||||||
String escapedFilename = stmt.enquoteLiteral(filename);
|
JSONArray tradeBotDataToImport = new JSONArray(jsonString);
|
||||||
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " CONTINUE ON ERROR");
|
Iterator<Object> iterator = tradeBotDataToImport.iterator();
|
||||||
|
while(iterator.hasNext()) {
|
||||||
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
|
JSONObject tradeBotDataJson = (JSONObject)iterator.next();
|
||||||
} catch (SQLException e) {
|
TradeBotData tradeBotData = TradeBotData.fromJson(tradeBotDataJson);
|
||||||
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
|
this.getCrossChainRepository().save(tradeBotData);
|
||||||
throw new DataException("Unable to import sensitive/node-local data to repository: " + e.getMessage());
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new DataException("Unable to import sensitive/node-local trade bot states to repository: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
LOGGER.info(() -> String.format("Imported trade bot states into repository from %s", filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -122,6 +122,15 @@ 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;
|
||||||
|
|
||||||
|
/** Minimum peer version number required in order to sync with them */
|
||||||
|
private String minPeerVersion = "1.5.0";
|
||||||
|
/** Whether to allow connections with peers below minPeerVersion
|
||||||
|
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||||
|
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||||
|
private boolean allowConnectionsWithOlderPeerVersions = true;
|
||||||
|
|
||||||
// Which blockchains this node is running
|
// Which blockchains this node is running
|
||||||
private String blockchainConfig = null; // use default from resources
|
private String blockchainConfig = null; // use default from resources
|
||||||
@@ -408,6 +417,12 @@ public class Settings {
|
|||||||
return this.networkPoWComputePoolSize;
|
return this.networkPoWComputePoolSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxRetries() { return this.maxRetries; }
|
||||||
|
|
||||||
|
public String getMinPeerVersion() { return this.minPeerVersion; }
|
||||||
|
|
||||||
|
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
|
||||||
|
|
||||||
public String getBlockchainConfig() {
|
public String getBlockchainConfig() {
|
||||||
return this.blockchainConfig;
|
return this.blockchainConfig;
|
||||||
}
|
}
|
||||||
|
148
tools/block-timings.sh
Executable file
148
tools/block-timings.sh
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
start_height=$1
|
||||||
|
count=$2
|
||||||
|
target=$3
|
||||||
|
deviation=$4
|
||||||
|
power=$5
|
||||||
|
|
||||||
|
if [ -z "${start_height}" ]; then
|
||||||
|
echo
|
||||||
|
echo "Error: missing start height."
|
||||||
|
echo
|
||||||
|
echo "Usage:"
|
||||||
|
echo "block-timings.sh <startheight> [count] [target] [deviation] [power]"
|
||||||
|
echo
|
||||||
|
echo "startheight: a block height, preferably within the untrimmed range, to avoid data gaps"
|
||||||
|
echo "count: the number of blocks to request and analyse after the start height. Default: 100"
|
||||||
|
echo "target: the target block time in milliseconds. Originates from blockchain.json. Default: 60000"
|
||||||
|
echo "deviation: the allowed block time deviation in milliseconds. Originates from blockchain.json. Default: 30000"
|
||||||
|
echo "power: used when transforming key distance to a time offset. Originates from blockchain.json. Default: 0.2"
|
||||||
|
echo
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
count=${count:=100}
|
||||||
|
target=${target:=60000}
|
||||||
|
deviation=${deviation:=30000}
|
||||||
|
power=${power:=0.2}
|
||||||
|
|
||||||
|
finish_height=$((start_height + count - 1))
|
||||||
|
height=$start_height
|
||||||
|
|
||||||
|
echo "Settings:"
|
||||||
|
echo "Target time offset: ${target}"
|
||||||
|
echo "Deviation: ${deviation}"
|
||||||
|
echo "Power transform: ${power}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
function calculate_time_offset {
|
||||||
|
local key_distance_ratio=$1
|
||||||
|
local transformed=$( echo "" | awk "END {print ${key_distance_ratio} ^ ${power}}")
|
||||||
|
local time_offset=$(echo "${deviation}*2*${transformed}" | bc)
|
||||||
|
time_offset=${time_offset%.*}
|
||||||
|
echo $time_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function fetch_and_process_blocks {
|
||||||
|
|
||||||
|
echo "Fetching blocks from height ${start_height} to ${finish_height}..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
total_time_offset=0
|
||||||
|
errors=0
|
||||||
|
|
||||||
|
while [ "${height}" -le "${finish_height}" ]; do
|
||||||
|
block_minting_info=$(curl -s "http://localhost:12391/blocks/byheight/${height}/mintinginfo")
|
||||||
|
error=$(echo "${block_minting_info}" | jq -r .error)
|
||||||
|
if [ "${error}" != "null" ]; then
|
||||||
|
echo "Error fetching minting info for block ${height}"
|
||||||
|
echo
|
||||||
|
errors=$((errors+1))
|
||||||
|
height=$((height+1))
|
||||||
|
continue;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse minting info
|
||||||
|
minter_level=$(echo "${block_minting_info}" | jq -r .minterLevel)
|
||||||
|
online_accounts_count=$(echo "${block_minting_info}" | jq -r .onlineAccountsCount)
|
||||||
|
key_distance_ratio=$(echo "${block_minting_info}" | jq -r .keyDistanceRatio)
|
||||||
|
time_delta=$(echo "${block_minting_info}" | jq -r .timeDelta)
|
||||||
|
|
||||||
|
time_offset=$(calculate_time_offset "${key_distance_ratio}")
|
||||||
|
block_time=$((target-deviation+time_offset))
|
||||||
|
|
||||||
|
echo "=== BLOCK ${height} ==="
|
||||||
|
echo "Minter level: ${minter_level}"
|
||||||
|
echo "Online accounts: ${online_accounts_count}"
|
||||||
|
echo "Key distance ratio: ${key_distance_ratio}"
|
||||||
|
echo "Time offset: ${time_offset}"
|
||||||
|
echo "Block time (real): ${time_delta}"
|
||||||
|
echo "Block time (calculated): ${block_time}"
|
||||||
|
|
||||||
|
if [ "${time_delta}" -ne "${block_time}" ]; then
|
||||||
|
echo "WARNING: Block time mismatch. This is to be expected when using custom settings."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
total_time_offset=$((total_time_offset+block_time))
|
||||||
|
|
||||||
|
height=$((height+1))
|
||||||
|
done
|
||||||
|
|
||||||
|
adjusted_count=$((count-errors))
|
||||||
|
if [ "${adjusted_count}" -eq 0 ]; then
|
||||||
|
echo "No blocks were retrieved."
|
||||||
|
echo
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
mean_time_offset=$((total_time_offset/adjusted_count))
|
||||||
|
time_offset_diff=$((mean_time_offset-target))
|
||||||
|
|
||||||
|
echo "==================="
|
||||||
|
echo "===== SUMMARY ====="
|
||||||
|
echo "==================="
|
||||||
|
echo "Total blocks retrieved: ${adjusted_count}"
|
||||||
|
echo "Total blocks failed: ${errors}"
|
||||||
|
echo "Mean time offset: ${mean_time_offset}ms"
|
||||||
|
echo "Target time offset: ${target}ms"
|
||||||
|
echo "Difference from target: ${time_offset_diff}ms"
|
||||||
|
echo
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimate_key_distance_ratio_for_level {
|
||||||
|
local level=$1
|
||||||
|
local example_key_distance="0.5"
|
||||||
|
echo "(${example_key_distance}/${level})"
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimate_block_timestamps {
|
||||||
|
min_block_time=9999999
|
||||||
|
max_block_time=0
|
||||||
|
|
||||||
|
echo "===== BLOCK TIME ESTIMATES ====="
|
||||||
|
|
||||||
|
for level in {1..10}; do
|
||||||
|
example_key_distance_ratio=$(estimate_key_distance_ratio_for_level "${level}")
|
||||||
|
time_offset=$(calculate_time_offset "${example_key_distance_ratio}")
|
||||||
|
block_time=$((target-deviation+time_offset))
|
||||||
|
|
||||||
|
if [ "${block_time}" -gt "${max_block_time}" ]; then
|
||||||
|
max_block_time=${block_time}
|
||||||
|
fi
|
||||||
|
if [ "${block_time}" -lt "${min_block_time}" ]; then
|
||||||
|
min_block_time=${block_time}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Level: ${level}, time offset: ${time_offset}, block time: ${block_time}"
|
||||||
|
done
|
||||||
|
block_time_range=$((max_block_time-min_block_time))
|
||||||
|
echo "Range: ${block_time_range}"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_and_process_blocks
|
||||||
|
estimate_block_timestamps
|
Reference in New Issue
Block a user