mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-14 11:15:49 +00:00
New synchronizer and other improvements
API call GET /addresses/online reports online accounts, including both addresses relating to the proxy-forge public key. New PeerChainTipData class to replace the broken "peer data lock" that was supposed to make sure peer's last height/blockSig/timestamp were all in sync. Now peer's chain tip data is a single object reference that can be replaced in one go. Removed pointless API calls /blocks/time and /blocks/{generatingbalance}. Various changes, mostly in Block class, to do with switching to BlockTimingByHeight from old min/max block time. New block 'weight' based on number of online accounts and 'distance' of perturbed generator public key from 'ideal' public key (for that particular block's height). New sub-chain 'weight' based on accumulating block weights, currently by shifting previous accumulator left by 8 bits then adding next block's weight. More validation of BlockChain config. Helpful for debugging, probably not very useful to end-users. BlockGenerator now uses unified Peer predicates from Controller, like: Controller.hasMisbehaved, Controller.hasNoRecentBlock, etc. Controller now keeps a list of chain-tip signatures that are for inferior chains, so it doesn't try to synchronize with peers with inferior chains. (This list is wiped when node's blockchain changes/block is generated). Controller now asks Gui to display error box if it can't parse Settings. Controller.potentiallySynchronize() does more filtering of potential peers before calling actuallySynchronize(). (Mostly moved from Synchronizer, so now we expect actuallySynchronize() to do something rather than bail out because it doesn't like the peer after all). If synchronization discovers that peer has an inferior chain, then Controller notifies that peer of our superior chain, to help keep the network in sync. Renamed OnlineAccount to OnlineAccountData, as it is in package org.qora.data after all... Synchronizer reworked to request block summaries so it can judge which chain is better, and hence whether to sync with peer or abort. Slight optimization of Peer.readChannel() to exit earlier if no more network messages can be extracted from buffer. More tests. Improved documentation and logging.
This commit is contained in:
parent
c889e95da4
commit
4c6656dd17
50
src/main/java/org/qora/api/model/ApiOnlineAccount.java
Normal file
50
src/main/java/org/qora/api/model/ApiOnlineAccount.java
Normal file
@ -0,0 +1,50 @@
|
||||
package org.qora.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ApiOnlineAccount {
|
||||
|
||||
protected long timestamp;
|
||||
protected byte[] signature;
|
||||
protected byte[] publicKey;
|
||||
protected String generatorAddress;
|
||||
protected String recipientAddress;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB serialization
|
||||
protected ApiOnlineAccount() {
|
||||
}
|
||||
|
||||
public ApiOnlineAccount(long timestamp, byte[] signature, byte[] publicKey, String generatorAddress, String recipientAddress) {
|
||||
this.timestamp = timestamp;
|
||||
this.signature = signature;
|
||||
this.publicKey = publicKey;
|
||||
this.generatorAddress = generatorAddress;
|
||||
this.recipientAddress = recipientAddress;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
public String getGeneratorAddress() {
|
||||
return this.generatorAddress;
|
||||
}
|
||||
|
||||
public String getRecipientAddress() {
|
||||
return this.recipientAddress;
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package org.qora.api.model;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qora.data.network.PeerChainTipData;
|
||||
import org.qora.data.network.PeerData;
|
||||
import org.qora.network.Handshake;
|
||||
import org.qora.network.Peer;
|
||||
@ -49,9 +50,12 @@ public class ConnectedPeer {
|
||||
this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp();
|
||||
}
|
||||
|
||||
this.lastHeight = peer.getLastHeight();
|
||||
this.lastBlockSignature = peer.getLastBlockSignature();
|
||||
this.lastBlockTimestamp = peer.getLastBlockTimestamp();
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData != null) {
|
||||
this.lastHeight = peerChainTipData.getLastHeight();
|
||||
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -27,6 +28,7 @@ import org.qora.api.ApiError;
|
||||
import org.qora.api.ApiErrors;
|
||||
import org.qora.api.ApiException;
|
||||
import org.qora.api.ApiExceptionFactory;
|
||||
import org.qora.api.model.ApiOnlineAccount;
|
||||
import org.qora.api.model.ProxyKeyRequest;
|
||||
import org.qora.api.resource.TransactionsResource;
|
||||
import org.qora.asset.Asset;
|
||||
@ -34,7 +36,7 @@ import org.qora.controller.Controller;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.account.ProxyForgerData;
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.data.transaction.ProxyForgingTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
@ -159,12 +161,32 @@ public class AddressesResource {
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "online accounts",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = OnlineAccount.class)))
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = ApiOnlineAccount.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<OnlineAccount> getOnlineAccounts() {
|
||||
return Controller.getInstance().getOnlineAccounts();
|
||||
@ApiErrors({ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ApiOnlineAccount> getOnlineAccounts() {
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
|
||||
// Map OnlineAccountData entries to OnlineAccount via proxy-relationship data
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ApiOnlineAccount> apiOnlineAccounts = new ArrayList<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
ProxyForgerData proxyForgerData = repository.getAccountRepository().getProxyForgeData(onlineAccountData.getPublicKey());
|
||||
if (proxyForgerData == null)
|
||||
// This shouldn't happen?
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
|
||||
|
||||
apiOnlineAccounts.add(new ApiOnlineAccount(onlineAccountData.getTimestamp(), onlineAccountData.getSignature(), onlineAccountData.getPublicKey(),
|
||||
proxyForgerData.getForger(), proxyForgerData.getRecipient()));
|
||||
}
|
||||
|
||||
return apiOnlineAccounts;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -320,58 +320,6 @@ public class BlocksResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/time")
|
||||
@Operation(
|
||||
summary = "Estimated time to forge next block",
|
||||
description = "Calculates the time it should take for the network to generate the next block",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the time in seconds",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public long getTimePerBlock() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
return Block.calcForgingDelay(blockData.getGeneratingBalance());
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/time/{generatingbalance}")
|
||||
@Operation(
|
||||
summary = "Estimated time to forge block given generating balance",
|
||||
description = "Calculates the time it should take for the network to generate blocks based on specified generating balance",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the time", // in seconds?
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public long getTimePerBlock(@PathParam("generatingbalance") BigDecimal generatingbalance) {
|
||||
return Block.calcForgingDelay(generatingbalance);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/height")
|
||||
@Operation(
|
||||
|
@ -29,8 +29,9 @@ import org.qora.data.account.ProxyForgerData;
|
||||
import org.qora.data.at.ATData;
|
||||
import org.qora.data.at.ATStateData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.block.BlockSummaryData;
|
||||
import org.qora.data.block.BlockTransactionData;
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.ATRepository;
|
||||
import org.qora.repository.BlockRepository;
|
||||
@ -128,6 +129,11 @@ public class Block {
|
||||
// Other properties
|
||||
private static final Logger LOGGER = LogManager.getLogger(Block.class);
|
||||
|
||||
/** Number of left-shifts to apply to block's online accounts count when calculating block's weight. */
|
||||
private static final int ACCOUNTS_COUNT_SHIFT = Transformer.PUBLIC_KEY_LENGTH * 8;
|
||||
/** Number of left-shifts to apply to previous block's weight when calculating a chain's weight. */
|
||||
private static final int CHAIN_WEIGHT_SHIFT = 8;
|
||||
|
||||
/** Sorted list of transactions attached to this block */
|
||||
protected List<Transaction> transactions;
|
||||
|
||||
@ -228,26 +234,26 @@ public class Block {
|
||||
BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance();
|
||||
|
||||
// Fetch our list of online accounts
|
||||
List<OnlineAccount> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
if (onlineAccounts.isEmpty())
|
||||
throw new IllegalStateException("No online accounts - not even our own?");
|
||||
|
||||
// Find newest online accounts timestamp
|
||||
long onlineAccountsTimestamp = 0;
|
||||
for (OnlineAccount onlineAccount : onlineAccounts) {
|
||||
if (onlineAccount.getTimestamp() > onlineAccountsTimestamp)
|
||||
onlineAccountsTimestamp = onlineAccount.getTimestamp();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
if (onlineAccountData.getTimestamp() > onlineAccountsTimestamp)
|
||||
onlineAccountsTimestamp = onlineAccountData.getTimestamp();
|
||||
}
|
||||
|
||||
// Map using account index (in list of proxy forger accounts)
|
||||
Map<Integer, OnlineAccount> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccount onlineAccount : onlineAccounts) {
|
||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
// Disregard online accounts with different timestamps
|
||||
if (onlineAccount.getTimestamp() != onlineAccountsTimestamp)
|
||||
if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
|
||||
continue;
|
||||
|
||||
int accountIndex = repository.getAccountRepository().getProxyAccountIndex(onlineAccount.getPublicKey());
|
||||
indexedOnlineAccounts.put(accountIndex, onlineAccount);
|
||||
int accountIndex = repository.getAccountRepository().getProxyAccountIndex(onlineAccountData.getPublicKey());
|
||||
indexedOnlineAccounts.put(accountIndex, onlineAccountData);
|
||||
}
|
||||
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
|
||||
accountIndexes.sort(null);
|
||||
@ -262,8 +268,8 @@ public class Block {
|
||||
byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
|
||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||
Integer accountIndex = accountIndexes.get(i);
|
||||
OnlineAccount onlineAccount = indexedOnlineAccounts.get(accountIndex);
|
||||
System.arraycopy(onlineAccount.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
}
|
||||
|
||||
byte[] generatorSignature;
|
||||
@ -421,8 +427,10 @@ public class Block {
|
||||
if (this.blockData.getHeight() == null)
|
||||
throw new IllegalStateException("Can't calculate next block's generating balance as this block's height is unset");
|
||||
|
||||
final int blockDifficultyInterval = BlockChain.getInstance().getBlockDifficultyInterval();
|
||||
|
||||
// This block not at the start of an interval?
|
||||
if (this.blockData.getHeight() % BlockChain.getInstance().getBlockDifficultyInterval() != 0)
|
||||
if (this.blockData.getHeight() % blockDifficultyInterval != 0)
|
||||
return this.blockData.getGeneratingBalance();
|
||||
|
||||
// Return cached calculation if we have one
|
||||
@ -437,7 +445,7 @@ public class Block {
|
||||
BlockData firstBlock = this.blockData;
|
||||
|
||||
try {
|
||||
for (int i = 1; firstBlock != null && i < BlockChain.getInstance().getBlockDifficultyInterval(); ++i)
|
||||
for (int i = 1; firstBlock != null && i < blockDifficultyInterval; ++i)
|
||||
firstBlock = blockRepo.fromSignature(firstBlock.getReference());
|
||||
} catch (DataException e) {
|
||||
firstBlock = null;
|
||||
@ -451,8 +459,7 @@ public class Block {
|
||||
long previousGeneratingTime = this.blockData.getTimestamp() - firstBlock.getTimestamp();
|
||||
|
||||
// Calculate expected forging time (in ms) for a whole interval based on this block's generating balance.
|
||||
long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BlockChain.getInstance().getBlockDifficultyInterval()
|
||||
* 1000;
|
||||
long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance(), this.blockData.getHeight()) * blockDifficultyInterval;
|
||||
|
||||
// Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances.
|
||||
// NOTE: we have to use doubles and longs here to keep compatibility with Qora v1 results
|
||||
@ -464,20 +471,17 @@ public class Block {
|
||||
return this.cachedNextGeneratingBalance;
|
||||
}
|
||||
|
||||
public static long calcBaseTarget(BigDecimal generatingBalance) {
|
||||
generatingBalance = Block.minMaxBalance(generatingBalance);
|
||||
return generatingBalance.longValue() * calcForgingDelay(generatingBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expected forging delay, in seconds, since previous block based on passed generating balance.
|
||||
*/
|
||||
public static long calcForgingDelay(BigDecimal generatingBalance) {
|
||||
public static long calcForgingDelay(BigDecimal generatingBalance, int previousBlockHeight) {
|
||||
generatingBalance = Block.minMaxBalance(generatingBalance);
|
||||
|
||||
double percentageOfTotal = generatingBalance.divide(BlockChain.getInstance().getMaxBalance()).doubleValue();
|
||||
long actualBlockTime = (long) (BlockChain.getInstance().getMinBlockTime()
|
||||
+ ((BlockChain.getInstance().getMaxBlockTime() - BlockChain.getInstance().getMinBlockTime()) * (1 - percentageOfTotal)));
|
||||
|
||||
BlockTimingByHeight blockTiming = BlockChain.getInstance().getBlockTimingByHeight(previousBlockHeight + 1);
|
||||
|
||||
long actualBlockTime = (long) (blockTiming.target + (blockTiming.deviation * (1 - (2 * percentageOfTotal))));
|
||||
|
||||
return actualBlockTime;
|
||||
}
|
||||
@ -723,14 +727,32 @@ public class Block {
|
||||
return Crypto.digest(Bytes.concat(Longs.toByteArray(height), publicKey));
|
||||
}
|
||||
|
||||
public static BigInteger calcGeneratorDistance(BlockData parentBlockData, byte[] generatorPublicKey) {
|
||||
final int parentHeight = parentBlockData.getHeight();
|
||||
final int thisHeight = parentHeight + 1;
|
||||
public static BigInteger calcKeyDistance(int parentHeight, byte[] parentBlockSignature, byte[] publicKey) {
|
||||
byte[] idealKey = calcIdealGeneratorPublicKey(parentHeight, parentBlockSignature);
|
||||
byte[] perturbedKey = calcHeightPerturbedPublicKey(parentHeight + 1, publicKey);
|
||||
|
||||
// Convert all bits into unsigned BigInteger
|
||||
BigInteger idealBI = new BigInteger(1, calcIdealGeneratorPublicKey(parentHeight, parentBlockData.getSignature()));
|
||||
BigInteger generatorBI = new BigInteger(1, calcHeightPerturbedPublicKey(thisHeight, generatorPublicKey));
|
||||
return idealBI.subtract(generatorBI).abs();
|
||||
BigInteger keyDistance = MAX_DISTANCE.subtract(new BigInteger(idealKey).subtract(new BigInteger(perturbedKey)).abs());
|
||||
return keyDistance;
|
||||
}
|
||||
|
||||
public static BigInteger calcBlockWeight(int parentHeight, byte[] parentBlockSignature, BlockSummaryData blockSummaryData) {
|
||||
BigInteger keyDistance = calcKeyDistance(parentHeight, parentBlockSignature, blockSummaryData.getGeneratorPublicKey());
|
||||
BigInteger weight = BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
||||
return weight;
|
||||
}
|
||||
|
||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries) {
|
||||
BigInteger cumulativeWeight = BigInteger.ZERO;
|
||||
int parentHeight = commonBlockHeight;
|
||||
byte[] parentBlockSignature = commonBlockSignature;
|
||||
|
||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData));
|
||||
parentHeight = blockSummaryData.getHeight();
|
||||
parentBlockSignature = blockSummaryData.getSignature();
|
||||
}
|
||||
|
||||
return cumulativeWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -746,7 +768,7 @@ public class Block {
|
||||
* So this block's timestamp is previous block's timestamp + 30s + 12s.
|
||||
*/
|
||||
public static long calcTimestamp(BlockData parentBlockData, byte[] generatorPublicKey) {
|
||||
BigInteger distance = calcGeneratorDistance(parentBlockData, generatorPublicKey);
|
||||
BigInteger distance = calcKeyDistance(parentBlockData.getHeight(), parentBlockData.getSignature(), generatorPublicKey);
|
||||
final int thisHeight = parentBlockData.getHeight() + 1;
|
||||
BlockTimingByHeight blockTiming = BlockChain.getInstance().getBlockTimingByHeight(thisHeight);
|
||||
|
||||
|
@ -61,10 +61,6 @@ public class BlockChain {
|
||||
|
||||
/** Number of blocks between recalculating block's generating balance. */
|
||||
private int blockDifficultyInterval;
|
||||
/** Minimum target time between blocks, in seconds. */
|
||||
private long minBlockTime;
|
||||
/** Maximum target time between blocks, in seconds. */
|
||||
private long maxBlockTime;
|
||||
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
|
||||
private long blockTimestampMargin;
|
||||
/** Maximum block size, in bytes. */
|
||||
@ -253,14 +249,6 @@ public class BlockChain {
|
||||
return this.blockDifficultyInterval;
|
||||
}
|
||||
|
||||
public long getMinBlockTime() {
|
||||
return this.minBlockTime;
|
||||
}
|
||||
|
||||
public long getMaxBlockTime() {
|
||||
return this.maxBlockTime;
|
||||
}
|
||||
|
||||
public long getBlockTimestampMargin() {
|
||||
return this.blockTimestampMargin;
|
||||
}
|
||||
@ -361,22 +349,25 @@ public class BlockChain {
|
||||
|
||||
/** Validate blockchain config read from JSON */
|
||||
private void validateConfig() {
|
||||
if (this.genesisInfo == null) {
|
||||
LOGGER.error("No \"genesisInfo\" entry found in blockchain config");
|
||||
throw new RuntimeException("No \"genesisInfo\" entry found in blockchain config");
|
||||
}
|
||||
if (this.genesisInfo == null)
|
||||
Settings.throwValidationError("No \"genesisInfo\" entry found in blockchain config");
|
||||
|
||||
if (this.featureTriggers == null) {
|
||||
LOGGER.error("No \"featureTriggers\" entry found in blockchain config");
|
||||
throw new RuntimeException("No \"featureTriggers\" entry found in blockchain config");
|
||||
}
|
||||
if (this.featureTriggers == null)
|
||||
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
|
||||
|
||||
if (this.blockTimestampMargin <= 0)
|
||||
Settings.throwValidationError("Invalid \"blockTimestampMargin\" in blockchain config");
|
||||
|
||||
if (this.transactionExpiryPeriod <= 0)
|
||||
Settings.throwValidationError("Invalid \"transactionExpiryPeriod\" in blockchain config");
|
||||
|
||||
if (this.maxBlockSize <= 0)
|
||||
Settings.throwValidationError("Invalid \"maxBlockSize\" in blockchain config");
|
||||
|
||||
// Check all featureTriggers are present
|
||||
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
|
||||
if (!this.featureTriggers.containsKey(featureTrigger.name())) {
|
||||
LOGGER.error(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
||||
throw new RuntimeException("Missing feature trigger in blockchain config");
|
||||
}
|
||||
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
||||
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,14 +113,14 @@ public class BlockGenerator extends Thread {
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasPeerMisbehaved);
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Don't generate if we don't have enough connected peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(peer -> peer.getLastBlockTimestamp() == null || peer.getLastBlockTimestamp() < minLatestBlockTimestamp);
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// If we have any peers with a recent block, but our latest block isn't recent
|
||||
// then we need to synchronize instead of generating.
|
||||
|
@ -30,13 +30,15 @@ import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.api.ApiService;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qora.block.BlockGenerator;
|
||||
import org.qora.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.account.ForgingAccountData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.block.BlockSummaryData;
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.data.network.PeerChainTipData;
|
||||
import org.qora.data.network.PeerData;
|
||||
import org.qora.data.transaction.ArbitraryTransactionData;
|
||||
import org.qora.data.transaction.ArbitraryTransactionData.DataType;
|
||||
@ -77,6 +79,7 @@ import org.qora.transaction.Transaction.TransactionType;
|
||||
import org.qora.transaction.Transaction.ValidationResult;
|
||||
import org.qora.ui.UiService;
|
||||
import org.qora.utils.Base58;
|
||||
import org.qora.utils.ByteArray;
|
||||
import org.qora.utils.NTP;
|
||||
import org.qora.utils.Triple;
|
||||
|
||||
@ -129,10 +132,8 @@ public class Controller extends Thread {
|
||||
/** Whether we can generate new blocks, as reported by BlockGenerator. */
|
||||
private volatile boolean isGenerationPossible = false;
|
||||
|
||||
/** Signature of peer's latest block that will result in no sync action needed (e.g. INFERIOR_CHAIN, NOTHING_TO_DO, OK). */
|
||||
private byte[] noSyncPeerBlockSignature = null;
|
||||
/** Signature of our latest block that will result in no sync action needed (e.g. INFERIOR_CHAIN, NOTHING_TO_DO, OK). */
|
||||
private byte[] noSyncOurBlockSignature = null;
|
||||
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Map of recent requests for ARBITRARY transaction data payloads.
|
||||
@ -157,7 +158,7 @@ public class Controller extends Thread {
|
||||
private final ReentrantLock blockchainLock = new ReentrantLock();
|
||||
|
||||
/** Cache of 'online accounts' */
|
||||
List<OnlineAccount> onlineAccounts = new ArrayList<>();
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
// Constructors
|
||||
|
||||
@ -205,7 +206,7 @@ public class Controller extends Thread {
|
||||
return this.buildVersion;
|
||||
}
|
||||
|
||||
/** Returns current blockchain height, or 0 if there's a repository issue */
|
||||
/** Returns current blockchain height, or 0 if it's not available. */
|
||||
public int getChainHeight() {
|
||||
BlockData blockData = this.chainTip.get();
|
||||
if (blockData == null)
|
||||
@ -214,7 +215,7 @@ public class Controller extends Thread {
|
||||
return blockData.getHeight();
|
||||
}
|
||||
|
||||
/** Returns highest block, or null if there's a repository issue */
|
||||
/** Returns highest block, or null if it's not available. */
|
||||
public BlockData getChainTip() {
|
||||
return this.chainTip.get();
|
||||
}
|
||||
@ -240,10 +241,15 @@ public class Controller extends Thread {
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
try {
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
} catch (Throwable t) {
|
||||
Gui.getInstance().fatalError("Settings file", t.getMessage());
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
}
|
||||
|
||||
LOGGER.info("Starting NTP");
|
||||
NTP.start();
|
||||
@ -406,54 +412,72 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private void potentiallySynchronize() throws InterruptedException {
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return;
|
||||
public static final Predicate<Peer> hasMisbehaved = peer -> {
|
||||
final Long lastMisbehaved = peer.getPeerData().getLastMisbehaved();
|
||||
return lastMisbehaved != null && lastMisbehaved > NTP.getTime() - MISBEHAVIOUR_COOLOFF;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
|
||||
final BlockData latestBlockData = getInstance().getChainTip();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
final List<ByteArray> inferiorChainTips = getInstance().inferiorChainSignatures;
|
||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(new ByteArray(peerChainTipData.getLastBlockSignature()));
|
||||
};
|
||||
|
||||
private void potentiallySynchronize() throws InterruptedException {
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(hasPeerMisbehaved);
|
||||
peers.removeIf(hasMisbehaved);
|
||||
|
||||
// Disregard peers that only have genesis block
|
||||
peers.removeIf(hasOnlyGenesisBlock);
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(hasNoRecentBlock);
|
||||
|
||||
// Disregard peers that have no block signature or the same block signature as us
|
||||
peers.removeIf(hasNoOrSameBlock);
|
||||
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
peers.removeIf(hasInferiorChainTip);
|
||||
|
||||
// Check we have enough peers to potentially synchronize
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
return;
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(peer -> peer.getLastBlockTimestamp() == null || peer.getLastBlockTimestamp() < minLatestBlockTimestamp);
|
||||
// Pick random peer to sync with
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
|
||||
BlockData latestBlockData = getChainTip();
|
||||
|
||||
// Disregard peers that have no block signature or the same block signature as us
|
||||
peers.removeIf(peer -> peer.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peer.getLastBlockSignature()));
|
||||
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
if (noSyncOurBlockSignature != null && Arrays.equals(noSyncOurBlockSignature, latestBlockData.getSignature()))
|
||||
peers.removeIf(peer -> Arrays.equals(noSyncPeerBlockSignature, peer.getLastBlockSignature()));
|
||||
|
||||
if (!peers.isEmpty()) {
|
||||
// Pick random peer to sync with
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
|
||||
actuallySynchronize(peer, false);
|
||||
}
|
||||
actuallySynchronize(peer, false);
|
||||
}
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
BlockData latestBlockData = getChainTip();
|
||||
|
||||
noSyncOurBlockSignature = null;
|
||||
noSyncPeerBlockSignature = null;
|
||||
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
case GENESIS_ONLY:
|
||||
case NO_COMMON_BLOCK:
|
||||
case TOO_FAR_BEHIND:
|
||||
case TOO_DIVERGENT:
|
||||
case INVALID_DATA:
|
||||
case INVALID_DATA: {
|
||||
// These are more serious results that warrant a cool-off
|
||||
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
||||
|
||||
@ -470,13 +494,22 @@ public class Controller extends Thread {
|
||||
LOGGER.warn("Repository issue while updating peer synchronization info", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case INFERIOR_CHAIN: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
case INFERIOR_CHAIN:
|
||||
noSyncOurBlockSignature = latestBlockData.getSignature();
|
||||
noSyncPeerBlockSignature = peer.getLastBlockSignature();
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
|
||||
// Notify peer of our superior chain
|
||||
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, latestBlockData)))
|
||||
peer.disconnect("failed to notify peer of our superior chain");
|
||||
break;
|
||||
}
|
||||
|
||||
case NO_REPLY:
|
||||
case NO_BLOCKCHAIN_LOCK:
|
||||
@ -488,14 +521,18 @@ public class Controller extends Thread {
|
||||
case OK:
|
||||
requestSysTrayUpdate = true;
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO:
|
||||
noSyncOurBlockSignature = latestBlockData.getSignature();
|
||||
noSyncPeerBlockSignature = peer.getLastBlockSignature();
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast our new chain tip (if changed)
|
||||
// Has our chain tip changed?
|
||||
BlockData newLatestBlockData;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@ -506,9 +543,14 @@ public class Controller extends Thread {
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
if (!Arrays.equals(newLatestBlockData.getSignature(), latestBlockData.getSignature()))
|
||||
if (!Arrays.equals(newLatestBlockData.getSignature(), latestBlockData.getSignature())) {
|
||||
// Broadcast our new chain tip
|
||||
Network.getInstance().broadcast(recipientPeer -> Network.getInstance().buildHeightMessage(recipientPeer, newLatestBlockData));
|
||||
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
@ -725,17 +767,9 @@ public class Controller extends Thread {
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
// We want to update atomically so use lock
|
||||
ReentrantLock peerLock = connectedPeer.getPeerDataLock();
|
||||
peerLock.lock();
|
||||
try {
|
||||
connectedPeer.setLastHeight(blockData.getHeight());
|
||||
connectedPeer.setLastBlockSignature(blockData.getSignature());
|
||||
connectedPeer.setLastBlockTimestamp(blockData.getTimestamp());
|
||||
connectedPeer.setLastBlockGenerator(blockData.getGeneratorPublicKey());
|
||||
} finally {
|
||||
peerLock.unlock();
|
||||
}
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(blockData.getHeight(), blockData.getSignature(), blockData.getTimestamp(), blockData.getGeneratorPublicKey());
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
|
||||
// Potentially synchronize
|
||||
@ -754,7 +788,9 @@ public class Controller extends Thread {
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
connectedPeer.setLastHeight(heightMessage.getHeight());
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightMessage.getHeight(), null, null, null);
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
|
||||
// Potentially synchronize
|
||||
@ -769,7 +805,7 @@ public class Controller extends Thread {
|
||||
// If peer is inbound and we've not updated their height
|
||||
// then this is probably their initial HEIGHT_V2 message
|
||||
// so they need a corresponding HEIGHT_V2 message from us
|
||||
if (!peer.isOutbound() && peer.getLastHeight() == null)
|
||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||
|
||||
// Update all peers with same ID
|
||||
@ -780,17 +816,9 @@ public class Controller extends Thread {
|
||||
if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId()))
|
||||
continue;
|
||||
|
||||
// We want to update atomically so use lock
|
||||
ReentrantLock peerLock = connectedPeer.getPeerDataLock();
|
||||
peerLock.lock();
|
||||
try {
|
||||
connectedPeer.setLastHeight(heightV2Message.getHeight());
|
||||
connectedPeer.setLastBlockSignature(heightV2Message.getSignature());
|
||||
connectedPeer.setLastBlockTimestamp(heightV2Message.getTimestamp());
|
||||
connectedPeer.setLastBlockGenerator(heightV2Message.getGenerator());
|
||||
} finally {
|
||||
peerLock.unlock();
|
||||
}
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getGenerator());
|
||||
connectedPeer.setChainTipData(newChainTipData);
|
||||
}
|
||||
|
||||
// Potentially synchronize
|
||||
@ -1168,24 +1196,24 @@ public class Controller extends Thread {
|
||||
case GET_ONLINE_ACCOUNTS: {
|
||||
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccount> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccount> accountsToSend;
|
||||
List<OnlineAccountData> accountsToSend;
|
||||
synchronized (this.onlineAccounts) {
|
||||
accountsToSend = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
|
||||
Iterator<OnlineAccount> iterator = accountsToSend.iterator();
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
|
||||
SEND_ITERATOR:
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccount onlineAccount = iterator.next();
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
for (int i = 0; i < excludeAccounts.size(); ++i) {
|
||||
OnlineAccount excludeAccount = excludeAccounts.get(i);
|
||||
OnlineAccountData excludeAccountData = excludeAccounts.get(i);
|
||||
|
||||
if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.getPublicKey())) {
|
||||
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
|
||||
iterator.remove();
|
||||
continue SEND_ITERATOR;
|
||||
}
|
||||
@ -1203,11 +1231,11 @@ public class Controller extends Thread {
|
||||
case ONLINE_ACCOUNTS: {
|
||||
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccount> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.trace(() -> String.format("Received %d online accounts from %s", onlineAccounts.size(), peer));
|
||||
|
||||
for (OnlineAccount onlineAccount : onlineAccounts)
|
||||
this.verifyAndAddAccount(onlineAccount);
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts)
|
||||
this.verifyAndAddAccount(onlineAccountData);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -1220,35 +1248,35 @@ public class Controller extends Thread {
|
||||
|
||||
// Utilities
|
||||
|
||||
private void verifyAndAddAccount(OnlineAccount onlineAccount) {
|
||||
private void verifyAndAddAccount(OnlineAccountData onlineAccountData) {
|
||||
// We would check timestamp is 'recent' here
|
||||
|
||||
// Verify
|
||||
byte[] data = Longs.toByteArray(onlineAccount.getTimestamp());
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
|
||||
if (!otherAccount.verify(onlineAccount.getSignature(), data)) {
|
||||
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccountData.getPublicKey());
|
||||
if (!otherAccount.verify(onlineAccountData.getSignature(), data)) {
|
||||
LOGGER.trace(() -> String.format("Rejecting invalid online account %s", otherAccount.getAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccount.getPublicKey())).findFirst().orElse(null);
|
||||
OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null);
|
||||
|
||||
if (existingAccount != null) {
|
||||
if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) {
|
||||
this.onlineAccounts.remove(existingAccount);
|
||||
if (existingAccountData != null) {
|
||||
if (existingAccountData.getTimestamp() < onlineAccountData.getTimestamp()) {
|
||||
this.onlineAccounts.remove(existingAccountData);
|
||||
|
||||
LOGGER.trace(() -> String.format("Updated online account %s with timestamp %d (was %d)", otherAccount.getAddress(), onlineAccount.getTimestamp(), existingAccount.getTimestamp()));
|
||||
LOGGER.trace(() -> String.format("Updated online account %s with timestamp %d (was %d)", otherAccount.getAddress(), onlineAccountData.getTimestamp(), existingAccountData.getTimestamp()));
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Not updating existing online account %s", otherAccount.getAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp()));
|
||||
LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp()));
|
||||
}
|
||||
|
||||
this.onlineAccounts.add(onlineAccount);
|
||||
this.onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1260,16 +1288,16 @@ public class Controller extends Thread {
|
||||
// Expire old entries
|
||||
final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD;
|
||||
synchronized (this.onlineAccounts) {
|
||||
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
|
||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccount onlineAccount = iterator.next();
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
if (onlineAccount.getTimestamp() < cutoffThreshold) {
|
||||
if (onlineAccountData.getTimestamp() < cutoffThreshold) {
|
||||
iterator.remove();
|
||||
|
||||
LOGGER.trace(() -> {
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
|
||||
return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp());
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccountData.getPublicKey());
|
||||
return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1322,7 +1350,7 @@ public class Controller extends Thread {
|
||||
boolean hasInfoChanged = false;
|
||||
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
List<OnlineAccount> ourOnlineAccounts = new ArrayList<>();
|
||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||
|
||||
FORGING_ACCOUNTS:
|
||||
for (ForgingAccountData forgingAccountData : forgingAccounts) {
|
||||
@ -1332,28 +1360,28 @@ public class Controller extends Thread {
|
||||
byte[] publicKey = forgingAccount.getPublicKey();
|
||||
|
||||
// Our account is online
|
||||
OnlineAccount onlineAccount = new OnlineAccount(onlineAccountsTimestamp, signature, publicKey);
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
synchronized (this.onlineAccounts) {
|
||||
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
|
||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccount account = iterator.next();
|
||||
OnlineAccountData existingOnlineAccountData = iterator.next();
|
||||
|
||||
if (Arrays.equals(account.getPublicKey(), forgingAccount.getPublicKey())) {
|
||||
// If onlineAccount is already present, with same timestamp, then move on to next forgingAccount
|
||||
if (account.getTimestamp() == onlineAccountsTimestamp)
|
||||
if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) {
|
||||
// If our online account is already present, with same timestamp, then move on to next forgingAccount
|
||||
if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp)
|
||||
continue FORGING_ACCOUNTS;
|
||||
|
||||
// If onlineAccount is already present, but with older timestamp, then remove it
|
||||
// If our online account is already present, but with older timestamp, then remove it
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.onlineAccounts.add(onlineAccount);
|
||||
this.onlineAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", forgingAccount.getAddress(), onlineAccountsTimestamp));
|
||||
ourOnlineAccounts.add(onlineAccount);
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
hasInfoChanged = true;
|
||||
}
|
||||
|
||||
@ -1370,7 +1398,7 @@ public class Controller extends Thread {
|
||||
return (timestamp / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
|
||||
}
|
||||
|
||||
public List<OnlineAccount> getOnlineAccounts() {
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
final long onlineTimestamp = Controller.toOnlineAccountTimestamp(NTP.getTime());
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
@ -1423,45 +1451,93 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
public static final Predicate<Peer> hasPeerMisbehaved = peer -> {
|
||||
Long lastMisbehaved = peer.getPeerData().getLastMisbehaved();
|
||||
return lastMisbehaved != null && lastMisbehaved > NTP.getTime() - MISBEHAVIOUR_COOLOFF;
|
||||
};
|
||||
/** Returns a list of peers that are not misbehaving, and have a recent block. */
|
||||
public List<Peer> getRecentBehavingPeers() {
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return null;
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
|
||||
// Filter out unsuitable peers
|
||||
Iterator<Peer> iterator = peers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final Peer peer = iterator.next();
|
||||
|
||||
final PeerData peerData = peer.getPeerData();
|
||||
if (peerData == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
if (hasMisbehaved.test(peer)) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
if (peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return peers;
|
||||
}
|
||||
|
||||
/** Returns whether we think our node has up-to-date blockchain based on our info about other peers. */
|
||||
public boolean isUpToDate() {
|
||||
// Do we even have a vaguely recent block?
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return false;
|
||||
|
||||
BlockData latestBlockData = getChainTip();
|
||||
|
||||
// Is our blockchain too old?
|
||||
if (latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
final BlockData latestBlockData = getChainTip();
|
||||
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
return false;
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(hasPeerMisbehaved);
|
||||
|
||||
// Check we have enough peers to potentially synchronize/generator
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
if (peers == null)
|
||||
return false;
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(hasMisbehaved);
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(peer -> peer.getLastBlockTimestamp() == null || peer.getLastBlockTimestamp() < minLatestBlockTimestamp);
|
||||
peers.removeIf(hasNoRecentBlock);
|
||||
|
||||
// Check we have enough peers to potentially synchronize/generate
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
return false;
|
||||
|
||||
// If we don't have any peers left then can't synchronize, therefore consider ourself not up to date
|
||||
return !peers.isEmpty();
|
||||
}
|
||||
|
||||
/** Returns minimum block timestamp for block to be considered 'recent', or <tt>null</tt> if NTP not synced. */
|
||||
public static Long getMinimumLatestBlockTimestamp() {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return null;
|
||||
|
||||
return now - BlockChain.getInstance().getMaxBlockTime() * 1000L * MAX_BLOCKCHAIN_TIP_AGE;
|
||||
int height = getInstance().getChainHeight();
|
||||
if (height == 0)
|
||||
return null;
|
||||
|
||||
long offset = 0;
|
||||
for (int ai = 0; height >= 1 && ai < MAX_BLOCKCHAIN_TIP_AGE; ++ai, --height) {
|
||||
BlockTimingByHeight blockTiming = BlockChain.getInstance().getBlockTimingByHeight(height);
|
||||
offset += blockTiming.target + blockTiming.deviation;
|
||||
}
|
||||
|
||||
return now - offset;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package org.qora.controller;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -10,6 +14,7 @@ import org.qora.block.Block;
|
||||
import org.qora.block.Block.ValidationResult;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.block.BlockSummaryData;
|
||||
import org.qora.data.network.PeerChainTipData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.network.Peer;
|
||||
import org.qora.network.message.BlockMessage;
|
||||
@ -33,16 +38,13 @@ public class Synchronizer {
|
||||
|
||||
private static final int INITIAL_BLOCK_STEP = 8;
|
||||
private static final int MAXIMUM_BLOCK_STEP = 500;
|
||||
private static final int MAXIMUM_HEIGHT_DELTA = 300; // XXX move to blockchain config?
|
||||
private static final int MAXIMUM_COMMON_DELTA = 60; // XXX move to blockchain config?
|
||||
private static final int MAXIMUM_COMMON_DELTA = 1440; // XXX move to Settings?
|
||||
private static final int SYNC_BATCH_SIZE = 200;
|
||||
|
||||
private static Synchronizer instance;
|
||||
|
||||
private Repository repository;
|
||||
|
||||
public enum SynchronizationResult {
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_FAR_BEHIND, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE;
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE;
|
||||
}
|
||||
|
||||
// Constructors
|
||||
@ -78,55 +80,34 @@ public class Synchronizer {
|
||||
try {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try {
|
||||
this.repository = repository;
|
||||
final BlockData ourLatestBlockData = this.repository.getBlockRepository().getLastBlock();
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||
int ourHeight = ourInitialHeight;
|
||||
|
||||
int peerHeight;
|
||||
byte[] peersLastBlockSignature;
|
||||
|
||||
ReentrantLock peerLock = peer.getPeerDataLock();
|
||||
peerLock.lockInterruptibly();
|
||||
try {
|
||||
peerHeight = peer.getLastHeight();
|
||||
peersLastBlockSignature = peer.getLastBlockSignature();
|
||||
} finally {
|
||||
peerLock.unlock();
|
||||
}
|
||||
|
||||
// If peer is at genesis block then peer has no blocks so ignore them for a while
|
||||
if (peerHeight == 1)
|
||||
return SynchronizationResult.GENESIS_ONLY;
|
||||
|
||||
// If peer is too far behind us then don't them.
|
||||
int minHeight = ourHeight - MAXIMUM_HEIGHT_DELTA;
|
||||
if (!force && peerHeight < minHeight) {
|
||||
LOGGER.info(String.format("Peer %s height %d is too far behind our height %d", peer, peerHeight, ourHeight));
|
||||
return SynchronizationResult.TOO_FAR_BEHIND;
|
||||
}
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getLastHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
|
||||
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
||||
LOGGER.debug(String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getLastBlockTimestamp(),
|
||||
ourHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||
|
||||
List<byte[]> signatures = findSignaturesFromCommonBlock(peer, ourHeight);
|
||||
if (signatures == null) {
|
||||
List<BlockSummaryData> peerBlockSummaries = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight);
|
||||
if (peerBlockSummaries == null) {
|
||||
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
if (peerBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Failure to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_COMMON_BLOCK;
|
||||
}
|
||||
|
||||
// First signature is common block
|
||||
BlockData commonBlockData = this.repository.getBlockRepository().fromSignature(signatures.get(0));
|
||||
// First summary is common block
|
||||
BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
|
||||
final int commonBlockHeight = commonBlockData.getHeight();
|
||||
LOGGER.debug(String.format("Common block with peer %s is at height %d, sig %.8s, ts %d", peer,
|
||||
commonBlockHeight, Base58.encode(commonBlockData.getSignature()), commonBlockData.getTimestamp()));
|
||||
signatures.remove(0);
|
||||
peerBlockSummaries.remove(0);
|
||||
|
||||
// If common block height is higher than peer's last reported height
|
||||
// then peer must have a very recent sync. Update our idea of peer's height.
|
||||
@ -137,7 +118,7 @@ public class Synchronizer {
|
||||
|
||||
// If common block is peer's latest block then we simply have the same, or longer, chain to peer, so exit now
|
||||
if (commonBlockHeight == peerHeight) {
|
||||
if (peerHeight == ourHeight)
|
||||
if (peerHeight == ourInitialHeight)
|
||||
LOGGER.debug(String.format("We have the same blockchain as peer %s", peer));
|
||||
else
|
||||
LOGGER.debug(String.format("We have the same blockchain as peer %s, but longer", peer));
|
||||
@ -146,14 +127,13 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
// If common block is too far behind us then we're on massively different forks so give up.
|
||||
int minCommonHeight = ourHeight - MAXIMUM_COMMON_DELTA;
|
||||
int minCommonHeight = ourInitialHeight - MAXIMUM_COMMON_DELTA;
|
||||
if (!force && commonBlockHeight < minCommonHeight) {
|
||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
// If we both have blocks after common block then decide whether we want to sync
|
||||
int highestMutualHeight = Math.min(peerHeight, ourHeight);
|
||||
// At this point, we both have blocks after common block
|
||||
|
||||
// If our latest block is very old, we're very behind and should ditch our fork.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
@ -162,45 +142,63 @@ public class Synchronizer {
|
||||
|
||||
if (ourInitialHeight > commonBlockHeight && ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
|
||||
highestMutualHeight = commonBlockHeight;
|
||||
}
|
||||
} else if (!force) {
|
||||
// Compare chain weights
|
||||
|
||||
for (int height = commonBlockHeight + 1; height <= highestMutualHeight; ++height) {
|
||||
int sigIndex = height - commonBlockHeight - 1;
|
||||
LOGGER.debug(String.format("Comparing chains from block %d with peer %s", commonBlockHeight + 1, peer));
|
||||
|
||||
// Do we need more signatures?
|
||||
if (signatures.size() - 1 < sigIndex) {
|
||||
// Grab more signatures
|
||||
byte[] previousSignature = sigIndex == 0 ? commonBlockData.getSignature() : signatures.get(sigIndex - 1);
|
||||
List<byte[]> moreSignatures = this.getBlockSignatures(peer, previousSignature, MAXIMUM_BLOCK_STEP);
|
||||
if (moreSignatures == null || moreSignatures.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
height - 1, Base58.encode(previousSignature)));
|
||||
// Fetch remaining peer's block summaries (which we also use to fill signatures list)
|
||||
int peerBlockCount = peerHeight - commonBlockHeight;
|
||||
byte[] previousSignature;
|
||||
if (peerBlockSummaries.isEmpty())
|
||||
previousSignature = commonBlockData.getSignature();
|
||||
else
|
||||
previousSignature = peerBlockSummaries.get(peerBlockSummaries.size() - 1).getSignature();
|
||||
|
||||
while (peerBlockSummaries.size() < peerBlockCount) {
|
||||
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
|
||||
|
||||
List<BlockSummaryData> moreBlockSummaries = this.getBlockSummaries(peer, previousSignature, peerBlockCount - peerBlockSummaries.size());
|
||||
|
||||
if (moreBlockSummaries == null || moreBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block summaries after height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(previousSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
signatures.addAll(moreSignatures);
|
||||
}
|
||||
// Check peer sent valid heights
|
||||
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
|
||||
++lastSummaryHeight;
|
||||
|
||||
byte[] ourSignature = this.repository.getBlockRepository().fromHeight(height).getSignature();
|
||||
byte[] peerSignature = signatures.get(sigIndex);
|
||||
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
|
||||
|
||||
for (int i = 0; i < ourSignature.length; ++i) {
|
||||
/*
|
||||
* If our byte is lower, we don't synchronize with this peer,
|
||||
* if their byte is lower, check next height,
|
||||
* (if bytes are equal, try next byte).
|
||||
*/
|
||||
if (ourSignature[i] < peerSignature[i]) {
|
||||
LOGGER.info(String.format("Not synchronizing with peer %s as we have better block at height %d", peer, height));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
if (blockSummary.getHeight() != lastSummaryHeight) {
|
||||
LOGGER.info(String.format("Peer %s responded with invalid block summary for height %d, sig %.8s", peer,
|
||||
lastSummaryHeight, Base58.encode(blockSummary.getSignature())));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
}
|
||||
|
||||
if (peerSignature[i] < ourSignature[i])
|
||||
break;
|
||||
peerBlockSummaries.addAll(moreBlockSummaries);
|
||||
}
|
||||
|
||||
// Fetch our corresponding block summaries
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourInitialHeight);
|
||||
|
||||
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockData.getSignature(), ourBlockSummaries);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockData.getSignature(), peerBlockSummaries);
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
}
|
||||
}
|
||||
|
||||
int ourHeight = ourInitialHeight;
|
||||
if (ourHeight > commonBlockHeight) {
|
||||
// Unwind to common block (unless common block is our latest block)
|
||||
LOGGER.debug(String.format("Orphaning blocks back to height %d", commonBlockHeight));
|
||||
@ -219,40 +217,44 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
// Fetch, and apply, blocks from peer
|
||||
byte[] signature = commonBlockData.getSignature();
|
||||
byte[] latestPeerSignature = commonBlockData.getSignature();
|
||||
int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE;
|
||||
|
||||
// Convert any block summaries from above into signatures to request from peer
|
||||
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(blockSummaryData -> blockSummaryData.getSignature()).collect(Collectors.toList());
|
||||
|
||||
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
|
||||
// Do we need more signatures?
|
||||
if (signatures.isEmpty()) {
|
||||
if (peerBlockSummaries.isEmpty()) {
|
||||
int numberRequested = maxBatchHeight - ourHeight;
|
||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d", numberRequested, (numberRequested != 1 ? "s": ""), ourHeight));
|
||||
|
||||
signatures = this.getBlockSignatures(peer, signature, numberRequested);
|
||||
peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested);
|
||||
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
if (peerBlockSummaries == null || peerBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(signature)));
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
LOGGER.trace(String.format("Received %s signature%s", signatures.size(), (signatures.size() != 1 ? "s" : "")));
|
||||
LOGGER.trace(String.format("Received %s signature%s", peerBlockSummaries.size(), (peerBlockSummaries.size() != 1 ? "s" : "")));
|
||||
}
|
||||
|
||||
signature = signatures.get(0);
|
||||
signatures.remove(0);
|
||||
latestPeerSignature = peerBlockSignatures.get(0);
|
||||
peerBlockSignatures.remove(0);
|
||||
++ourHeight;
|
||||
|
||||
Block newBlock = this.fetchBlock(repository, peer, signature);
|
||||
Block newBlock = this.fetchBlock(repository, peer, latestPeerSignature);
|
||||
|
||||
if (newBlock == null) {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(signature)));
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
if (!newBlock.isSignatureValid()) {
|
||||
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
|
||||
ourHeight, Base58.encode(signature)));
|
||||
ourHeight, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
@ -263,7 +265,7 @@ public class Synchronizer {
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
ourHeight, Base58.encode(signature), blockResult.name()));
|
||||
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
@ -283,7 +285,7 @@ public class Synchronizer {
|
||||
// Commit
|
||||
repository.saveChanges();
|
||||
|
||||
final BlockData newLatestBlockData = this.repository.getBlockRepository().getLastBlock();
|
||||
final BlockData newLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
LOGGER.info(String.format("Synchronized with peer %s to height %d, sig %.8s, ts: %d", peer,
|
||||
newLatestBlockData.getHeight(), Base58.encode(newLatestBlockData.getSignature()),
|
||||
newLatestBlockData.getTimestamp()));
|
||||
@ -291,7 +293,6 @@ public class Synchronizer {
|
||||
return SynchronizationResult.OK;
|
||||
} finally {
|
||||
repository.discardChanges(); // Free repository locks, if any, also in case anything went wrong
|
||||
this.repository = null;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue during synchronization with peer", e);
|
||||
@ -303,45 +304,45 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of peer's block signatures starting with common block with peer.
|
||||
* Returns list of peer's block summaries starting with common block with peer.
|
||||
*
|
||||
* @param peer
|
||||
* @return block signatures, or empty list if no common block, or null if there was an issue
|
||||
* @return block summaries, or empty list if no common block, or null if there was an issue
|
||||
* @throws DataException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private List<byte[]> findSignaturesFromCommonBlock(Peer peer, int ourHeight) throws DataException, InterruptedException {
|
||||
private List<BlockSummaryData> fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight) throws DataException, InterruptedException {
|
||||
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
||||
// Failing that, back off exponentially
|
||||
int step = INITIAL_BLOCK_STEP;
|
||||
|
||||
List<byte[]> blockSignatures = null;
|
||||
List<BlockSummaryData> blockSummaries = null;
|
||||
|
||||
int testHeight = Math.max(ourHeight - step, 1);
|
||||
byte[] testSignature = null;
|
||||
BlockData testBlockData = null;
|
||||
|
||||
while (testHeight >= 1) {
|
||||
// Fetch our block signature at this height
|
||||
BlockData testBlockData = this.repository.getBlockRepository().fromHeight(testHeight);
|
||||
testBlockData = repository.getBlockRepository().fromHeight(testHeight);
|
||||
if (testBlockData == null) {
|
||||
// Not found? But we've locked the blockchain and height is below blockchain's tip!
|
||||
LOGGER.error("Failed to get block at height lower than blockchain tip during synchronization?");
|
||||
return null;
|
||||
}
|
||||
|
||||
testSignature = testBlockData.getSignature();
|
||||
|
||||
// Ask for block signatures since test block's signature
|
||||
LOGGER.trace(String.format("Requesting %d signature%s after height %d", step, (step != 1 ? "s": ""), testHeight));
|
||||
blockSignatures = this.getBlockSignatures(peer, testSignature, step);
|
||||
byte[] testSignature = testBlockData.getSignature();
|
||||
LOGGER.trace(String.format("Requesting %d summar%s after height %d", step, (step != 1 ? "ies": "y"), testHeight));
|
||||
blockSummaries = this.getBlockSummaries(peer, testSignature, step);
|
||||
|
||||
if (blockSignatures == null)
|
||||
if (blockSummaries == null)
|
||||
// No response - give up this time
|
||||
return null;
|
||||
|
||||
LOGGER.trace(String.format("Received %s signature%s", blockSignatures.size(), (blockSignatures.size() != 1 ? "s" : "")));
|
||||
LOGGER.trace(String.format("Received %s summar%s", blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
|
||||
|
||||
// Empty list means remote peer is unaware of test signature OR has no new blocks after test signature
|
||||
if (!blockSignatures.isEmpty())
|
||||
if (!blockSummaries.isEmpty())
|
||||
// We have entries so we have found a common block
|
||||
break;
|
||||
|
||||
@ -361,22 +362,22 @@ public class Synchronizer {
|
||||
testHeight = Math.max(testHeight - step, 1);
|
||||
}
|
||||
|
||||
// Prepend common block's signature as first block sig
|
||||
blockSignatures.add(0, testSignature);
|
||||
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||
blockSummaries.add(0, testBlockSummary);
|
||||
|
||||
// Work through returned signatures to get closer common block
|
||||
// Do this by trimming all-but-one leading known signatures
|
||||
for (int i = blockSignatures.size() - 1; i > 0; --i) {
|
||||
BlockData blockData = this.repository.getBlockRepository().fromSignature(blockSignatures.get(i));
|
||||
|
||||
if (blockData != null) {
|
||||
// Trim summaries so that first summary is common block.
|
||||
// Currently we work back from the end until we hit a block we also have.
|
||||
// TODO: rewrite as modified binary search!
|
||||
for (int i = blockSummaries.size() - 1; i > 0; --i) {
|
||||
if (repository.getBlockRepository().exists(blockSummaries.get(i).getSignature())) {
|
||||
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
||||
blockSignatures.subList(0, i).clear();
|
||||
blockSummaries.subList(0, i).clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return blockSignatures;
|
||||
return blockSummaries;
|
||||
}
|
||||
|
||||
private List<BlockSummaryData> getBlockSummaries(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
||||
|
@ -8,7 +8,7 @@ import org.qora.account.PublicKeyAccount;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class OnlineAccount {
|
||||
public class OnlineAccountData {
|
||||
|
||||
protected long timestamp;
|
||||
protected byte[] signature;
|
||||
@ -17,10 +17,10 @@ public class OnlineAccount {
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB serialization
|
||||
protected OnlineAccount() {
|
||||
protected OnlineAccountData() {
|
||||
}
|
||||
|
||||
public OnlineAccount(long timestamp, byte[] signature, byte[] publicKey) {
|
||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
||||
this.timestamp = timestamp;
|
||||
this.signature = signature;
|
||||
this.publicKey = publicKey;
|
37
src/main/java/org/qora/data/network/PeerChainTipData.java
Normal file
37
src/main/java/org/qora/data/network/PeerChainTipData.java
Normal file
@ -0,0 +1,37 @@
|
||||
package org.qora.data.network;
|
||||
|
||||
public class PeerChainTipData {
|
||||
|
||||
/** Latest block height as reported by peer. */
|
||||
private Integer lastHeight;
|
||||
/** Latest block signature as reported by peer. */
|
||||
private byte[] lastBlockSignature;
|
||||
/** Latest block timestamp as reported by peer. */
|
||||
private Long lastBlockTimestamp;
|
||||
/** Latest block generator public key as reported by peer. */
|
||||
private byte[] lastBlockGenerator;
|
||||
|
||||
public PeerChainTipData(Integer lastHeight, byte[] lastBlockSignature, Long lastBlockTimestamp, byte[] lastBlockGenerator) {
|
||||
this.lastHeight = lastHeight;
|
||||
this.lastBlockSignature = lastBlockSignature;
|
||||
this.lastBlockTimestamp = lastBlockTimestamp;
|
||||
this.lastBlockGenerator = lastBlockGenerator;
|
||||
}
|
||||
|
||||
public Integer getLastHeight() {
|
||||
return this.lastHeight;
|
||||
}
|
||||
|
||||
public byte[] getLastBlockSignature() {
|
||||
return this.lastBlockSignature;
|
||||
}
|
||||
|
||||
public Long getLastBlockTimestamp() {
|
||||
return this.lastBlockTimestamp;
|
||||
}
|
||||
|
||||
public byte[] getLastBlockGenerator() {
|
||||
return this.lastBlockGenerator;
|
||||
}
|
||||
|
||||
}
|
@ -17,11 +17,11 @@ import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.controller.Controller;
|
||||
import org.qora.data.network.PeerChainTipData;
|
||||
import org.qora.data.network.PeerData;
|
||||
import org.qora.network.message.Message;
|
||||
import org.qora.network.message.Message.MessageException;
|
||||
@ -77,7 +77,6 @@ public class Peer {
|
||||
private volatile byte[] verificationCodeExpected;
|
||||
|
||||
private volatile PeerData peerData = null;
|
||||
private final ReentrantLock peerDataLock = new ReentrantLock();
|
||||
|
||||
/** Timestamp of when socket was accepted, or connected. */
|
||||
private volatile Long connectionTimestamp = null;
|
||||
@ -93,17 +92,8 @@ public class Peer {
|
||||
/** When last PING message was sent, or null if pings not started yet. */
|
||||
private volatile Long lastPingSent;
|
||||
|
||||
/** Latest block height as reported by peer. */
|
||||
private volatile Integer lastHeight;
|
||||
|
||||
/** Latest block signature as reported by peer. */
|
||||
private volatile byte[] lastBlockSignature;
|
||||
|
||||
/** Latest block timestamp as reported by peer. */
|
||||
private volatile Long lastBlockTimestamp;
|
||||
|
||||
/** Latest block generator public key as reported by peer. */
|
||||
private volatile byte[] lastBlockGenerator;
|
||||
/** Latest block info as reported by peer. */
|
||||
private volatile PeerChainTipData chainTipData;
|
||||
|
||||
// Constructors
|
||||
|
||||
@ -231,41 +221,12 @@ public class Peer {
|
||||
this.verificationCodeExpected = expected;
|
||||
}
|
||||
|
||||
public Integer getLastHeight() {
|
||||
return this.lastHeight;
|
||||
public PeerChainTipData getChainTipData() {
|
||||
return this.chainTipData;
|
||||
}
|
||||
|
||||
public void setLastHeight(Integer lastHeight) {
|
||||
this.lastHeight = lastHeight;
|
||||
}
|
||||
|
||||
public byte[] getLastBlockSignature() {
|
||||
return lastBlockSignature;
|
||||
}
|
||||
|
||||
public void setLastBlockSignature(byte[] lastBlockSignature) {
|
||||
this.lastBlockSignature = lastBlockSignature;
|
||||
}
|
||||
|
||||
public Long getLastBlockTimestamp() {
|
||||
return lastBlockTimestamp;
|
||||
}
|
||||
|
||||
public void setLastBlockTimestamp(Long lastBlockTimestamp) {
|
||||
this.lastBlockTimestamp = lastBlockTimestamp;
|
||||
}
|
||||
|
||||
public byte[] getLastBlockGenerator() {
|
||||
return lastBlockGenerator;
|
||||
}
|
||||
|
||||
public void setLastBlockGenerator(byte[] lastBlockGenerator) {
|
||||
this.lastBlockGenerator = lastBlockGenerator;
|
||||
}
|
||||
|
||||
/** Returns the lock used for synchronizing access to peer info. */
|
||||
public ReentrantLock getPeerDataLock() {
|
||||
return this.peerDataLock;
|
||||
public void setChainTipData(PeerChainTipData chainTipData) {
|
||||
this.chainTipData = chainTipData;
|
||||
}
|
||||
|
||||
/* package */ void queueMessage(Message message) {
|
||||
@ -334,13 +295,14 @@ public class Peer {
|
||||
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed())
|
||||
return;
|
||||
|
||||
int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||
if (bytesRead == -1) {
|
||||
this.disconnect("EOF");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Received %d bytes from peer %s", bytesRead, this));
|
||||
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
||||
|
||||
while (true) {
|
||||
final Message message;
|
||||
@ -354,8 +316,8 @@ public class Peer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message == null && bytesRead == 0)
|
||||
// No complete message in buffer and no more bytes to read from socket
|
||||
if (message == null && bytesRead == 0 && !wasByteBufferFull)
|
||||
// No complete message in buffer, no more bytes to read from socket and there was room to read bytes
|
||||
return;
|
||||
|
||||
if (message == null)
|
||||
@ -380,7 +342,8 @@ public class Peer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prematurely end any blocking channel select so that new messages can be processed
|
||||
// Prematurely end any blocking channel select so that new messages can be processed.
|
||||
// This might cause this.socketChannel.read() above to return zero into bytesRead.
|
||||
Network.getInstance().wakeupChannelSelector();
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.transform.Transformer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
@ -16,19 +16,19 @@ import com.google.common.primitives.Longs;
|
||||
public class GetOnlineAccountsMessage extends Message {
|
||||
private static final int MAX_ACCOUNT_COUNT = 1000;
|
||||
|
||||
private List<OnlineAccount> onlineAccounts;
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
|
||||
public GetOnlineAccountsMessage(List<OnlineAccount> onlineAccounts) {
|
||||
public GetOnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
}
|
||||
|
||||
private GetOnlineAccountsMessage(int id, List<OnlineAccount> onlineAccounts) {
|
||||
private GetOnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.GET_ONLINE_ACCOUNTS);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccount> getOnlineAccounts() {
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
if (accountCount > MAX_ACCOUNT_COUNT)
|
||||
return null;
|
||||
|
||||
List<OnlineAccount> onlineAccounts = new ArrayList<>(accountCount);
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
long timestamp = bytes.getLong();
|
||||
@ -46,7 +46,7 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
onlineAccounts.add(new OnlineAccount(timestamp, null, publicKey));
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey));
|
||||
}
|
||||
|
||||
return new GetOnlineAccountsMessage(id, onlineAccounts);
|
||||
@ -60,10 +60,10 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccount onlineAccount = this.onlineAccounts.get(i);
|
||||
bytes.write(Longs.toByteArray(onlineAccount.getTimestamp()));
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
|
||||
|
||||
bytes.write(onlineAccount.getPublicKey());
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
|
@ -7,7 +7,7 @@ import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.transform.Transformer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
@ -16,19 +16,19 @@ import com.google.common.primitives.Longs;
|
||||
public class OnlineAccountsMessage extends Message {
|
||||
private static final int MAX_ACCOUNT_COUNT = 1000;
|
||||
|
||||
private List<OnlineAccount> onlineAccounts;
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
|
||||
public OnlineAccountsMessage(List<OnlineAccount> onlineAccounts) {
|
||||
public OnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
}
|
||||
|
||||
private OnlineAccountsMessage(int id, List<OnlineAccount> onlineAccounts) {
|
||||
private OnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.ONLINE_ACCOUNTS);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccount> getOnlineAccounts() {
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ public class OnlineAccountsMessage extends Message {
|
||||
if (accountCount > MAX_ACCOUNT_COUNT)
|
||||
return null;
|
||||
|
||||
List<OnlineAccount> onlineAccounts = new ArrayList<>(accountCount);
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
long timestamp = bytes.getLong();
|
||||
@ -49,8 +49,8 @@ public class OnlineAccountsMessage extends Message {
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey);
|
||||
onlineAccounts.add(onlineAccount);
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(timestamp, signature, publicKey);
|
||||
onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
|
||||
return new OnlineAccountsMessage(id, onlineAccounts);
|
||||
@ -64,13 +64,13 @@ public class OnlineAccountsMessage extends Message {
|
||||
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccount onlineAccount = this.onlineAccounts.get(i);
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
|
||||
bytes.write(Longs.toByteArray(onlineAccount.getTimestamp()));
|
||||
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
|
||||
|
||||
bytes.write(onlineAccount.getSignature());
|
||||
bytes.write(onlineAccountData.getSignature());
|
||||
|
||||
bytes.write(onlineAccount.getPublicKey());
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
|
@ -76,7 +76,7 @@ public class Settings {
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
private Integer listenPort;
|
||||
/** Minimum number of peers to allow block generation / synchronization. */
|
||||
private int minBlockchainPeers = 3;
|
||||
private int minBlockchainPeers = 5;
|
||||
/** Target number of outbound connections to peers we should make. */
|
||||
private int minOutboundPeers = 20;
|
||||
/** Maximum number of peer connections we allow. */
|
||||
@ -128,6 +128,21 @@ public class Settings {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse settings from given file.
|
||||
* <p>
|
||||
* Throws <tt>RuntimeException</tt> with <tt>UnmarshalException</tt> as cause if settings file could not be parsed.
|
||||
* <p>
|
||||
* We use <tt>RuntimeException</tt> because it can be caught first caller of {@link #getInstance()} above,
|
||||
* but it's not necessary to surround later {@link #getInstance()} calls
|
||||
* with <tt>try-catch</tt> as they should be read-only.
|
||||
*
|
||||
* @param filename
|
||||
* @throws RuntimeException with UnmarshalException as cause if settings file could not be parsed
|
||||
* @throws RuntimeException with FileNotFoundException as cause if settings file could not be found/opened
|
||||
* @throws RuntimeException with JAXBException as cause if some unexpected JAXB-related error occurred
|
||||
* @throws RuntimeException with IOException as cause if some unexpected I/O-related error occurred
|
||||
*/
|
||||
public static void fileInstance(String filename) {
|
||||
JAXBContext jc;
|
||||
Unmarshaller unmarshaller;
|
||||
@ -147,8 +162,9 @@ public class Settings {
|
||||
// Tell unmarshaller that there's no JSON root element in the JSON input
|
||||
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
} catch (JAXBException e) {
|
||||
LOGGER.error("Unable to process settings file", e);
|
||||
throw new RuntimeException("Unable to process settings file", e);
|
||||
String message = "Failed to setup unmarshaller to process settings file";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
|
||||
Settings settings = null;
|
||||
@ -164,24 +180,28 @@ public class Settings {
|
||||
// Attempt to unmarshal JSON stream to Settings
|
||||
settings = unmarshaller.unmarshal(json, Settings.class).getValue();
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.error("Settings file not found: " + path + filename);
|
||||
throw new RuntimeException("Settings file not found: " + path + filename);
|
||||
String message = "Settings file not found: " + path + filename;
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
} catch (UnmarshalException e) {
|
||||
Throwable linkedException = e.getLinkedException();
|
||||
if (linkedException instanceof XMLMarshalException) {
|
||||
String message = ((XMLMarshalException) linkedException).getInternalException().getLocalizedMessage();
|
||||
LOGGER.error(message);
|
||||
throw new RuntimeException(message);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
|
||||
LOGGER.error("Unable to process settings file", e);
|
||||
throw new RuntimeException("Unable to process settings file", e);
|
||||
String message = "Failed to parse settings file";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
} catch (JAXBException e) {
|
||||
LOGGER.error("Unable to process settings file", e);
|
||||
throw new RuntimeException("Unable to process settings file", e);
|
||||
String message = "Unexpected JAXB issue while processing settings file";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unable to process settings file", e);
|
||||
throw new RuntimeException("Unable to process settings file", e);
|
||||
String message = "Unexpected I/O issue while processing settings file";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
|
||||
if (settings.userPath != null) {
|
||||
@ -207,8 +227,14 @@ public class Settings {
|
||||
BlockChain.fileInstance(settings.getUserPath(), settings.getBlockchainConfig());
|
||||
}
|
||||
|
||||
public static void throwValidationError(String message) {
|
||||
throw new RuntimeException(message, new UnmarshalException(message));
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
// Validation goes here
|
||||
if (this.minBlockchainPeers < 1)
|
||||
throwValidationError("minBlockchainPeers must be at least 1");
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
@ -4,6 +4,8 @@
|
||||
"minBlockTime": 60,
|
||||
"maxBlockTime": 300,
|
||||
"blockTimestampMargin": 2000,
|
||||
"transactionExpiryPeriod": 86400000,
|
||||
"maxBlockSize": 1048576,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "1.0",
|
||||
"useBrokenMD160ForAddresses": false,
|
||||
@ -15,7 +17,7 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 3196800000,
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": "1568720000000",
|
||||
"timestamp": "1569510000000",
|
||||
"generatingBalance": "100000",
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 10000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
|
||||
|
134
src/test/java/org/qora/test/ChainWeightTests.java
Normal file
134
src/test/java/org/qora/test/ChainWeightTests.java
Normal file
@ -0,0 +1,134 @@
|
||||
package org.qora.test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.block.BlockSummaryData;
|
||||
import org.qora.transform.Transformer;
|
||||
import org.qora.transform.block.BlockTransformer;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public class ChainWeightTests {
|
||||
|
||||
private static final int ACCOUNTS_COUNT_SHIFT = Transformer.PUBLIC_KEY_LENGTH * 8;
|
||||
private static final int CHAIN_WEIGHT_SHIFT = 8;
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final BigInteger MAX_DISTANCE;
|
||||
static {
|
||||
byte[] maxValue = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Arrays.fill(maxValue, (byte) 0xFF);
|
||||
MAX_DISTANCE = new BigInteger(1, maxValue);
|
||||
}
|
||||
|
||||
|
||||
private static byte[] perturbPublicKey(int height, byte[] publicKey) {
|
||||
return Crypto.digest(Bytes.concat(Longs.toByteArray(height), publicKey));
|
||||
}
|
||||
|
||||
private static BigInteger calcKeyDistance(int parentHeight, byte[] parentGeneratorKey, byte[] publicKey) {
|
||||
byte[] idealKey = perturbPublicKey(parentHeight, parentGeneratorKey);
|
||||
byte[] perturbedKey = perturbPublicKey(parentHeight + 1, publicKey);
|
||||
|
||||
BigInteger keyDistance = MAX_DISTANCE.subtract(new BigInteger(idealKey).subtract(new BigInteger(perturbedKey)).abs());
|
||||
return keyDistance;
|
||||
}
|
||||
|
||||
private static BigInteger calcBlockWeight(int parentHeight, byte[] parentGeneratorKey, BlockSummaryData blockSummaryData) {
|
||||
BigInteger keyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, blockSummaryData.getGeneratorPublicKey());
|
||||
BigInteger weight = BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
||||
return weight;
|
||||
}
|
||||
|
||||
private static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockGeneratorKey, List<BlockSummaryData> blockSummaries) {
|
||||
BigInteger cumulativeWeight = BigInteger.ZERO;
|
||||
int parentHeight = commonBlockHeight;
|
||||
byte[] parentGeneratorKey = commonBlockGeneratorKey;
|
||||
|
||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentGeneratorKey, blockSummaryData));
|
||||
parentHeight = blockSummaryData.getHeight();
|
||||
parentGeneratorKey = blockSummaryData.getGeneratorPublicKey();
|
||||
}
|
||||
|
||||
return cumulativeWeight;
|
||||
}
|
||||
|
||||
private static BlockSummaryData genBlockSummary(int height) {
|
||||
byte[] generatorPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
RANDOM.nextBytes(generatorPublicKey);
|
||||
|
||||
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
RANDOM.nextBytes(signature);
|
||||
|
||||
int onlineAccountsCount = RANDOM.nextInt(1000);
|
||||
|
||||
return new BlockSummaryData(height, signature, generatorPublicKey, onlineAccountsCount);
|
||||
}
|
||||
|
||||
private static List<BlockSummaryData> genBlockSummaries(int count, BlockSummaryData commonBlockSummary) {
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
blockSummaries.add(commonBlockSummary);
|
||||
|
||||
final int commonBlockHeight = commonBlockSummary.getHeight();
|
||||
|
||||
for (int i = 1; i <= count; ++i)
|
||||
blockSummaries.add(genBlockSummary(commonBlockHeight + i));
|
||||
|
||||
return blockSummaries;
|
||||
}
|
||||
|
||||
// Check that more online accounts beats a better key
|
||||
@Test
|
||||
public void testMoreAccountsBlock() {
|
||||
final int parentHeight = 1;
|
||||
final byte[] parentGeneratorKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
|
||||
int betterAccountsCount = 100;
|
||||
int worseAccountsCount = 20;
|
||||
|
||||
byte[] betterKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
betterKey[0] = 0x41;
|
||||
|
||||
byte[] worseKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
worseKey[0] = 0x23;
|
||||
|
||||
BigInteger betterKeyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, betterKey);
|
||||
BigInteger worseKeyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, worseKey);
|
||||
assertEquals("hard-coded keys are wrong", 1, betterKeyDistance.compareTo(worseKeyDistance));
|
||||
|
||||
BlockSummaryData betterBlockSummary = new BlockSummaryData(parentHeight + 1, null, worseKey, betterAccountsCount);
|
||||
BlockSummaryData worseBlockSummary = new BlockSummaryData(parentHeight + 1, null, betterKey, worseAccountsCount);
|
||||
|
||||
BigInteger betterBlockWeight = calcBlockWeight(parentHeight, parentGeneratorKey, betterBlockSummary);
|
||||
BigInteger worseBlockWeight = calcBlockWeight(parentHeight, parentGeneratorKey, worseBlockSummary);
|
||||
|
||||
assertEquals("block weights are wrong", 1, betterBlockWeight.compareTo(worseBlockWeight));
|
||||
}
|
||||
|
||||
// Check that a longer chain beats a shorter chain
|
||||
@Test
|
||||
public void testLongerChain() {
|
||||
final int commonBlockHeight = 1;
|
||||
BlockSummaryData commonBlockSummary = genBlockSummary(commonBlockHeight);
|
||||
byte[] commonBlockGeneratorKey = commonBlockSummary.getGeneratorPublicKey();
|
||||
|
||||
List<BlockSummaryData> shorterChain = genBlockSummaries(3, commonBlockSummary);
|
||||
List<BlockSummaryData> longerChain = genBlockSummaries(shorterChain.size() + 1, commonBlockSummary);
|
||||
|
||||
BigInteger shorterChainWeight = calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
|
||||
BigInteger longerChainWeight = calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
|
||||
|
||||
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
|
||||
}
|
||||
|
||||
}
|
@ -13,7 +13,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.data.network.OnlineAccount;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.network.message.GetOnlineAccountsMessage;
|
||||
import org.qora.network.message.Message;
|
||||
import org.qora.network.message.OnlineAccountsMessage;
|
||||
@ -48,7 +48,7 @@ public class OnlineTests extends Common {
|
||||
|
||||
private final PrivateKeyAccount account;
|
||||
|
||||
private List<OnlineAccount> onlineAccounts;
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
private long nextOnlineRefresh = 0;
|
||||
|
||||
public OnlinePeer(int id, PrivateKeyAccount account) {
|
||||
@ -65,22 +65,22 @@ public class OnlineTests extends Common {
|
||||
case GET_ONLINE_ACCOUNTS: {
|
||||
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccount> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccount> accountsToSend;
|
||||
List<OnlineAccountData> accountsToSend;
|
||||
synchronized (this.onlineAccounts) {
|
||||
accountsToSend = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
|
||||
Iterator<OnlineAccount> iterator = accountsToSend.iterator();
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
|
||||
SEND_ITERATOR:
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccount onlineAccount = iterator.next();
|
||||
OnlineAccountData onlineAccount = iterator.next();
|
||||
|
||||
for (int i = 0; i < excludeAccounts.size(); ++i) {
|
||||
OnlineAccount excludeAccount = excludeAccounts.get(i);
|
||||
OnlineAccountData excludeAccount = excludeAccounts.get(i);
|
||||
|
||||
if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.getPublicKey())) {
|
||||
iterator.remove();
|
||||
@ -101,12 +101,12 @@ public class OnlineTests extends Common {
|
||||
case ONLINE_ACCOUNTS: {
|
||||
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccount> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
if (LOG_ACCOUNT_CHANGES)
|
||||
System.out.println(String.format("[%d] received %d online accounts from %d", this.getId(), onlineAccounts.size(), peer.getId()));
|
||||
|
||||
for (OnlineAccount onlineAccount : onlineAccounts)
|
||||
for (OnlineAccountData onlineAccount : onlineAccounts)
|
||||
verifyAndAddAccount(onlineAccount);
|
||||
|
||||
break;
|
||||
@ -117,7 +117,7 @@ public class OnlineTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyAndAddAccount(OnlineAccount onlineAccount) {
|
||||
private void verifyAndAddAccount(OnlineAccountData onlineAccount) {
|
||||
// we would check timestamp is 'recent' here
|
||||
|
||||
// Verify
|
||||
@ -131,7 +131,7 @@ public class OnlineTests extends Common {
|
||||
ByteArray publicKeyBA = new ByteArray(onlineAccount.getPublicKey());
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> new ByteArray(account.getPublicKey()).equals(publicKeyBA)).findFirst().orElse(null);
|
||||
OnlineAccountData existingAccount = this.onlineAccounts.stream().filter(account -> new ByteArray(account.getPublicKey()).equals(publicKeyBA)).findFirst().orElse(null);
|
||||
|
||||
if (existingAccount != null) {
|
||||
if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) {
|
||||
@ -161,9 +161,9 @@ public class OnlineTests extends Common {
|
||||
// Expire old entries
|
||||
final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD;
|
||||
synchronized (this.onlineAccounts) {
|
||||
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
|
||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccount onlineAccount = iterator.next();
|
||||
OnlineAccountData onlineAccount = iterator.next();
|
||||
|
||||
if (onlineAccount.getTimestamp() < cutoffThreshold) {
|
||||
iterator.remove();
|
||||
@ -211,7 +211,7 @@ public class OnlineTests extends Common {
|
||||
byte[] publicKey = this.account.getPublicKey();
|
||||
|
||||
// Our account is online
|
||||
OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey);
|
||||
OnlineAccountData onlineAccount = new OnlineAccountData(timestamp, signature, publicKey);
|
||||
synchronized (this.onlineAccounts) {
|
||||
this.onlineAccounts.removeIf(account -> account.getPublicKey() == this.account.getPublicKey());
|
||||
this.onlineAccounts.add(onlineAccount);
|
||||
|
Loading…
x
Reference in New Issue
Block a user