Browse Source

Merge branch 'master' into sync-multiple-blocks

# Conflicts:
#	src/main/java/org/qortal/controller/Synchronizer.java
sync-multiple-blocks
CalDescent 3 years ago
parent
commit
27aeb4f05f
  1. 7
      pom.xml
  2. 19
      src/main/java/org/qortal/api/resource/AdminResource.java
  3. 9
      src/main/java/org/qortal/api/resource/CrossChainResource.java
  4. 2
      src/main/java/org/qortal/api/resource/PeersResource.java
  5. 3
      src/main/java/org/qortal/block/Block.java
  6. 2
      src/main/java/org/qortal/controller/Controller.java
  7. 90
      src/main/java/org/qortal/controller/Synchronizer.java
  8. 12
      src/main/java/org/qortal/controller/tradebot/TradeBot.java
  9. 55
      src/main/java/org/qortal/data/crosschain/TradeBotData.java
  10. 2
      src/main/java/org/qortal/repository/Repository.java
  11. 101
      src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java

7
pom.xml

@ -8,7 +8,7 @@
<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>

19
src/main/java/org/qortal/api/resource/AdminResource.java

@ -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();

9
src/main/java/org/qortal/api/resource/CrossChainResource.java

@ -255,14 +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 = 10; 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;

2
src/main/java/org/qortal/api/resource/PeersResource.java

@ -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;

3
src/main/java/org/qortal/block/Block.java

@ -843,7 +843,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;
} }
@ -2023,6 +2023,7 @@ 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 || minterLevel == 0) if (this.getParent() == null || this.getParent().getSignature() == null || blockSummaryData == null || minterLevel == 0)

2
src/main/java/org/qortal/controller/Controller.java

@ -691,7 +691,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())

90
src/main/java/org/qortal/controller/Synchronizer.java

@ -119,6 +119,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?
@ -139,10 +140,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 {
@ -180,7 +186,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);
@ -290,7 +296,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);
@ -300,7 +308,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;
} }
} }
@ -310,15 +318,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
@ -391,8 +407,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);
} }
@ -414,6 +430,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.
} }
} }
@ -432,14 +451,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));
} }
} }
@ -459,6 +478,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.
@ -494,7 +519,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
@ -576,7 +601,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;
@ -605,8 +630,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;
} }
@ -793,19 +822,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 ...
@ -820,10 +843,10 @@ 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;
} }
numberSignaturesRequired = peerHeight - height - peerBlockSignatures.size(); numberSignaturesRequired = peerHeight - height - peerBlockSignatures.size();
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" : "")));
@ -845,20 +868,13 @@ public class Synchronizer {
nextHeight, Base58.encode(nextPeerSignature))); nextHeight, Base58.encode(nextPeerSignature)));
if (retryCount >= maxRetries) { 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 ...
@ -874,7 +890,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 {

12
src/main/java/org/qortal/controller/tradebot/TradeBot.java

@ -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()));
} }
} }

55
src/main/java/org/qortal/data/crosschain/TradeBotData.java

@ -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);

2
src/main/java/org/qortal/repository/Repository.java

@ -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;

101
src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java

@ -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

Loading…
Cancel
Save