diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip
index 0e3d5791..06643cca 100755
--- a/WindowsInstaller/Qortal.aip
+++ b/WindowsInstaller/Qortal.aip
@@ -17,10 +17,10 @@
-
+
-
+
@@ -212,7 +212,7 @@
-
+
diff --git a/pom.xml b/pom.xml
index 224640df..e951c7c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0org.qortalqortal
- 3.3.2
+ 3.4.0jartrue
diff --git a/src/main/java/org/qortal/account/PrivateKeyAccount.java b/src/main/java/org/qortal/account/PrivateKeyAccount.java
index 3b370d12..4b646b4a 100644
--- a/src/main/java/org/qortal/account/PrivateKeyAccount.java
+++ b/src/main/java/org/qortal/account/PrivateKeyAccount.java
@@ -11,15 +11,15 @@ public class PrivateKeyAccount extends PublicKeyAccount {
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
/**
- * Create PrivateKeyAccount using byte[32] seed.
+ * Create PrivateKeyAccount using byte[32] private key.
*
- * @param seed
+ * @param privateKey
* byte[32] used to create private/public key pair
* @throws IllegalArgumentException
- * if passed invalid seed
+ * if passed invalid privateKey
*/
- public PrivateKeyAccount(Repository repository, byte[] seed) {
- this(repository, new Ed25519PrivateKeyParameters(seed, 0));
+ public PrivateKeyAccount(Repository repository, byte[] privateKey) {
+ this(repository, new Ed25519PrivateKeyParameters(privateKey, 0));
}
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
@@ -37,10 +37,6 @@ public class PrivateKeyAccount extends PublicKeyAccount {
return this.privateKey;
}
- public static byte[] toPublicKey(byte[] seed) {
- return new Ed25519PrivateKeyParameters(seed, 0).generatePublicKey().getEncoded();
- }
-
public byte[] sign(byte[] message) {
return Crypto.sign(this.edPrivateKeyParams, message);
}
diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
index 73860047..451d9b8a 100644
--- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java
+++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
@@ -57,6 +57,7 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
+import org.qortal.utils.NTP;
import org.qortal.utils.ZipUtils;
@Path("/arbitrary")
@@ -1099,7 +1100,8 @@ public class ArbitraryResource {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, error);
}
- if (!Controller.getInstance().isUpToDate()) {
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
+ if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
index 35a678f2..66800eb7 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
@@ -42,6 +42,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
+import org.qortal.utils.NTP;
@Path("/crosschain/tradebot")
@Tag(name = "Cross-Chain (Trade-Bot)")
@@ -137,7 +138,8 @@ public class CrossChainTradeBotResource {
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
- if (!Controller.getInstance().isUpToDate())
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
+ if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -198,7 +200,8 @@ public class CrossChainTradeBotResource {
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
- if (!Controller.getInstance().isUpToDate())
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
+ if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
// Extract data from cross-chain trading AT
diff --git a/src/main/java/org/qortal/api/resource/TransactionsResource.java b/src/main/java/org/qortal/api/resource/TransactionsResource.java
index 4c440304..75724310 100644
--- a/src/main/java/org/qortal/api/resource/TransactionsResource.java
+++ b/src/main/java/org/qortal/api/resource/TransactionsResource.java
@@ -723,9 +723,9 @@ public class TransactionsResource {
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
})
public String processTransaction(String rawBytes58) {
- // Only allow a transaction to be processed if our latest block is less than 30 minutes old
+ // Only allow a transaction to be processed if our latest block is less than 60 minutes old
// If older than this, we should first wait until the blockchain is synced
- final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index 7800f2a1..ddfe247a 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -3,10 +3,14 @@ package org.qortal.block;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
+import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -24,6 +28,7 @@ import org.qortal.block.BlockChain.BlockTimingByHeight;
import org.qortal.block.BlockChain.AccountLevelShareBin;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.crypto.Crypto;
+import org.qortal.crypto.Qortal25519Extras;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.EligibleQoraHolderData;
@@ -118,6 +123,8 @@ public class Block {
/** Remote/imported/loaded AT states */
protected List atStates;
+ /** Remote hash of AT states - in lieu of full AT state data in {@code atStates} */
+ protected byte[] atStatesHash;
/** Locally-generated AT states */
protected List ourAtStates;
/** Locally-generated AT fees */
@@ -216,11 +223,10 @@ public class Block {
return accountAmount;
}
}
+
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
private List cachedExpandedAccounts = null;
- /** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
- private List cachedValidOnlineAccounts = null;
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
private List cachedOnlineRewardShares = null;
@@ -255,7 +261,7 @@ public class Block {
* Constructs new Block using passed transaction and AT states.
*
* This constructor typically used when receiving a serialized block over the network.
- *
+ *
* @param repository
* @param blockData
* @param transactions
@@ -281,6 +287,35 @@ public class Block {
this.blockData.setTotalFees(totalFees);
}
+ /**
+ * Constructs new Block using passed transaction and minimal AT state info.
+ *
+ * This constructor typically used when receiving a serialized block over the network.
+ *
+ * @param repository
+ * @param blockData
+ * @param transactions
+ * @param atStatesHash
+ */
+ public Block(Repository repository, BlockData blockData, List transactions, byte[] atStatesHash) {
+ this(repository, blockData);
+
+ this.transactions = new ArrayList<>();
+
+ long totalFees = 0;
+
+ // We have to sum fees too
+ for (TransactionData transactionData : transactions) {
+ this.transactions.add(Transaction.fromData(repository, transactionData));
+ totalFees += transactionData.getFee();
+ }
+
+ this.atStatesHash = atStatesHash;
+ totalFees += this.blockData.getATFees();
+
+ this.blockData.setTotalFees(totalFees);
+ }
+
/**
* Constructs new Block with empty transaction list, using passed minter account.
*
@@ -313,18 +348,21 @@ public class Block {
int version = parentBlock.getNextBlockVersion();
byte[] reference = parentBlockData.getSignature();
- // Fetch our list of online accounts
- List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
- if (onlineAccounts.isEmpty()) {
- LOGGER.error("No online accounts - not even our own?");
+ // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
+ int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
+ if (minterLevel == 0) {
+ LOGGER.error("Minter effective level returned zero?");
return null;
}
- // Find newest online accounts timestamp
- long onlineAccountsTimestamp = 0;
- for (OnlineAccountData onlineAccountData : onlineAccounts) {
- if (onlineAccountData.getTimestamp() > onlineAccountsTimestamp)
- onlineAccountsTimestamp = onlineAccountData.getTimestamp();
+ long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
+ long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
+
+ // Fetch our list of online accounts
+ List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
+ if (onlineAccounts.isEmpty()) {
+ LOGGER.error("No online accounts - not even our own?");
+ return null;
}
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
@@ -335,10 +373,6 @@ public class Block {
// Map using index into sorted list of reward-shares as key
Map indexedOnlineAccounts = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
- // Disregard online accounts with different timestamps
- if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
- continue;
-
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
if (accountIndex == null)
// Online account (reward-share) with current timestamp but reward-share cancelled
@@ -355,26 +389,29 @@ public class Block {
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
int onlineAccountsCount = onlineAccountsSet.size();
- // Concatenate online account timestamp signatures (in correct order)
- byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
- for (int i = 0; i < onlineAccountsCount; ++i) {
- Integer accountIndex = accountIndexes.get(i);
- OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
- System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
+ byte[] onlineAccountsSignatures;
+ if (timestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
+ // Collate all signatures
+ Collection signaturesToAggregate = indexedOnlineAccounts.values()
+ .stream()
+ .map(OnlineAccountData::getSignature)
+ .collect(Collectors.toList());
+
+ // Aggregated, single signature
+ onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
+ } else {
+ // Concatenate online account timestamp signatures (in correct order)
+ onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
+ for (int i = 0; i < onlineAccountsCount; ++i) {
+ Integer accountIndex = accountIndexes.get(i);
+ OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
+ System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
+ }
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
minter.getPublicKey(), encodedOnlineAccounts));
- // Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
- int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
- if (minterLevel == 0) {
- LOGGER.error("Minter effective level returned zero?");
- return null;
- }
-
- long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
-
int transactionCount = 0;
byte[] transactionsSignature = null;
int height = parentBlockData.getHeight() + 1;
@@ -979,49 +1016,59 @@ public class Block {
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
- if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
- return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
+ if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
+ // We expect just the one, aggregated signature
+ if (this.blockData.getOnlineAccountsSignatures().length != Transformer.SIGNATURE_LENGTH)
+ return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
+ } else {
+ if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
+ return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
+ }
// Check signatures
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
- // If this block is much older than current online timestamp, then there's no point checking current online accounts
- List currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.getOnlineTimestampModulus()
- ? null
- : OnlineAccountsManager.getInstance().getOnlineAccounts();
- List latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts();
-
- // Extract online accounts' timestamp signatures from block data
+ // Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
- // We'll build up a list of online accounts to hand over to Controller if block is added to chain
- // and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
- List ourOnlineAccounts = new ArrayList<>();
+ if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
+ // Aggregate all public keys
+ Collection publicKeys = onlineRewardShares.stream()
+ .map(RewardShareData::getRewardSharePublicKey)
+ .collect(Collectors.toList());
- for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
- byte[] signature = onlineAccountsSignatures.get(i);
- byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
+ byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
- OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
- ourOnlineAccounts.add(onlineAccountData);
+ byte[] aggregateSignature = onlineAccountsSignatures.get(0);
- // If signature is still current then no need to perform Ed25519 verify
- if (currentOnlineAccounts != null && currentOnlineAccounts.remove(onlineAccountData))
- // remove() returned true, so online account still current
- // and one less entry in currentOnlineAccounts to check next time
- continue;
-
- // If signature was okay in latest block then no need to perform Ed25519 verify
- if (latestBlocksOnlineAccounts != null && latestBlocksOnlineAccounts.contains(onlineAccountData))
- continue;
-
- if (!Crypto.verify(publicKey, signature, onlineTimestampBytes))
+ // One-step verification of aggregate signature using aggregate public key
+ if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
+ } else {
+ // Build block's view of online accounts
+ Set onlineAccounts = new HashSet<>();
+ for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
+ byte[] signature = onlineAccountsSignatures.get(i);
+ byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
+
+ OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
+ onlineAccounts.add(onlineAccountData);
+ }
+
+ // Remove those already validated & cached by online accounts manager - no need to re-validate them
+ OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
+
+ // Validate the rest
+ for (OnlineAccountData onlineAccount : onlineAccounts)
+ if (!Crypto.verify(onlineAccount.getPublicKey(), onlineAccount.getSignature(), onlineTimestampBytes))
+ return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
+
+ // We've validated these, so allow online accounts manager to cache
+ OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
}
// All online accounts valid, so save our list of online accounts for potential later use
- this.cachedValidOnlineAccounts = ourOnlineAccounts;
this.cachedOnlineRewardShares = onlineRewardShares;
return ValidationResult.OK;
@@ -1194,7 +1241,7 @@ public class Block {
*/
private ValidationResult areAtsValid() throws DataException {
// Locally generated AT states should be valid so no need to re-execute them
- if (this.ourAtStates == this.getATStates()) // Note object reference compare
+ if (this.ourAtStates != null && this.ourAtStates == this.atStates) // Note object reference compare
return ValidationResult.OK;
// Generate local AT states for comparison
@@ -1208,8 +1255,33 @@ public class Block {
if (this.ourAtFees != this.blockData.getATFees())
return ValidationResult.AT_STATES_MISMATCH;
- // Note: this.atStates fully loaded thanks to this.getATStates() call above
- for (int s = 0; s < this.atStates.size(); ++s) {
+ // If we have a single AT states hash then compare that in preference
+ if (this.atStatesHash != null) {
+ int atBytesLength = blockData.getATCount() * BlockTransformer.AT_ENTRY_LENGTH;
+ ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
+
+ try {
+ for (ATStateData atStateData : this.ourAtStates) {
+ atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
+ atHashBytes.write(atStateData.getStateHash());
+ atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
+ }
+ } catch (IOException e) {
+ throw new DataException("Couldn't validate AT states hash due to serialization issue?", e);
+ }
+
+ byte[] ourAtStatesHash = Crypto.digest(atHashBytes.toByteArray());
+ if (!Arrays.equals(ourAtStatesHash, this.atStatesHash))
+ return ValidationResult.AT_STATES_MISMATCH;
+
+ // Use our AT state data from now on
+ this.atStates = this.ourAtStates;
+ return ValidationResult.OK;
+ }
+
+ // Note: this.atStates fully loaded thanks to this.getATStates() call:
+ this.getATStates();
+ for (int s = 0; s < this.ourAtStates.size(); ++s) {
ATStateData ourAtState = this.ourAtStates.get(s);
ATStateData theirAtState = this.atStates.get(s);
@@ -1367,9 +1439,6 @@ public class Block {
postBlockTidy();
- // Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
- OnlineAccountsManager.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
-
// Log some debugging info relating to the block weight calculation
this.logDebugInfo();
}
@@ -1585,9 +1654,6 @@ public class Block {
this.blockData.setHeight(null);
postBlockTidy();
-
- // Remove any cached, valid online accounts data from Controller
- OnlineAccountsManager.getInstance().popLatestBlocksOnlineAccounts();
}
protected void orphanTransactionsFromBlock() throws DataException {
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index b54c9613..8bbefb11 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -70,7 +70,9 @@ public class BlockChain {
shareBinFix,
calcChainWeightTimestamp,
transactionV5Timestamp,
- transactionV6Timestamp;
+ transactionV6Timestamp,
+ disableReferenceTimestamp,
+ aggregateSignatureTimestamp;
}
// Custom transaction fees
@@ -419,6 +421,14 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.transactionV6Timestamp.name()).longValue();
}
+ public long getDisableReferenceTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
+ }
+
+ public long getAggregateSignatureTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.aggregateSignatureTimestamp.name()).longValue();
+ }
+
// More complex getters for aspects that change by height or timestamp
public long getRewardAtHeight(int ourHeight) {
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 9966d6a9..2d736e76 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -65,9 +65,8 @@ public class BlockMinter extends Thread {
// Lite nodes do not mint
return;
}
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
+ if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
// Wipe existing unconfirmed transactions
List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
@@ -77,30 +76,31 @@ public class BlockMinter extends Thread {
}
repository.saveChanges();
+ } catch (DataException e) {
+ LOGGER.warn("Repository issue trying to wipe unconfirmed transactions on start-up: {}", e.getMessage());
+ // Fall-through to normal behaviour in case we can recover
}
+ }
- // Going to need this a lot...
- BlockRepository blockRepository = repository.getBlockRepository();
- BlockData previousBlockData = null;
+ BlockData previousBlockData = null;
- // Vars to keep track of blocks that were skipped due to chain weight
- byte[] parentSignatureForLastLowWeightBlock = null;
- Long timeOfLastLowWeightBlock = null;
+ // Vars to keep track of blocks that were skipped due to chain weight
+ byte[] parentSignatureForLastLowWeightBlock = null;
+ Long timeOfLastLowWeightBlock = null;
- List newBlocks = new ArrayList<>();
+ List newBlocks = new ArrayList<>();
- // Flags for tracking change in whether minting is possible,
- // so we can notify Controller, and further update SysTray, etc.
- boolean isMintingPossible = false;
- boolean wasMintingPossible = isMintingPossible;
- while (running) {
- repository.discardChanges(); // Free repository locks, if any
+ // Flags for tracking change in whether minting is possible,
+ // so we can notify Controller, and further update SysTray, etc.
+ boolean isMintingPossible = false;
+ boolean wasMintingPossible = isMintingPossible;
+ while (running) {
+ if (isMintingPossible != wasMintingPossible)
+ Controller.getInstance().onMintingPossibleChange(isMintingPossible);
- if (isMintingPossible != wasMintingPossible)
- Controller.getInstance().onMintingPossibleChange(isMintingPossible);
-
- wasMintingPossible = isMintingPossible;
+ wasMintingPossible = isMintingPossible;
+ try {
// Sleep for a while
Thread.sleep(1000);
@@ -114,319 +114,338 @@ public class BlockMinter extends Thread {
if (minLatestBlockTimestamp == null)
continue;
- // No online accounts? (e.g. during startup)
- if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
+ // No online accounts for current timestamp? (e.g. during startup)
+ if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
continue;
- List mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
- // No minting accounts?
- if (mintingAccountsData.isEmpty())
- continue;
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ // Going to need this a lot...
+ BlockRepository blockRepository = repository.getBlockRepository();
- // Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
- // Note that minting accounts are actually reward-shares in Qortal
- Iterator madi = mintingAccountsData.iterator();
- while (madi.hasNext()) {
- MintingAccountData mintingAccountData = madi.next();
-
- RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
- if (rewardShareData == null) {
- // Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
- madi.remove();
- continue;
- }
-
- Account mintingAccount = new Account(repository, rewardShareData.getMinter());
- if (!mintingAccount.canMint()) {
- // Minting-account component of reward-share can no longer mint - disregard
- madi.remove();
- continue;
- }
-
- // Optional (non-validated) prevention of block submissions below a defined level.
- // This is an unvalidated version of Blockchain.minAccountLevelToMint
- // and exists only to reduce block candidates by default.
- int level = mintingAccount.getEffectiveMintingLevel();
- if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
- madi.remove();
- continue;
- }
- }
-
- // Needs a mutable copy of the unmodifiableList
- List peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
- BlockData lastBlockData = blockRepository.getLastBlock();
-
- // Disregard peers that have "misbehaved" recently
- peers.removeIf(Controller.hasMisbehaved);
-
- // Disregard peers that don't have a recent block, but only if we're not in recovery mode.
- // In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
- if (Synchronizer.getInstance().getRecoveryMode() == false)
- peers.removeIf(Controller.hasNoRecentBlock);
-
- // Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
- if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
- continue;
-
- // If we are stuck on an invalid block, we should allow an alternative to be minted
- boolean recoverInvalidBlock = false;
- if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
- // We've had at least one invalid block
- long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
- long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
- if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
- if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
- // Last valid block was more than 10 mins ago, but we've had an invalid block since then
- // Assume that the chain has stalled because there is no alternative valid candidate
- // Enter recovery mode to allow alternative, valid candidates to be minted
- recoverInvalidBlock = true;
- }
- }
- }
-
- // If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
- if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
- if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
+ List mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
+ // No minting accounts?
+ if (mintingAccountsData.isEmpty())
continue;
- // There are enough peers with a recent block and our latest block is recent
- // so go ahead and mint a block if possible.
- isMintingPossible = true;
+ // Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
+ // Note that minting accounts are actually reward-shares in Qortal
+ Iterator madi = mintingAccountsData.iterator();
+ while (madi.hasNext()) {
+ MintingAccountData mintingAccountData = madi.next();
- // Check blockchain hasn't changed
- if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
- previousBlockData = lastBlockData;
- newBlocks.clear();
-
- // Reduce log timeout
- logTimeout = 10 * 1000L;
-
- // Last low weight block is no longer valid
- parentSignatureForLastLowWeightBlock = null;
- }
-
- // Discard accounts we have already built blocks with
- mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
-
- // Do we need to build any potential new blocks?
- List newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
-
- // We might need to sit the next block out, if one of our minting accounts signed the previous one
- final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
- final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
- if (mintedLastBlock) {
- LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
- continue;
- }
-
- if (parentSignatureForLastLowWeightBlock != null) {
- // The last iteration found a higher weight block in the network, so sleep for a while
- // to allow is to sync the higher weight chain. We are sleeping here rather than when
- // detected as we don't want to hold the blockchain lock open.
- LOGGER.debug("Sleeping for 10 seconds...");
- Thread.sleep(10 * 1000L);
- }
-
- for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
- // First block does the AT heavy-lifting
- if (newBlocks.isEmpty()) {
- Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
- if (newBlock == null) {
- // For some reason we can't mint right now
- moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
+ RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
+ if (rewardShareData == null) {
+ // Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
+ madi.remove();
continue;
}
- newBlocks.add(newBlock);
- } else {
- // The blocks for other minters require less effort...
- Block newBlock = newBlocks.get(0).remint(mintingAccount);
- if (newBlock == null) {
- // For some reason we can't mint right now
- moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
+ Account mintingAccount = new Account(repository, rewardShareData.getMinter());
+ if (!mintingAccount.canMint()) {
+ // Minting-account component of reward-share can no longer mint - disregard
+ madi.remove();
continue;
}
- newBlocks.add(newBlock);
+ // Optional (non-validated) prevention of block submissions below a defined level.
+ // This is an unvalidated version of Blockchain.minAccountLevelToMint
+ // and exists only to reduce block candidates by default.
+ int level = mintingAccount.getEffectiveMintingLevel();
+ if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
+ madi.remove();
+ continue;
+ }
}
- }
- // No potential block candidates?
- if (newBlocks.isEmpty())
- continue;
+ // Needs a mutable copy of the unmodifiableList
+ List peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
+ BlockData lastBlockData = blockRepository.getLastBlock();
- // Make sure we're the only thread modifying the blockchain
- ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
- if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
- LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
- continue;
- }
+ // Disregard peers that have "misbehaved" recently
+ peers.removeIf(Controller.hasMisbehaved);
- boolean newBlockMinted = false;
- Block newBlock = null;
+ // Disregard peers that don't have a recent block, but only if we're not in recovery mode.
+ // In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
+ if (Synchronizer.getInstance().getRecoveryMode() == false)
+ peers.removeIf(Controller.hasNoRecentBlock);
- try {
- // Clear repository session state so we have latest view of data
- repository.discardChanges();
-
- // Now that we have blockchain lock, do final check that chain hasn't changed
- BlockData latestBlockData = blockRepository.getLastBlock();
- if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
+ // Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
+ if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
continue;
- List goodBlocks = new ArrayList<>();
- for (Block testBlock : newBlocks) {
- // Is new block's timestamp valid yet?
- // We do a separate check as some timestamp checks are skipped for testchains
- if (testBlock.isTimestampValid() != ValidationResult.OK)
- continue;
-
- testBlock.preProcess();
-
- // Is new block valid yet? (Before adding unconfirmed transactions)
- ValidationResult result = testBlock.isValid();
- if (result != ValidationResult.OK) {
- moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
-
- continue;
- }
-
- goodBlocks.add(testBlock);
- }
-
- if (goodBlocks.isEmpty())
- continue;
-
- // Pick best block
- final int parentHeight = previousBlockData.getHeight();
- final byte[] parentBlockSignature = previousBlockData.getSignature();
-
- BigInteger bestWeight = null;
-
- for (int bi = 0; bi < goodBlocks.size(); ++bi) {
- BlockData blockData = goodBlocks.get(bi).getBlockData();
-
- BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
- int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
- blockSummaryData.setMinterLevel(minterLevel);
-
- BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
-
- if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
- newBlock = goodBlocks.get(bi);
- bestWeight = blockWeight;
- }
- }
-
- try {
- if (this.higherWeightChainExists(repository, bestWeight)) {
-
- // Check if the base block has updated since the last time we were here
- if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
- !Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
- // We've switched to a different chain, so reset the timer
- timeOfLastLowWeightBlock = NTP.getTime();
+ // If we are stuck on an invalid block, we should allow an alternative to be minted
+ boolean recoverInvalidBlock = false;
+ if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
+ // We've had at least one invalid block
+ long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
+ long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
+ if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
+ if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
+ // Last valid block was more than 10 mins ago, but we've had an invalid block since then
+ // Assume that the chain has stalled because there is no alternative valid candidate
+ // Enter recovery mode to allow alternative, valid candidates to be minted
+ recoverInvalidBlock = true;
}
- parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
+ }
+ }
- // If less than 30 seconds has passed since first detection the higher weight chain,
- // we should skip our block submission to give us the opportunity to sync to the better chain
- if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
- LOGGER.debug("Higher weight chain found in peers, so not signing a block this round");
- LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
+ // If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
+ if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
+ if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
+ continue;
+
+ // There are enough peers with a recent block and our latest block is recent
+ // so go ahead and mint a block if possible.
+ isMintingPossible = true;
+
+ // Reattach newBlocks to new repository handle
+ for (Block newBlock : newBlocks)
+ newBlock.setRepository(repository);
+
+ // Check blockchain hasn't changed
+ if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
+ previousBlockData = lastBlockData;
+ newBlocks.clear();
+
+ // Reduce log timeout
+ logTimeout = 10 * 1000L;
+
+ // Last low weight block is no longer valid
+ parentSignatureForLastLowWeightBlock = null;
+ }
+
+ // Discard accounts we have already built blocks with
+ mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
+
+ // Do we need to build any potential new blocks?
+ List newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
+
+ // We might need to sit the next block out, if one of our minting accounts signed the previous one
+ byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
+ boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
+ if (mintedLastBlock) {
+ LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
+ continue;
+ }
+
+ if (parentSignatureForLastLowWeightBlock != null) {
+ // The last iteration found a higher weight block in the network, so sleep for a while
+ // to allow is to sync the higher weight chain. We are sleeping here rather than when
+ // detected as we don't want to hold the blockchain lock open.
+ LOGGER.info("Sleeping for 10 seconds...");
+ Thread.sleep(10 * 1000L);
+ }
+
+ for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
+ // First block does the AT heavy-lifting
+ if (newBlocks.isEmpty()) {
+ Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
+ if (newBlock == null) {
+ // For some reason we can't mint right now
+ moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
continue;
}
- else {
- // More than 30 seconds have passed, so we should submit our block candidate anyway.
- LOGGER.debug("More than 30 seconds passed, so proceeding to submit block candidate...");
+
+ newBlocks.add(newBlock);
+ } else {
+ // The blocks for other minters require less effort...
+ Block newBlock = newBlocks.get(0).remint(mintingAccount);
+ if (newBlock == null) {
+ // For some reason we can't mint right now
+ moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
+ continue;
}
+
+ newBlocks.add(newBlock);
}
- else {
- LOGGER.debug("No higher weight chain found in peers");
- }
- } catch (DataException e) {
- LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
}
- // Discard any uncommitted changes as a result of the higher weight chain detection
- repository.discardChanges();
+ // No potential block candidates?
+ if (newBlocks.isEmpty())
+ continue;
- // Clear variables that track low weight blocks
- parentSignatureForLastLowWeightBlock = null;
- timeOfLastLowWeightBlock = null;
-
-
- // Add unconfirmed transactions
- addUnconfirmedTransactions(repository, newBlock);
-
- // Sign to create block's signature
- newBlock.sign();
-
- // Is newBlock still valid?
- ValidationResult validationResult = newBlock.isValid();
- if (validationResult != ValidationResult.OK) {
- // No longer valid? Report and discard
- LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
-
- // Rebuild block candidates, just to be sure
- newBlocks.clear();
+ // Make sure we're the only thread modifying the blockchain
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+ if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
+ LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
continue;
}
- // Add to blockchain - something else will notice and broadcast new block to network
+ boolean newBlockMinted = false;
+ Block newBlock = null;
+
try {
- newBlock.process();
+ // Clear repository session state so we have latest view of data
+ repository.discardChanges();
- repository.saveChanges();
+ // Now that we have blockchain lock, do final check that chain hasn't changed
+ BlockData latestBlockData = blockRepository.getLastBlock();
+ if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
+ continue;
- LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
+ List goodBlocks = new ArrayList<>();
+ boolean wasInvalidBlockDiscarded = false;
+ Iterator newBlocksIterator = newBlocks.iterator();
- RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
+ while (newBlocksIterator.hasNext()) {
+ Block testBlock = newBlocksIterator.next();
- if (rewardShareData != null) {
- LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
- newBlock.getBlockData().getHeight(),
- Base58.encode(newBlock.getBlockData().getSignature()),
- Base58.encode(newBlock.getParent().getSignature()),
- rewardShareData.getMinter(),
- rewardShareData.getRecipient()));
- } else {
- LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
- newBlock.getBlockData().getHeight(),
- Base58.encode(newBlock.getBlockData().getSignature()),
- Base58.encode(newBlock.getParent().getSignature()),
- newBlock.getMinter().getAddress()));
+ // Is new block's timestamp valid yet?
+ // We do a separate check as some timestamp checks are skipped for testchains
+ if (testBlock.isTimestampValid() != ValidationResult.OK)
+ continue;
+
+ testBlock.preProcess();
+
+ // Is new block valid yet? (Before adding unconfirmed transactions)
+ ValidationResult result = testBlock.isValid();
+ if (result != ValidationResult.OK) {
+ moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
+
+ newBlocksIterator.remove();
+ wasInvalidBlockDiscarded = true;
+ /*
+ * Bail out fast so that we loop around from the top again.
+ * This gives BlockMinter the possibility to remint this candidate block using another block from newBlocks,
+ * via the Blocks.remint() method, which avoids having to re-process Block ATs all over again.
+ * Particularly useful if some aspect of Blocks changes due a timestamp-based feature-trigger (see BlockChain class).
+ */
+ break;
+ }
+
+ goodBlocks.add(testBlock);
}
- // Notify network after we're released blockchain lock
- newBlockMinted = true;
+ if (wasInvalidBlockDiscarded || goodBlocks.isEmpty())
+ continue;
- // Notify Controller
- repository.discardChanges(); // clear transaction status to prevent deadlocks
- Controller.getInstance().onNewBlock(newBlock.getBlockData());
- } catch (DataException e) {
- // Unable to process block - report and discard
- LOGGER.error("Unable to process newly minted block?", e);
- newBlocks.clear();
+ // Pick best block
+ final int parentHeight = previousBlockData.getHeight();
+ final byte[] parentBlockSignature = previousBlockData.getSignature();
+
+ BigInteger bestWeight = null;
+
+ for (int bi = 0; bi < goodBlocks.size(); ++bi) {
+ BlockData blockData = goodBlocks.get(bi).getBlockData();
+
+ BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
+ int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
+ blockSummaryData.setMinterLevel(minterLevel);
+
+ BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
+
+ if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
+ newBlock = goodBlocks.get(bi);
+ bestWeight = blockWeight;
+ }
+ }
+
+ try {
+ if (this.higherWeightChainExists(repository, bestWeight)) {
+
+ // Check if the base block has updated since the last time we were here
+ if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
+ !Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
+ // We've switched to a different chain, so reset the timer
+ timeOfLastLowWeightBlock = NTP.getTime();
+ }
+ parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
+
+ // If less than 30 seconds has passed since first detection the higher weight chain,
+ // we should skip our block submission to give us the opportunity to sync to the better chain
+ if (NTP.getTime() - timeOfLastLowWeightBlock < 30 * 1000L) {
+ LOGGER.info("Higher weight chain found in peers, so not signing a block this round");
+ LOGGER.info("Time since detected: {}", NTP.getTime() - timeOfLastLowWeightBlock);
+ continue;
+ } else {
+ // More than 30 seconds have passed, so we should submit our block candidate anyway.
+ LOGGER.info("More than 30 seconds passed, so proceeding to submit block candidate...");
+ }
+ } else {
+ LOGGER.debug("No higher weight chain found in peers");
+ }
+ } catch (DataException e) {
+ LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
+ }
+
+ // Discard any uncommitted changes as a result of the higher weight chain detection
+ repository.discardChanges();
+
+ // Clear variables that track low weight blocks
+ parentSignatureForLastLowWeightBlock = null;
+ timeOfLastLowWeightBlock = null;
+
+ // Add unconfirmed transactions
+ addUnconfirmedTransactions(repository, newBlock);
+
+ // Sign to create block's signature
+ newBlock.sign();
+
+ // Is newBlock still valid?
+ ValidationResult validationResult = newBlock.isValid();
+ if (validationResult != ValidationResult.OK) {
+ // No longer valid? Report and discard
+ LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
+
+ // Rebuild block candidates, just to be sure
+ newBlocks.clear();
+ continue;
+ }
+
+ // Add to blockchain - something else will notice and broadcast new block to network
+ try {
+ newBlock.process();
+
+ repository.saveChanges();
+
+ LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
+
+ RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
+
+ if (rewardShareData != null) {
+ LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
+ newBlock.getBlockData().getHeight(),
+ Base58.encode(newBlock.getBlockData().getSignature()),
+ Base58.encode(newBlock.getParent().getSignature()),
+ rewardShareData.getMinter(),
+ rewardShareData.getRecipient()));
+ } else {
+ LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
+ newBlock.getBlockData().getHeight(),
+ Base58.encode(newBlock.getBlockData().getSignature()),
+ Base58.encode(newBlock.getParent().getSignature()),
+ newBlock.getMinter().getAddress()));
+ }
+
+ // Notify network after we're released blockchain lock
+ newBlockMinted = true;
+
+ // Notify Controller
+ repository.discardChanges(); // clear transaction status to prevent deadlocks
+ Controller.getInstance().onNewBlock(newBlock.getBlockData());
+ } catch (DataException e) {
+ // Unable to process block - report and discard
+ LOGGER.error("Unable to process newly minted block?", e);
+ newBlocks.clear();
+ }
+ } finally {
+ blockchainLock.unlock();
}
- } finally {
- blockchainLock.unlock();
- }
- if (newBlockMinted) {
- // Broadcast our new chain to network
- BlockData newBlockData = newBlock.getBlockData();
+ if (newBlockMinted) {
+ // Broadcast our new chain to network
+ BlockData newBlockData = newBlock.getBlockData();
- Network network = Network.getInstance();
- network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
+ Network network = Network.getInstance();
+ network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
+ }
+ } catch (DataException e) {
+ LOGGER.warn("Repository issue while running block minter", e);
}
+ } catch (InterruptedException e) {
+ // We've been interrupted - time to exit
+ return;
}
- } catch (DataException e) {
- LOGGER.warn("Repository issue while running block minter", e);
- } catch (InterruptedException e) {
- // We've been interrupted - time to exit
- return;
}
}
@@ -557,18 +576,23 @@ public class BlockMinter extends Thread {
// This peer has common block data
CommonBlockData commonBlockData = peer.getCommonBlockData();
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
- if (commonBlockData.getChainWeight() != null) {
+ if (commonBlockData.getChainWeight() != null && peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
// The synchronizer has calculated this peer's chain weight
- BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
- BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
- BigInteger peerChainWeight = commonBlockData.getChainWeight();
- if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
- // This peer has a higher weight chain than ours
- LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
- return true;
+ if (!Synchronizer.getInstance().containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
+ // .. and it doesn't hold any invalid blocks
+ BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
+ BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
+ BigInteger peerChainWeight = commonBlockData.getChainWeight();
+ if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
+ // This peer has a higher weight chain than ours
+ LOGGER.info("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
+ return true;
+ } else {
+ LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
+ }
} else {
- LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
+ LOGGER.debug("Peer {} has an invalid block", peer);
}
} else {
LOGGER.debug("Peer {} has no chain weight", peer);
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index a5ada0c2..cde965c1 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -113,6 +113,7 @@ public class Controller extends Thread {
private long repositoryBackupTimestamp = startTime; // ms
private long repositoryMaintenanceTimestamp = startTime; // ms
private long repositoryCheckpointTimestamp = startTime; // ms
+ private long prunePeersTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
@@ -552,6 +553,7 @@ public class Controller extends Thread {
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
+ final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
// Start executor service for trimming or pruning
PruneManager.getInstance().start();
@@ -649,10 +651,15 @@ public class Controller extends Thread {
}
// Prune stuck/slow/old peers
- try {
- Network.getInstance().prunePeers();
- } catch (DataException e) {
- LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
+ if (now >= prunePeersTimestamp + prunePeersInterval) {
+ prunePeersTimestamp = now + prunePeersInterval;
+
+ try {
+ LOGGER.debug("Pruning peers...");
+ Network.getInstance().prunePeers();
+ } catch (DataException e) {
+ LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
+ }
}
// Delete expired transactions
@@ -787,23 +794,24 @@ public class Controller extends Thread {
String actionText;
// Use a more tolerant latest block timestamp in the isUpToDate() calls below to reduce misleading statuses.
- // Any block in the last 30 minutes is considered "up to date" for the purposes of displaying statuses.
- final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
+ // Any block in the last 2 hours is considered "up to date" for the purposes of displaying statuses.
+ // This also aligns with the time interval required for continued online account submission.
+ final Long minLatestBlockTimestamp = NTP.getTime() - (2 * 60 * 60 * 1000L);
+
+ // Only show sync percent if it's less than 100, to avoid confusion
+ final Integer syncPercent = Synchronizer.getInstance().getSyncPercent();
+ final boolean isSyncing = (syncPercent != null && syncPercent < 100);
synchronized (Synchronizer.getInstance().syncLock) {
if (Settings.getInstance().isLite()) {
actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE");
SysTray.getInstance().setTrayIcon(4);
}
- else if (this.isMintingPossible) {
- actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
- SysTray.getInstance().setTrayIcon(2);
- }
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
SysTray.getInstance().setTrayIcon(3);
}
- else if (!this.isUpToDate(minLatestBlockTimestamp) && Synchronizer.getInstance().isSynchronizing()) {
+ else if (!this.isUpToDate(minLatestBlockTimestamp) && isSyncing) {
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
SysTray.getInstance().setTrayIcon(3);
}
@@ -811,6 +819,10 @@ public class Controller extends Thread {
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
SysTray.getInstance().setTrayIcon(3);
}
+ else if (OnlineAccountsManager.getInstance().hasOnlineAccounts()) {
+ actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
+ SysTray.getInstance().setTrayIcon(2);
+ }
else {
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
SysTray.getInstance().setTrayIcon(4);
@@ -1229,6 +1241,10 @@ public class Controller extends Thread {
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
break;
+ case GET_ONLINE_ACCOUNTS_V3:
+ OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
+ break;
+
case GET_ARBITRARY_DATA:
// Not currently supported
break;
@@ -1362,6 +1378,18 @@ public class Controller extends Thread {
Block block = new Block(repository, blockData);
+ // V2 support
+ if (peer.getPeersVersion() >= BlockV2Message.MIN_PEER_VERSION) {
+ Message blockMessage = new BlockV2Message(block);
+ blockMessage.setId(message.getId());
+ if (!peer.sendMessage(blockMessage)) {
+ peer.disconnect("failed to send block");
+ // Don't fall-through to caching because failure to send might be from failure to build message
+ return;
+ }
+ return;
+ }
+
CachedBlockMessage blockMessage = new CachedBlockMessage(block);
blockMessage.setId(message.getId());
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index 092cae05..52d3b5fa 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -1,12 +1,15 @@
package org.qortal.controller;
+import com.google.common.hash.HashCode;
import com.google.common.primitives.Longs;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
-import org.qortal.account.PublicKeyAccount;
+import org.qortal.block.Block;
import org.qortal.block.BlockChain;
+import org.qortal.crypto.Crypto;
+import org.qortal.crypto.Qortal25519Extras;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.network.OnlineAccountData;
@@ -18,103 +21,63 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
+import org.qortal.utils.NamedThreadFactory;
import java.util.*;
+import java.util.concurrent.*;
import java.util.stream.Collectors;
-public class OnlineAccountsManager extends Thread {
-
- private class OurOnlineAccountsThread extends Thread {
-
- public void run() {
- try {
- while (!isStopping) {
- Thread.sleep(10000L);
-
- // Refresh our online accounts signatures?
- sendOurOnlineAccountsInfo();
-
- }
- } catch (InterruptedException e) {
- // Fall through to exit thread
- }
- }
- }
-
+public class OnlineAccountsManager {
private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsManager.class);
- private static OnlineAccountsManager instance;
+ // 'Current' as in 'now'
+
+ /**
+ * How long online accounts signatures last before they expire.
+ */
+ private static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L;
+ private static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L;
+
+ /**
+ * How many 'current' timestamp-sets of online accounts we cache.
+ */
+ private static final int MAX_CACHED_TIMESTAMP_SETS = 2;
+
+ /**
+ * How many timestamp-sets of online accounts we cache for 'latest blocks'.
+ */
+ private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 3;
+
+ private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; //ms
+ private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
+ private static final long ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL = 60 * 1000L; // ms
+ private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 15 * 1000L; // ms
+
+ private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; // v3.2.0
+ private static final long ONLINE_ACCOUNTS_V3_PEER_VERSION = 0x03000300cbL; // v3.3.203
+
+ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, new NamedThreadFactory("OnlineAccounts"));
private volatile boolean isStopping = false;
- // To do with online accounts list
- private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
- private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms
- public static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L;
- public static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L;
- /** How many (latest) blocks' worth of online accounts we cache */
- private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2;
- private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L;
+ private final Set onlineAccountsImportQueue = ConcurrentHashMap.newKeySet();
- private long onlineAccountsTasksTimestamp = Controller.startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
+ /**
+ * Cache of 'current' online accounts, keyed by timestamp
+ */
+ private final Map> currentOnlineAccounts = new ConcurrentHashMap<>();
+ /**
+ * Cache of hash-summary of 'current' online accounts, keyed by timestamp, then leading byte of public key.
+ */
+ private final Map> currentOnlineAccountsHashes = new ConcurrentHashMap<>();
- private final List onlineAccountsImportQueue = Collections.synchronizedList(new ArrayList<>());
+ /**
+ * Cache of online accounts for latest blocks - not necessarily 'current' / now.
+ * Probably only accessed / modified by a single Synchronizer thread.
+ */
+ private final SortedMap> latestBlocksOnlineAccounts = new ConcurrentSkipListMap<>();
-
- /** Cache of current 'online accounts' */
- List onlineAccounts = new ArrayList<>();
- /** Cache of latest blocks' online accounts */
- Deque> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
-
- public OnlineAccountsManager() {
-
- }
-
- public static synchronized OnlineAccountsManager getInstance() {
- if (instance == null) {
- instance = new OnlineAccountsManager();
- }
-
- return instance;
- }
-
- public void run() {
-
- // Start separate thread to prepare our online accounts
- // This could be converted to a thread pool later if more concurrency is needed
- OurOnlineAccountsThread ourOnlineAccountsThread = new OurOnlineAccountsThread();
- ourOnlineAccountsThread.start();
-
- try {
- while (!Controller.isStopping()) {
- Thread.sleep(100L);
-
- final Long now = NTP.getTime();
- if (now == null) {
- continue;
- }
-
- // Perform tasks to do with managing online accounts list
- if (now >= onlineAccountsTasksTimestamp) {
- onlineAccountsTasksTimestamp = now + ONLINE_ACCOUNTS_TASKS_INTERVAL;
- performOnlineAccountsTasks();
- }
-
- // Process queued online account verifications
- this.processOnlineAccountsImportQueue();
-
- }
- } catch (InterruptedException e) {
- // Fall through to exit thread
- }
-
- ourOnlineAccountsThread.interrupt();
- }
-
- public void shutdown() {
- isStopping = true;
- this.interrupt();
- }
+ private boolean hasOurOnlineAccounts = false;
public static long getOnlineTimestampModulus() {
Long now = NTP.getTime();
@@ -123,183 +86,316 @@ public class OnlineAccountsManager extends Thread {
}
return ONLINE_TIMESTAMP_MODULUS_V1;
}
-
-
- // Online accounts import queue
-
- private void processOnlineAccountsImportQueue() {
- if (this.onlineAccountsImportQueue.isEmpty()) {
- // Nothing to do
- return;
- }
-
- LOGGER.debug("Processing online accounts import queue (size: {})", this.onlineAccountsImportQueue.size());
-
- try (final Repository repository = RepositoryManager.getRepository()) {
-
- List onlineAccountDataCopy = new ArrayList<>(this.onlineAccountsImportQueue);
- for (OnlineAccountData onlineAccountData : onlineAccountDataCopy) {
- if (isStopping) {
- return;
- }
-
- this.verifyAndAddAccount(repository, onlineAccountData);
-
- // Remove from queue
- onlineAccountsImportQueue.remove(onlineAccountData);
- }
-
- LOGGER.debug("Finished processing online accounts import queue");
-
- } catch (DataException e) {
- LOGGER.error(String.format("Repository issue while verifying online accounts"), e);
- }
- }
-
-
- // Utilities
-
- private void verifyAndAddAccount(Repository repository, OnlineAccountData onlineAccountData) throws DataException {
- final Long now = NTP.getTime();
+ public static Long getCurrentOnlineAccountTimestamp() {
+ Long now = NTP.getTime();
if (now == null)
- return;
+ return null;
- PublicKeyAccount otherAccount = new PublicKeyAccount(repository, onlineAccountData.getPublicKey());
-
- // Check timestamp is 'recent' here
- if (Math.abs(onlineAccountData.getTimestamp() - now) > getOnlineTimestampModulus() * 2) {
- LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp()));
- return;
- }
-
- // Verify
- byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
- if (!otherAccount.verify(onlineAccountData.getSignature(), data)) {
- LOGGER.trace(() -> String.format("Rejecting invalid online account %s", otherAccount.getAddress()));
- return;
- }
-
- // Qortal: check online account is actually reward-share
- RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(onlineAccountData.getPublicKey());
- if (rewardShareData == null) {
- // Reward-share doesn't even exist - probably not a good sign
- LOGGER.trace(() -> String.format("Rejecting unknown online reward-share public key %s", Base58.encode(onlineAccountData.getPublicKey())));
- return;
- }
-
- Account mintingAccount = new Account(repository, rewardShareData.getMinter());
- if (!mintingAccount.canMint()) {
- // Minting-account component of reward-share can no longer mint - disregard
- LOGGER.trace(() -> String.format("Rejecting online reward-share with non-minting account %s", mintingAccount.getAddress()));
- return;
- }
-
- synchronized (this.onlineAccounts) {
- OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null);
-
- 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(), 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(), onlineAccountData.getTimestamp()));
- }
-
- this.onlineAccounts.add(onlineAccountData);
- }
+ long onlineTimestampModulus = getOnlineTimestampModulus();
+ return (now / onlineTimestampModulus) * onlineTimestampModulus;
}
+ private OnlineAccountsManager() {
+ }
+
+ private static class SingletonContainer {
+ private static final OnlineAccountsManager INSTANCE = new OnlineAccountsManager();
+ }
+
+ public static OnlineAccountsManager getInstance() {
+ return SingletonContainer.INSTANCE;
+ }
+
+ public void start() {
+ // Expire old online accounts signatures
+ executor.scheduleAtFixedRate(this::expireOldOnlineAccounts, ONLINE_ACCOUNTS_TASKS_INTERVAL, ONLINE_ACCOUNTS_TASKS_INTERVAL, TimeUnit.MILLISECONDS);
+
+ // Send our online accounts
+ executor.scheduleAtFixedRate(this::sendOurOnlineAccountsInfo, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
+
+ // Request online accounts from peers (legacy)
+ executor.scheduleAtFixedRate(this::requestLegacyRemoteOnlineAccounts, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
+ // Request online accounts from peers (V3+)
+ executor.scheduleAtFixedRate(this::requestRemoteOnlineAccounts, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
+
+ // Process import queue
+ executor.scheduleWithFixedDelay(this::processOnlineAccountsImportQueue, ONLINE_ACCOUNTS_QUEUE_INTERVAL, ONLINE_ACCOUNTS_QUEUE_INTERVAL, TimeUnit.MILLISECONDS);
+ }
+
+ public void shutdown() {
+ isStopping = true;
+ executor.shutdownNow();
+ }
+
+ // Testing support
public void ensureTestingAccountsOnline(PrivateKeyAccount... onlineAccounts) {
if (!BlockChain.getInstance().isTestChain()) {
LOGGER.warn("Ignoring attempt to ensure test account is online for non-test chain!");
return;
}
- final Long now = NTP.getTime();
- if (now == null)
+ final Long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
+ if (onlineAccountsTimestamp == null)
return;
- final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now);
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
+ final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
- synchronized (this.onlineAccounts) {
- this.onlineAccounts.clear();
+ Set replacementAccounts = new HashSet<>();
+ for (PrivateKeyAccount onlineAccount : onlineAccounts) {
+ // Check mintingAccount is actually reward-share?
- for (PrivateKeyAccount onlineAccount : onlineAccounts) {
- // Check mintingAccount is actually reward-share?
+ byte[] signature = useAggregateCompatibleSignature
+ ? Qortal25519Extras.signForAggregation(onlineAccount.getPrivateKey(), timestampBytes)
+ : onlineAccount.sign(timestampBytes);
+ byte[] publicKey = onlineAccount.getPublicKey();
- byte[] signature = onlineAccount.sign(timestampBytes);
- byte[] publicKey = onlineAccount.getPublicKey();
+ OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
+ replacementAccounts.add(ourOnlineAccountData);
+ }
- OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
- this.onlineAccounts.add(ourOnlineAccountData);
+ this.currentOnlineAccounts.clear();
+ addAccounts(replacementAccounts);
+ }
+
+ // Online accounts import queue
+
+ private void processOnlineAccountsImportQueue() {
+ if (this.onlineAccountsImportQueue.isEmpty())
+ // Nothing to do
+ return;
+
+ LOGGER.debug("Processing online accounts import queue (size: {})", this.onlineAccountsImportQueue.size());
+
+ Set onlineAccountsToAdd = new HashSet<>();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
+ if (isStopping)
+ return;
+
+ boolean isValid = this.isValidCurrentAccount(repository, onlineAccountData);
+ if (isValid)
+ onlineAccountsToAdd.add(onlineAccountData);
+
+ // Remove from queue
+ onlineAccountsImportQueue.remove(onlineAccountData);
}
+ } catch (DataException e) {
+ LOGGER.error("Repository issue while verifying online accounts", e);
+ }
+
+ if (!onlineAccountsToAdd.isEmpty()) {
+ LOGGER.debug("Merging {} validated online accounts from import queue", onlineAccountsToAdd.size());
+ addAccounts(onlineAccountsToAdd);
}
}
- private void performOnlineAccountsTasks() {
+ // Utilities
+
+ public static byte[] xorByteArrayInPlace(byte[] inplaceArray, byte[] otherArray) {
+ if (inplaceArray == null)
+ return Arrays.copyOf(otherArray, otherArray.length);
+
+ // Start from index 1 to enforce static leading byte
+ for (int i = 1; i < otherArray.length; i++)
+ inplaceArray[i] ^= otherArray[i];
+
+ return inplaceArray;
+ }
+
+ private static boolean isValidCurrentAccount(Repository repository, OnlineAccountData onlineAccountData) throws DataException {
+ final Long now = NTP.getTime();
+ if (now == null)
+ return false;
+
+ byte[] rewardSharePublicKey = onlineAccountData.getPublicKey();
+ long onlineAccountTimestamp = onlineAccountData.getTimestamp();
+
+ // Check timestamp is 'recent' here
+ if (Math.abs(onlineAccountTimestamp - now) > getOnlineTimestampModulus() * 2) {
+ LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
+ return false;
+ }
+
+ // Verify signature
+ byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
+ boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()
+ ? Qortal25519Extras.verifyAggregated(rewardSharePublicKey, onlineAccountData.getSignature(), data)
+ : Crypto.verify(rewardSharePublicKey, onlineAccountData.getSignature(), data);
+ if (!isSignatureValid) {
+ LOGGER.trace(() -> String.format("Rejecting invalid online account %s", Base58.encode(rewardSharePublicKey)));
+ return false;
+ }
+
+ // Qortal: check online account is actually reward-share
+ RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
+ if (rewardShareData == null) {
+ // Reward-share doesn't even exist - probably not a good sign
+ LOGGER.trace(() -> String.format("Rejecting unknown online reward-share public key %s", Base58.encode(rewardSharePublicKey)));
+ return false;
+ }
+
+ Account mintingAccount = new Account(repository, rewardShareData.getMinter());
+ if (!mintingAccount.canMint()) {
+ // Minting-account component of reward-share can no longer mint - disregard
+ LOGGER.trace(() -> String.format("Rejecting online reward-share with non-minting account %s", mintingAccount.getAddress()));
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Adds accounts, maybe rebuilds hashes, returns whether any new accounts were added / hashes rebuilt. */
+ private boolean addAccounts(Collection onlineAccountsToAdd) {
+ // For keeping track of which hashes to rebuild
+ Map> hashesToRebuild = new HashMap<>();
+
+ for (OnlineAccountData onlineAccountData : onlineAccountsToAdd) {
+ boolean isNewEntry = this.addAccount(onlineAccountData);
+
+ if (isNewEntry)
+ hashesToRebuild.computeIfAbsent(onlineAccountData.getTimestamp(), k -> new HashSet<>()).add(onlineAccountData.getPublicKey()[0]);
+ }
+
+ if (hashesToRebuild.isEmpty())
+ return false;
+
+ for (var entry : hashesToRebuild.entrySet()) {
+ Long timestamp = entry.getKey();
+
+ LOGGER.debug(() -> String.format("Rehashing for timestamp %d and leading bytes %s",
+ timestamp,
+ entry.getValue().stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
+ )
+ );
+
+ for (Byte leadingByte : entry.getValue()) {
+ byte[] pubkeyHash = currentOnlineAccounts.get(timestamp).stream()
+ .map(OnlineAccountData::getPublicKey)
+ .filter(publicKey -> leadingByte == publicKey[0])
+ .reduce(null, OnlineAccountsManager::xorByteArrayInPlace);
+
+ currentOnlineAccountsHashes.computeIfAbsent(timestamp, k -> new ConcurrentHashMap<>()).put(leadingByte, pubkeyHash);
+
+ LOGGER.trace(() -> String.format("Rebuilt hash %s for timestamp %d and leading byte %02x using %d public keys",
+ HashCode.fromBytes(pubkeyHash),
+ timestamp,
+ leadingByte,
+ currentOnlineAccounts.get(timestamp).stream()
+ .map(OnlineAccountData::getPublicKey)
+ .filter(publicKey -> leadingByte == publicKey[0])
+ .count()
+ ));
+ }
+ }
+
+ LOGGER.debug(String.format("we have online accounts for timestamps: %s", String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
+
+ return true;
+ }
+
+ private boolean addAccount(OnlineAccountData onlineAccountData) {
+ byte[] rewardSharePublicKey = onlineAccountData.getPublicKey();
+ long onlineAccountTimestamp = onlineAccountData.getTimestamp();
+
+ Set onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountTimestamp, k -> ConcurrentHashMap.newKeySet());
+ boolean isNewEntry = onlineAccounts.add(onlineAccountData);
+
+ if (isNewEntry)
+ LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
+ else
+ LOGGER.trace(() -> String.format("Not updating existing online account %s with timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
+
+ return isNewEntry;
+ }
+
+ /**
+ * Expire old entries.
+ */
+ private void expireOldOnlineAccounts() {
final Long now = NTP.getTime();
if (now == null)
return;
- // Expire old entries
- final long lastSeenExpiryPeriod = (getOnlineTimestampModulus() * 2) + (1 * 60 * 1000L);
- final long cutoffThreshold = now - lastSeenExpiryPeriod;
- synchronized (this.onlineAccounts) {
- Iterator iterator = this.onlineAccounts.iterator();
- while (iterator.hasNext()) {
- OnlineAccountData onlineAccountData = iterator.next();
-
- if (onlineAccountData.getTimestamp() < cutoffThreshold) {
- iterator.remove();
-
- LOGGER.trace(() -> {
- PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccountData.getPublicKey());
- return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp());
- });
- }
- }
- }
-
- // Request data from other peers?
- if ((this.onlineAccountsTasksTimestamp % ONLINE_ACCOUNTS_BROADCAST_INTERVAL) < ONLINE_ACCOUNTS_TASKS_INTERVAL) {
- List safeOnlineAccounts;
- synchronized (this.onlineAccounts) {
- safeOnlineAccounts = new ArrayList<>(this.onlineAccounts);
- }
-
- Message messageV1 = new GetOnlineAccountsMessage(safeOnlineAccounts);
- Message messageV2 = new GetOnlineAccountsV2Message(safeOnlineAccounts);
-
- Network.getInstance().broadcast(peer ->
- peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1
- );
- }
+ final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * getOnlineTimestampModulus();
+ this.currentOnlineAccounts.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
+ this.currentOnlineAccountsHashes.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
}
- private void sendOurOnlineAccountsInfo() {
+ /**
+ * Request data from other peers. (Pre-V3)
+ */
+ private void requestLegacyRemoteOnlineAccounts() {
final Long now = NTP.getTime();
+ if (now == null)
+ return;
+
+ // Don't bother if we're not up to date
+ if (!Controller.getInstance().isUpToDate())
+ return;
+
+ List mergedOnlineAccounts = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
+
+ Message messageV2 = new GetOnlineAccountsV2Message(mergedOnlineAccounts);
+
+ Network.getInstance().broadcast(peer ->
+ peer.getPeersVersion() < ONLINE_ACCOUNTS_V3_PEER_VERSION
+ ? messageV2
+ : null
+ );
+ }
+
+ /**
+ * Request data from other peers. V3+
+ */
+ private void requestRemoteOnlineAccounts() {
+ final Long now = NTP.getTime();
+ if (now == null)
+ return;
+
+ // Don't bother if we're not up to date
+ if (!Controller.getInstance().isUpToDate())
+ return;
+
+ Message messageV3 = new GetOnlineAccountsV3Message(currentOnlineAccountsHashes);
+
+ Network.getInstance().broadcast(peer ->
+ peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
+ ? messageV3
+ : null
+ );
+ }
+
+ /**
+ * Send online accounts that are minting on this node.
+ */
+ private void sendOurOnlineAccountsInfo() {
+ // 'current' timestamp
+ final Long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
+ if (onlineAccountsTimestamp == null)
+ return;
+
+ Long now = NTP.getTime();
if (now == null) {
return;
}
+ // Don't submit if we're more than 2 hours out of sync (unless we're in recovery mode)
+ final Long minLatestBlockTimestamp = now - (2 * 60 * 60 * 1000L);
+ if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp) && !Synchronizer.getInstance().getRecoveryMode()) {
+ return;
+ }
+
List mintingAccounts;
try (final Repository repository = RepositoryManager.getRepository()) {
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
- // We have no accounts, but don't reset timestamp
+ // We have no accounts to send
if (mintingAccounts.isEmpty())
return;
- // Only reward-share accounts allowed
+ // Only active reward-shares allowed
Iterator iterator = mintingAccounts.iterator();
- int i = 0;
while (iterator.hasNext()) {
MintingAccountData mintingAccountData = iterator.next();
@@ -316,107 +412,138 @@ public class OnlineAccountsManager extends Thread {
iterator.remove();
continue;
}
-
- if (++i > 1+1) {
- iterator.remove();
- continue;
- }
}
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
return;
}
- // 'current' timestamp
- final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now);
- boolean hasInfoChanged = false;
+ final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
List ourOnlineAccounts = new ArrayList<>();
- MINTING_ACCOUNTS:
for (MintingAccountData mintingAccountData : mintingAccounts) {
- PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey());
+ byte[] privateKey = mintingAccountData.getPrivateKey();
+ byte[] publicKey = Crypto.toPublicKey(privateKey);
- byte[] signature = mintingAccount.sign(timestampBytes);
- byte[] publicKey = mintingAccount.getPublicKey();
+ byte[] signature = useAggregateCompatibleSignature
+ ? Qortal25519Extras.signForAggregation(privateKey, timestampBytes)
+ : Crypto.sign(privateKey, timestampBytes);
// Our account is online
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
- synchronized (this.onlineAccounts) {
- Iterator iterator = this.onlineAccounts.iterator();
- while (iterator.hasNext()) {
- OnlineAccountData existingOnlineAccountData = iterator.next();
-
- if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) {
- // If our online account is already present, with same timestamp, then move on to next mintingAccount
- if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp)
- continue MINTING_ACCOUNTS;
-
- // If our online account is already present, but with older timestamp, then remove it
- iterator.remove();
- break;
- }
- }
-
- this.onlineAccounts.add(ourOnlineAccountData);
- }
-
- LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp));
ourOnlineAccounts.add(ourOnlineAccountData);
- hasInfoChanged = true;
}
+ this.hasOurOnlineAccounts = !ourOnlineAccounts.isEmpty();
+
+ boolean hasInfoChanged = addAccounts(ourOnlineAccounts);
+
if (!hasInfoChanged)
return;
Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts);
Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts);
+ Message messageV3 = new OnlineAccountsV2Message(ourOnlineAccounts); // TODO: V3 message
Network.getInstance().broadcast(peer ->
- peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1
+ peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
+ ? messageV3
+ : peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION
+ ? messageV2
+ : messageV1
);
- LOGGER.trace(() -> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp));
+ LOGGER.debug("Broadcasted {} online account{} with timestamp {}", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp);
}
- public static long toOnlineAccountTimestamp(long timestamp) {
- return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
+ /**
+ * Returns whether online accounts manager has any online accounts with timestamp recent enough to be considered currently online.
+ */
+ // BlockMinter: only calls this to check whether returned list is empty or not, to determine whether minting is even possible or not
+ public boolean hasOnlineAccounts() {
+ // 'current' timestamp
+ final Long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
+ if (onlineAccountsTimestamp == null)
+ return false;
+
+ return this.currentOnlineAccounts.containsKey(onlineAccountsTimestamp);
}
- /** Returns list of online accounts with timestamp recent enough to be considered currently online. */
+ public boolean hasOurOnlineAccounts() {
+ return this.hasOurOnlineAccounts;
+ }
+
+ /**
+ * Returns list of online accounts matching given timestamp.
+ */
+ // Block::mint() - only wants online accounts with (online) timestamp that matches block's (online) timestamp so they can be added to new block
+ public List getOnlineAccounts(long onlineTimestamp) {
+ LOGGER.info(String.format("caller's timestamp: %d, our timestamps: %s", onlineTimestamp, String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
+
+ return new ArrayList<>(Set.copyOf(this.currentOnlineAccounts.getOrDefault(onlineTimestamp, Collections.emptySet())));
+ }
+
+ /**
+ * Returns list of online accounts with timestamp recent enough to be considered currently online.
+ */
+ // API: calls this to return list of online accounts - probably expects ALL timestamps - but going to get 'current' from now on
public List getOnlineAccounts() {
- final long onlineTimestamp = toOnlineAccountTimestamp(NTP.getTime());
+ // 'current' timestamp
+ final Long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
+ if (onlineAccountsTimestamp == null)
+ return Collections.emptyList();
- synchronized (this.onlineAccounts) {
- return this.onlineAccounts.stream().filter(account -> account.getTimestamp() == onlineTimestamp).collect(Collectors.toList());
- }
+ return getOnlineAccounts(onlineAccountsTimestamp);
}
+ // Block processing
- /** Returns cached, unmodifiable list of latest block's online accounts. */
- public List getLatestBlocksOnlineAccounts() {
- synchronized (this.latestBlocksOnlineAccounts) {
- return this.latestBlocksOnlineAccounts.peekFirst();
- }
+ /**
+ * Removes previously validated entries from block's online accounts.
+ *
+ * Checks both 'current' and block caches.
+ *
+ * Typically called by {@link Block#areOnlineAccountsValid()}
+ */
+ public void removeKnown(Set blocksOnlineAccounts, Long timestamp) {
+ Set onlineAccounts = this.currentOnlineAccounts.get(timestamp);
+
+ // If not 'current' timestamp - try block cache instead
+ if (onlineAccounts == null)
+ onlineAccounts = this.latestBlocksOnlineAccounts.get(timestamp);
+
+ if (onlineAccounts != null)
+ blocksOnlineAccounts.removeAll(onlineAccounts);
}
- /** Caches list of latest block's online accounts. Typically called by Block.process() */
- public void pushLatestBlocksOnlineAccounts(List latestBlocksOnlineAccounts) {
- synchronized (this.latestBlocksOnlineAccounts) {
- if (this.latestBlocksOnlineAccounts.size() == MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS)
- this.latestBlocksOnlineAccounts.pollLast();
-
- this.latestBlocksOnlineAccounts.addFirst(latestBlocksOnlineAccounts == null
- ? Collections.emptyList()
- : Collections.unmodifiableList(latestBlocksOnlineAccounts));
+ /**
+ * Adds block's online accounts to one of OnlineAccountManager's caches.
+ *
+ * It is assumed that the online accounts have been verified.
+ *
+ * Typically called by {@link Block#areOnlineAccountsValid()}
+ */
+ public void addBlocksOnlineAccounts(Set blocksOnlineAccounts, Long timestamp) {
+ // We want to add to 'current' in preference if possible
+ if (this.currentOnlineAccounts.containsKey(timestamp)) {
+ addAccounts(blocksOnlineAccounts);
+ return;
}
- }
- /** Reverts list of latest block's online accounts. Typically called by Block.orphan() */
- public void popLatestBlocksOnlineAccounts() {
- synchronized (this.latestBlocksOnlineAccounts) {
- this.latestBlocksOnlineAccounts.pollFirst();
+ // Add to block cache instead
+ this.latestBlocksOnlineAccounts.computeIfAbsent(timestamp, k -> ConcurrentHashMap.newKeySet())
+ .addAll(blocksOnlineAccounts);
+
+ // If block cache has grown too large then we need to trim.
+ if (this.latestBlocksOnlineAccounts.size() > MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS) {
+ // However, be careful to trim the opposite end to the entry we just added!
+ Long firstKey = this.latestBlocksOnlineAccounts.firstKey();
+ if (!firstKey.equals(timestamp))
+ this.latestBlocksOnlineAccounts.remove(firstKey);
+ else
+ this.latestBlocksOnlineAccounts.remove(this.latestBlocksOnlineAccounts.lastKey());
}
}
@@ -429,45 +556,48 @@ public class OnlineAccountsManager extends Thread {
List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
- List accountsToSend;
- synchronized (this.onlineAccounts) {
- accountsToSend = new ArrayList<>(this.onlineAccounts);
- }
+ List accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
+ int prefilterSize = accountsToSend.size();
Iterator iterator = accountsToSend.iterator();
-
- SEND_ITERATOR:
while (iterator.hasNext()) {
OnlineAccountData onlineAccountData = iterator.next();
- for (int i = 0; i < excludeAccounts.size(); ++i) {
- OnlineAccountData excludeAccountData = excludeAccounts.get(i);
-
+ for (OnlineAccountData excludeAccountData : excludeAccounts) {
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
iterator.remove();
- continue SEND_ITERATOR;
+ break;
}
}
}
+ if (accountsToSend.isEmpty())
+ return;
+
Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
peer.sendMessage(onlineAccountsMessage);
- LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer));
+ LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
}
public void onNetworkOnlineAccountsMessage(Peer peer, Message message) {
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
- LOGGER.trace(() -> String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer));
+ LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
- try (final Repository repository = RepositoryManager.getRepository()) {
- for (OnlineAccountData onlineAccountData : peersOnlineAccounts)
- this.verifyAndAddAccount(repository, onlineAccountData);
- } catch (DataException e) {
- LOGGER.error(String.format("Repository issue while verifying online accounts from peer %s", peer), e);
+ int importCount = 0;
+
+ // Add any online accounts to the queue that aren't already present
+ for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
+ boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
+
+ if (isNewEntry)
+ importCount++;
}
+
+ if (importCount > 0)
+ LOGGER.debug("Added {} online accounts to queue", importCount);
}
public void onNetworkGetOnlineAccountsV2Message(Peer peer, Message message) {
@@ -476,58 +606,106 @@ public class OnlineAccountsManager extends Thread {
List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
- List accountsToSend;
- synchronized (this.onlineAccounts) {
- accountsToSend = new ArrayList<>(this.onlineAccounts);
- }
+ List accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
+ int prefilterSize = accountsToSend.size();
Iterator iterator = accountsToSend.iterator();
-
- SEND_ITERATOR:
while (iterator.hasNext()) {
OnlineAccountData onlineAccountData = iterator.next();
- for (int i = 0; i < excludeAccounts.size(); ++i) {
- OnlineAccountData excludeAccountData = excludeAccounts.get(i);
-
+ for (OnlineAccountData excludeAccountData : excludeAccounts) {
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
iterator.remove();
- continue SEND_ITERATOR;
+ break;
}
}
}
+ if (accountsToSend.isEmpty())
+ return;
+
Message onlineAccountsMessage = new OnlineAccountsV2Message(accountsToSend);
peer.sendMessage(onlineAccountsMessage);
- LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer));
+ LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
}
public void onNetworkOnlineAccountsV2Message(Peer peer, Message message) {
OnlineAccountsV2Message onlineAccountsMessage = (OnlineAccountsV2Message) message;
List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
- LOGGER.debug(String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer));
+ LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
int importCount = 0;
// Add any online accounts to the queue that aren't already present
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
+ boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
- // Do we already know about this online account data?
- if (onlineAccounts.contains(onlineAccountData)) {
- continue;
- }
-
- // Is it already in the import queue?
- if (onlineAccountsImportQueue.contains(onlineAccountData)) {
- continue;
- }
-
- onlineAccountsImportQueue.add(onlineAccountData);
- importCount++;
+ if (isNewEntry)
+ importCount++;
}
- LOGGER.debug(String.format("Added %d online accounts to queue", importCount));
+ if (importCount > 0)
+ LOGGER.debug("Added {} online accounts to queue", importCount);
+ }
+
+ public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
+ GetOnlineAccountsV3Message getOnlineAccountsMessage = (GetOnlineAccountsV3Message) message;
+
+ Map> peersHashes = getOnlineAccountsMessage.getHashesByTimestampThenByte();
+ List outgoingOnlineAccounts = new ArrayList<>();
+
+ // Warning: no double-checking/fetching - we must be ConcurrentMap compatible!
+ // So no contains()-then-get() or multiple get()s on the same key/map.
+ // We also use getOrDefault() with emptySet() on currentOnlineAccounts in case corresponding timestamp entry isn't there.
+ for (var ourOuterMapEntry : currentOnlineAccountsHashes.entrySet()) {
+ Long timestamp = ourOuterMapEntry.getKey();
+
+ var ourInnerMap = ourOuterMapEntry.getValue();
+ var peersInnerMap = peersHashes.get(timestamp);
+
+ if (peersInnerMap == null) {
+ // Peer doesn't have this timestamp, so if it's valid (i.e. not too old) then we'd have to send all of ours
+ Set timestampsOnlineAccounts = this.currentOnlineAccounts.getOrDefault(timestamp, Collections.emptySet());
+ outgoingOnlineAccounts.addAll(timestampsOnlineAccounts);
+
+ LOGGER.debug(() -> String.format("Going to send all %d online accounts for timestamp %d", timestampsOnlineAccounts.size(), timestamp));
+ } else {
+ // Quick cache of which leading bytes to send so we only have to filter once
+ Set outgoingLeadingBytes = new HashSet<>();
+
+ // We have entries for this timestamp so compare against peer's entries
+ for (var ourInnerMapEntry : ourInnerMap.entrySet()) {
+ Byte leadingByte = ourInnerMapEntry.getKey();
+ byte[] peersHash = peersInnerMap.get(leadingByte);
+
+ if (!Arrays.equals(ourInnerMapEntry.getValue(), peersHash)) {
+ // For this leading byte: hashes don't match or peer doesn't have entry
+ // Send all online accounts for this timestamp and leading byte
+ outgoingLeadingBytes.add(leadingByte);
+ }
+ }
+
+ int beforeAddSize = outgoingOnlineAccounts.size();
+
+ this.currentOnlineAccounts.getOrDefault(timestamp, Collections.emptySet()).stream()
+ .filter(account -> outgoingLeadingBytes.contains(account.getPublicKey()[0]))
+ .forEach(outgoingOnlineAccounts::add);
+
+ if (outgoingOnlineAccounts.size() > beforeAddSize)
+ LOGGER.debug(String.format("Going to send %d online accounts for timestamp %d and leading bytes %s",
+ outgoingOnlineAccounts.size() - beforeAddSize,
+ timestamp,
+ outgoingLeadingBytes.stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
+ )
+ );
+ }
+ }
+
+ Message onlineAccountsMessage = new OnlineAccountsV2Message(outgoingOnlineAccounts); // TODO: V3 message
+ peer.sendMessage(onlineAccountsMessage);
+
+ LOGGER.debug("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
}
}
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 8f3a34bb..74a4a785 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -26,14 +26,7 @@ import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.network.Network;
import org.qortal.network.Peer;
-import org.qortal.network.message.BlockMessage;
-import org.qortal.network.message.BlockSummariesMessage;
-import org.qortal.network.message.GetBlockMessage;
-import org.qortal.network.message.GetBlockSummariesMessage;
-import org.qortal.network.message.GetSignaturesV2Message;
-import org.qortal.network.message.Message;
-import org.qortal.network.message.SignaturesMessage;
-import org.qortal.network.message.MessageType;
+import org.qortal.network.message.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -88,7 +81,7 @@ public class Synchronizer extends Thread {
private boolean syncRequestPending = false;
// Keep track of invalid blocks so that we don't keep trying to sync them
- private Map invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
+ private Map invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
public Long timeValidBlockLastReceived = null;
public Long timeInvalidBlockLastReceived = null;
@@ -178,8 +171,8 @@ public class Synchronizer extends Thread {
public Integer getSyncPercent() {
synchronized (this.syncLock) {
- // Report as 100% synced if the latest block is within the last 30 mins
- final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
+ // Report as 100% synced if the latest block is within the last 60 mins
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
return 100;
}
@@ -624,7 +617,7 @@ public class Synchronizer extends Thread {
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
for (Peer peer : peersSharingCommonBlock) {
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
- this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
+ //this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
}
continue;
}
@@ -635,7 +628,9 @@ public class Synchronizer extends Thread {
int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary);
// Fetch block summaries from each peer
- for (Peer peer : peersSharingCommonBlock) {
+ Iterator peersSharingCommonBlockIterator = peersSharingCommonBlock.iterator();
+ while (peersSharingCommonBlockIterator.hasNext()) {
+ Peer peer = (Peer) peersSharingCommonBlockIterator.next();
// If we're shutting down, just return the latest peer list
if (Controller.isStopping())
@@ -692,6 +687,8 @@ public class Synchronizer extends Thread {
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
peers.remove(peer);
+ peersSharingCommonBlockIterator.remove();
+ continue;
}
// Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
@@ -847,6 +844,10 @@ public class Synchronizer extends Thread {
/* Invalid block signature tracking */
+ public Map getInvalidBlockSignatures() {
+ return this.invalidBlockSignatures;
+ }
+
private void addInvalidBlockSignature(byte[] signature) {
Long now = NTP.getTime();
if (now == null) {
@@ -854,8 +855,7 @@ public class Synchronizer extends Thread {
}
// Add or update existing entry
- String sig58 = Base58.encode(signature);
- invalidBlockSignatures.put(sig58, now);
+ invalidBlockSignatures.put(ByteArray.wrap(signature), now);
}
private void deleteOlderInvalidSignatures(Long now) {
if (now == null) {
@@ -874,17 +874,16 @@ public class Synchronizer extends Thread {
}
}
}
- private boolean containsInvalidBlockSummary(List blockSummaries) {
+ public boolean containsInvalidBlockSummary(List blockSummaries) {
if (blockSummaries == null || invalidBlockSignatures == null) {
return false;
}
// Loop through our known invalid blocks and check each one against supplied block summaries
- for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
- byte[] invalidSignature = Base58.decode(invalidSignature58);
+ for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
for (BlockSummaryData blockSummary : blockSummaries) {
byte[] signature = blockSummary.getSignature();
- if (Arrays.equals(signature, invalidSignature)) {
+ if (Arrays.equals(signature, invalidSignature.value)) {
return true;
}
}
@@ -897,10 +896,9 @@ public class Synchronizer extends Thread {
}
// Loop through our known invalid blocks and check each one against supplied block signatures
- for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
- byte[] invalidSignature = Base58.decode(invalidSignature58);
+ for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
for (byte[] signature : blockSignatures) {
- if (Arrays.equals(signature, invalidSignature)) {
+ if (Arrays.equals(signature, invalidSignature.value)) {
return true;
}
}
@@ -1579,12 +1577,23 @@ public class Synchronizer extends Thread {
Message getBlockMessage = new GetBlockMessage(signature);
Message message = peer.getResponse(getBlockMessage);
- if (message == null || message.getType() != MessageType.BLOCK)
+ if (message == null)
return null;
- BlockMessage blockMessage = (BlockMessage) message;
+ switch (message.getType()) {
+ case BLOCK: {
+ BlockMessage blockMessage = (BlockMessage) message;
+ return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
+ }
- return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
+ case BLOCK_V2: {
+ BlockV2Message blockMessage = (BlockV2Message) message;
+ return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStatesHash());
+ }
+
+ default:
+ return null;
+ }
}
public void populateBlockSummariesMinterLevels(Repository repository, List blockSummaries) throws DataException {
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java
index a0b4886b..60b3707b 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileListManager.java
@@ -67,6 +67,9 @@ public class ArbitraryDataFileListManager {
/** Maximum number of hops that a file list relay request is allowed to make */
public static int RELAY_REQUEST_MAX_HOPS = 4;
+ /** Minimum peer version to use relay */
+ public static String RELAY_MIN_PEER_VERSION = "3.4.0";
+
private ArbitraryDataFileListManager() {
}
@@ -524,6 +527,7 @@ public class ArbitraryDataFileListManager {
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
}
+ forwardArbitraryDataFileListMessage.setId(message.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
@@ -690,12 +694,14 @@ public class ArbitraryDataFileListManager {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
+ relayGetArbitraryDataFileListMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
- broadcastPeer -> broadcastPeer == peer ||
- Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
- ? null : relayGetArbitraryDataFileListMessage);
+ broadcastPeer ->
+ !broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
+ broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
+ );
}
else {
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryMetadataManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryMetadataManager.java
index 0903de60..eec0d935 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryMetadataManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryMetadataManager.java
@@ -22,8 +22,7 @@ import org.qortal.utils.Triple;
import java.io.IOException;
import java.util.*;
-import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_DURATION;
-import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_HOPS;
+import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
public class ArbitraryMetadataManager {
@@ -339,6 +338,7 @@ public class ArbitraryMetadataManager {
if (requestingPeer != null) {
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
+ forwardArbitraryMetadataMessage.setId(arbitraryMetadataMessage.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
@@ -434,12 +434,13 @@ public class ArbitraryMetadataManager {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
+ relayGetArbitraryMetadataMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
- broadcastPeer -> broadcastPeer == peer ||
- Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
- ? null : relayGetArbitraryMetadataMessage);
+ broadcastPeer ->
+ !broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
+ broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
}
else {
diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
index 938141e0..2c448607 100644
--- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
@@ -242,8 +242,8 @@ public class TradeBot implements Listener {
if (!(event instanceof Synchronizer.NewChainTipEvent))
return;
- // Don't process trade bots or broadcast presence timestamps if our chain is more than 30 minutes old
- final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
+ // Don't process trade bots or broadcast presence timestamps if our chain is more than 60 minutes old
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
return;
@@ -292,7 +292,7 @@ public class TradeBot implements Listener {
}
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
- return PrivateKeyAccount.toPublicKey(privateKey);
+ return Crypto.toPublicKey(privateKey);
}
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index f66ea939..56c5b409 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -375,7 +375,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
long balance = 0;
- Comparator oldestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp);
+ Comparator oldestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp);
List transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
for (SimpleTransaction transaction : transactions) {
balance += transaction.getTotalAmount();
@@ -455,7 +455,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Process new keys
} while (true);
- Comparator newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
+ Comparator newestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp).reversed();
// Update cache and return
transactionsCacheTimestamp = NTP.getTime();
@@ -537,7 +537,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// All inputs and outputs relate to this wallet, so the balance should be unaffected
amount = 0;
}
- return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
+ long timestampMillis = t.timestamp * 1000L;
+ return new SimpleTransaction(t.txHash, timestampMillis, amount, fee, inputs, outputs);
}
/**
diff --git a/src/main/java/org/qortal/crosschain/SimpleTransaction.java b/src/main/java/org/qortal/crosschain/SimpleTransaction.java
index 27c9f9e3..53039020 100644
--- a/src/main/java/org/qortal/crosschain/SimpleTransaction.java
+++ b/src/main/java/org/qortal/crosschain/SimpleTransaction.java
@@ -7,7 +7,7 @@ import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleTransaction {
private String txHash;
- private Integer timestamp;
+ private Long timestamp;
private long totalAmount;
private long feeAmount;
private List inputs;
@@ -74,7 +74,7 @@ public class SimpleTransaction {
public SimpleTransaction() {
}
- public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List inputs, List