diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip
index 1f579a9c..7af02485 100755
--- a/WindowsInstaller/Qortal.aip
+++ b/WindowsInstaller/Qortal.aip
@@ -17,10 +17,10 @@
-
+
-
+
@@ -212,7 +212,7 @@
-
+
diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
new file mode 100644
index 00000000..c2c3d355
Binary files /dev/null and b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar differ
diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
new file mode 100644
index 00000000..0dc1aedc
--- /dev/null
+++ b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
@@ -0,0 +1,9 @@
+
+
+ 4.0.0
+ org.ciyam
+ AT
+ 1.4.0
+ POM was created from install:install-file
+
diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml
index 8f8b1f6e..063c735d 100644
--- a/lib/org/ciyam/AT/maven-metadata-local.xml
+++ b/lib/org/ciyam/AT/maven-metadata-local.xml
@@ -3,14 +3,15 @@
org.ciyamAT
- 1.3.8
+ 1.4.01.3.41.3.51.3.61.3.71.3.8
+ 1.4.0
- 20200925114415
+ 20221105114346
diff --git a/pom.xml b/pom.xml
index eb306420..12f8472c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0org.qortalqortal
- 3.6.4
+ 3.8.4jartrue
@@ -11,7 +11,7 @@
0.15.101.69${maven.build.timestamp}
- 1.3.8
+ 1.4.03.61.82.6
diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java
index c3a25fb6..2c75dbc0 100644
--- a/src/main/java/org/qortal/account/Account.java
+++ b/src/main/java/org/qortal/account/Account.java
@@ -211,7 +211,8 @@ public class Account {
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
return true;
- if (Account.isFounder(accountData.getFlags()))
+ // Founders can always mint, unless they have a penalty
+ if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
return true;
return false;
@@ -222,6 +223,11 @@ public class Account {
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
}
+ /** Returns account's blockMintedPenalty or null if account not found in repository. */
+ public Integer getBlocksMintedPenalty() throws DataException {
+ return this.repository.getAccountRepository().getBlocksMintedPenaltyCount(this.address);
+ }
+
/** Returns whether account can build reward-shares.
*
@@ -243,7 +249,7 @@ public class Account {
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
return true;
- if (Account.isFounder(accountData.getFlags()))
+ if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
return true;
return false;
@@ -271,7 +277,7 @@ public class Account {
/**
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
*
- * For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
+ * For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
*
* @return 0+
* @throws DataException
@@ -281,7 +287,8 @@ public class Account {
if (accountData == null)
return 0;
- if (Account.isFounder(accountData.getFlags()))
+ // Founders are assigned a different effective minting level, as long as they have no penalty
+ if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
return accountData.getLevel();
@@ -289,8 +296,6 @@ public class Account {
/**
* Returns 'effective' minting level, or zero if reward-share does not exist.
- *
- * this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
*
* @param repository
* @param rewardSharePublicKey
@@ -309,7 +314,7 @@ public class Account {
/**
* Returns 'effective' minting level, with a fix for the zero level.
*
- * For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
+ * For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
*
* @param repository
* @param rewardSharePublicKey
@@ -322,7 +327,7 @@ public class Account {
if (rewardShareData == null)
return 0;
- else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
+ else if (!rewardShareData.getMinter().equals(rewardShareData.getRecipient())) // Sponsorship reward share
return 0;
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
diff --git a/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
new file mode 100644
index 00000000..725e53f5
--- /dev/null
+++ b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
@@ -0,0 +1,367 @@
+package org.qortal.account;
+
+import org.qortal.api.resource.TransactionsResource;
+import org.qortal.asset.Asset;
+import org.qortal.data.account.AccountData;
+import org.qortal.data.naming.NameData;
+import org.qortal.data.transaction.*;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.transaction.Transaction.TransactionType;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class SelfSponsorshipAlgoV1 {
+
+ private final Repository repository;
+ private final String address;
+ private final AccountData accountData;
+ private final long snapshotTimestamp;
+ private final boolean override;
+
+ private int registeredNameCount = 0;
+ private int suspiciousCount = 0;
+ private int suspiciousPercent = 0;
+ private int consolidationCount = 0;
+ private int bulkIssuanceCount = 0;
+ private int recentSponsorshipCount = 0;
+
+ private List sponsorshipRewardShares = new ArrayList<>();
+ private final Map> paymentsByAddress = new HashMap<>();
+ private final Set sponsees = new LinkedHashSet<>();
+ private Set consolidatedAddresses = new LinkedHashSet<>();
+ private final Set zeroTransactionAddreses = new LinkedHashSet<>();
+ private final Set penaltyAddresses = new LinkedHashSet<>();
+
+ public SelfSponsorshipAlgoV1(Repository repository, String address, long snapshotTimestamp, boolean override) throws DataException {
+ this.repository = repository;
+ this.address = address;
+ this.accountData = this.repository.getAccountRepository().getAccount(this.address);
+ this.snapshotTimestamp = snapshotTimestamp;
+ this.override = override;
+ }
+
+ public String getAddress() {
+ return this.address;
+ }
+
+ public Set getPenaltyAddresses() {
+ return this.penaltyAddresses;
+ }
+
+
+ public void run() throws DataException {
+ if (this.accountData == null) {
+ // Nothing to do
+ return;
+ }
+
+ this.fetchSponsorshipRewardShares();
+ if (this.sponsorshipRewardShares.isEmpty()) {
+ // Nothing to do
+ return;
+ }
+
+ this.findConsolidatedRewards();
+ this.findBulkIssuance();
+ this.findRegisteredNameCount();
+ this.findRecentSponsorshipCount();
+
+ int score = this.calculateScore();
+ if (score <= 0 && !override) {
+ return;
+ }
+
+ String newAddress = this.getDestinationAccount(this.address);
+ while (newAddress != null) {
+ // Found destination account
+ this.penaltyAddresses.add(newAddress);
+
+ // Run algo for this address, but in "override" mode because it has already been flagged
+ SelfSponsorshipAlgoV1 algoV1 = new SelfSponsorshipAlgoV1(this.repository, newAddress, this.snapshotTimestamp, true);
+ algoV1.run();
+ this.penaltyAddresses.addAll(algoV1.getPenaltyAddresses());
+
+ newAddress = this.getDestinationAccount(newAddress);
+ }
+
+ this.penaltyAddresses.add(this.address);
+
+ if (this.override || this.recentSponsorshipCount < 20) {
+ this.penaltyAddresses.addAll(this.consolidatedAddresses);
+ this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
+ }
+ else {
+ this.penaltyAddresses.addAll(this.sponsees);
+ }
+ }
+
+ private String getDestinationAccount(String address) throws DataException {
+ List transferPrivsTransactions = fetchTransferPrivsForAddress(address);
+ if (transferPrivsTransactions.isEmpty()) {
+ // No TRANSFER_PRIVS transactions for this address
+ return null;
+ }
+
+ AccountData accountData = this.repository.getAccountRepository().getAccount(address);
+ if (accountData == null) {
+ return null;
+ }
+
+ for (TransactionData transactionData : transferPrivsTransactions) {
+ TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
+ if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
+ return transferPrivsTransactionData.getRecipient();
+ }
+ }
+
+ return null;
+ }
+
+ private void findConsolidatedRewards() throws DataException {
+ List sponseesThatSentRewards = new ArrayList<>();
+ Map paymentRecipients = new HashMap<>();
+
+ // Collect outgoing payments of each sponsee
+ for (String sponseeAddress : this.sponsees) {
+
+ // Firstly fetch all payments for address, since the functions below depend on this data
+ this.fetchPaymentsForAddress(sponseeAddress);
+
+ // Check if the address has zero relevant transactions
+ if (this.hasZeroTransactions(sponseeAddress)) {
+ this.zeroTransactionAddreses.add(sponseeAddress);
+ }
+
+ // Get payment recipients
+ List allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
+ if (allPaymentRecipients.isEmpty()) {
+ continue;
+ }
+ sponseesThatSentRewards.add(sponseeAddress);
+
+ List addressesPaidByThisSponsee = new ArrayList<>();
+ for (String paymentRecipient : allPaymentRecipients) {
+ if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
+ // We already tracked this association - don't allow multiple to stack up
+ continue;
+ }
+ addressesPaidByThisSponsee.add(paymentRecipient);
+
+ // Increment count for this recipient, or initialize to 1 if not present
+ if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
+ paymentRecipients.put(paymentRecipient, 1);
+ }
+ }
+
+ }
+
+ // Exclude addresses with a low number of payments
+ Map filteredPaymentRecipients = paymentRecipients.entrySet().stream()
+ .filter(p -> p.getValue() != null && p.getValue() >= 10)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ // Now check how many sponsees have sent to this subset of addresses
+ Map sponseesThatConsolidatedRewards = new HashMap<>();
+ for (String sponseeAddress : sponseesThatSentRewards) {
+ List allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
+ // Remove any that aren't to one of the flagged recipients (i.e. consolidation)
+ allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
+
+ int count = allPaymentRecipients.size();
+ if (count == 0) {
+ continue;
+ }
+ if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
+ sponseesThatConsolidatedRewards.put(sponseeAddress, count);
+ }
+ }
+
+ // Remove sponsees that have only sent a low number of payments to the filtered addresses
+ Map filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
+ .filter(p -> p.getValue() != null && p.getValue() >= 2)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ this.consolidationCount = sponseesThatConsolidatedRewards.size();
+ this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
+ this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
+ this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
+ }
+
+ private void findBulkIssuance() {
+ Long lastTimestamp = null;
+ for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
+ long timestamp = rewardShareTransactionData.getTimestamp();
+ if (timestamp >= this.snapshotTimestamp) {
+ continue;
+ }
+
+ if (lastTimestamp != null) {
+ if (timestamp - lastTimestamp < 3*60*1000L) {
+ this.bulkIssuanceCount++;
+ }
+ }
+ lastTimestamp = timestamp;
+ }
+ }
+
+ private void findRegisteredNameCount() throws DataException {
+ int registeredNameCount = 0;
+ for (String sponseeAddress : sponsees) {
+ List names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
+ for (NameData name : names) {
+ if (name.getRegistered() < this.snapshotTimestamp) {
+ registeredNameCount++;
+ break;
+ }
+ }
+ }
+ this.registeredNameCount = registeredNameCount;
+ }
+
+ private void findRecentSponsorshipCount() {
+ final long referenceTimestamp = this.snapshotTimestamp - (365 * 24 * 60 * 60 * 1000L);
+ int recentSponsorshipCount = 0;
+ for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
+ if (rewardShare.getTimestamp() >= referenceTimestamp) {
+ recentSponsorshipCount++;
+ }
+ }
+ this.recentSponsorshipCount = recentSponsorshipCount;
+ }
+
+ private int calculateScore() {
+ final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
+ final int nameMultiplier = (this.sponsees.size() >= 50 && this.registeredNameCount == 0) ? 2 : 1;
+ final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
+ final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
+ final int offset = 9;
+ return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
+ }
+
+ private void fetchSponsorshipRewardShares() throws DataException {
+ List sponsorshipRewardShares = new ArrayList<>();
+
+ // Define relevant transactions
+ List txTypes = List.of(TransactionType.REWARD_SHARE);
+ List transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
+
+ for (TransactionData transactionData : transactionDataList) {
+ if (transactionData.getType() != TransactionType.REWARD_SHARE) {
+ continue;
+ }
+
+ RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
+
+ // Skip removals
+ if (rewardShareTransactionData.getSharePercent() < 0) {
+ continue;
+ }
+
+ // Skip if not sponsored by this account
+ if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
+ continue;
+ }
+
+ // Skip self shares
+ if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
+ continue;
+ }
+
+ boolean duplicateFound = false;
+ for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
+ if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
+ // Duplicate
+ duplicateFound = true;
+ break;
+ }
+ }
+ if (!duplicateFound) {
+ sponsorshipRewardShares.add(rewardShareTransactionData);
+ this.sponsees.add(rewardShareTransactionData.getRecipient());
+ }
+ }
+
+ this.sponsorshipRewardShares = sponsorshipRewardShares;
+ }
+
+ private List fetchTransferPrivsForAddress(String address) throws DataException {
+ return fetchTransactions(repository,
+ List.of(TransactionType.TRANSFER_PRIVS),
+ address, true);
+ }
+
+ private void fetchPaymentsForAddress(String address) throws DataException {
+ List payments = fetchTransactions(repository,
+ Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
+ address, false);
+ this.paymentsByAddress.put(address, payments);
+ }
+
+ private List fetchOutgoingPaymentRecipientsForAddress(String address) {
+ List outgoingPaymentRecipients = new ArrayList<>();
+
+ List transactionDataList = this.paymentsByAddress.get(address);
+ if (transactionDataList == null) transactionDataList = new ArrayList<>();
+ transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
+ for (TransactionData transactionData : transactionDataList) {
+ switch (transactionData.getType()) {
+
+ case PAYMENT:
+ PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
+ if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
+ // Outgoing payment from this account
+ outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
+ }
+ break;
+
+ case TRANSFER_ASSET:
+ TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
+ if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
+ if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
+ // Outgoing payment from this account
+ outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return outgoingPaymentRecipients;
+ }
+
+ private boolean hasZeroTransactions(String address) {
+ List transactionDataList = this.paymentsByAddress.get(address);
+ if (transactionDataList == null) {
+ return true;
+ }
+ transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
+ return transactionDataList.size() == 0;
+ }
+
+ private static List fetchTransactions(Repository repository, List txTypes, String address, boolean reverse) throws DataException {
+ // Fetch all relevant transactions for this account
+ List signatures = repository.getTransactionRepository()
+ .getSignaturesMatchingCriteria(null, null, null, txTypes,
+ null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
+ null, null, reverse);
+
+ List transactionDataList = new ArrayList<>();
+
+ for (byte[] signature : signatures) {
+ // Fetch transaction data
+ TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
+ if (transactionData == null) {
+ continue;
+ }
+ transactionDataList.add(transactionData);
+ }
+
+ return transactionDataList;
+ }
+
+}
diff --git a/src/main/java/org/qortal/api/model/AccountPenaltyStats.java b/src/main/java/org/qortal/api/model/AccountPenaltyStats.java
new file mode 100644
index 00000000..aafe25fc
--- /dev/null
+++ b/src/main/java/org/qortal/api/model/AccountPenaltyStats.java
@@ -0,0 +1,56 @@
+package org.qortal.api.model;
+
+import org.qortal.block.SelfSponsorshipAlgoV1Block;
+import org.qortal.data.account.AccountData;
+import org.qortal.data.naming.NameData;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import java.util.ArrayList;
+import java.util.List;
+
+@XmlAccessorType(XmlAccessType.FIELD)
+public class AccountPenaltyStats {
+
+ public Integer totalPenalties;
+ public Integer maxPenalty;
+ public Integer minPenalty;
+ public String penaltyHash;
+
+ protected AccountPenaltyStats() {
+ }
+
+ public AccountPenaltyStats(Integer totalPenalties, Integer maxPenalty, Integer minPenalty, String penaltyHash) {
+ this.totalPenalties = totalPenalties;
+ this.maxPenalty = maxPenalty;
+ this.minPenalty = minPenalty;
+ this.penaltyHash = penaltyHash;
+ }
+
+ public static AccountPenaltyStats fromAccounts(List accounts) {
+ int totalPenalties = 0;
+ Integer maxPenalty = null;
+ Integer minPenalty = null;
+
+ List addresses = new ArrayList<>();
+ for (AccountData accountData : accounts) {
+ int penalty = accountData.getBlocksMintedPenalty();
+ addresses.add(accountData.getAddress());
+ totalPenalties++;
+
+ // Penalties are expressed as a negative number, so the min and the max are reversed here
+ if (maxPenalty == null || penalty < maxPenalty) maxPenalty = penalty;
+ if (minPenalty == null || penalty > minPenalty) minPenalty = penalty;
+ }
+
+ String penaltyHash = SelfSponsorshipAlgoV1Block.getHash(addresses);
+ return new AccountPenaltyStats(totalPenalties, maxPenalty, minPenalty, penaltyHash);
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("totalPenalties: %d, maxPenalty: %d, minPenalty: %d, penaltyHash: %s", totalPenalties, maxPenalty, minPenalty, penaltyHash == null ? "null" : penaltyHash);
+ }
+}
diff --git a/src/main/java/org/qortal/api/model/ConnectedPeer.java b/src/main/java/org/qortal/api/model/ConnectedPeer.java
index 3d383321..c4198654 100644
--- a/src/main/java/org/qortal/api/model/ConnectedPeer.java
+++ b/src/main/java/org/qortal/api/model/ConnectedPeer.java
@@ -1,6 +1,7 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
+import org.qortal.controller.Controller;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Handshake;
@@ -36,6 +37,7 @@ public class ConnectedPeer {
public Long lastBlockTimestamp;
public UUID connectionId;
public String age;
+ public Boolean isTooDivergent;
protected ConnectedPeer() {
}
@@ -69,6 +71,11 @@ public class ConnectedPeer {
this.lastBlockSignature = peerChainTipData.getSignature();
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
}
+
+ // Only include isTooDivergent decision if we've had the opportunity to request block summaries this peer
+ if (peer.getLastTooDivergentTime() != null) {
+ this.isTooDivergent = Controller.wasRecentlyTooDivergent.test(peer);
+ }
}
}
diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java
index 468b90a8..79cb6e05 100644
--- a/src/main/java/org/qortal/api/resource/AddressesResource.java
+++ b/src/main/java/org/qortal/api/resource/AddressesResource.java
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
@@ -27,6 +28,7 @@ import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
+import org.qortal.api.model.AccountPenaltyStats;
import org.qortal.api.model.ApiOnlineAccount;
import org.qortal.api.model.RewardShareKeyRequest;
import org.qortal.asset.Asset;
@@ -34,6 +36,7 @@ import org.qortal.controller.LiteNode;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
+import org.qortal.data.account.AccountPenaltyData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.network.OnlineAccountLevel;
@@ -471,6 +474,54 @@ public class AddressesResource {
}
}
+ @GET
+ @Path("/penalties")
+ @Operation(
+ summary = "Get addresses with penalties",
+ description = "Returns a list of accounts with a blocksMintedPenalty",
+ responses = {
+ @ApiResponse(
+ description = "accounts with penalties",
+ content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyData.class)))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
+ public List getAccountsWithPenalties() {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ List accounts = repository.getAccountRepository().getPenaltyAccounts();
+ List penalties = accounts.stream().map(a -> new AccountPenaltyData(a.getAddress(), a.getBlocksMintedPenalty())).collect(Collectors.toList());
+
+ return penalties;
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
+ @GET
+ @Path("/penalties/stats")
+ @Operation(
+ summary = "Get stats about current penalties",
+ responses = {
+ @ApiResponse(
+ description = "aggregated stats about accounts with penalties",
+ content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyStats.class)))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
+ public AccountPenaltyStats getPenaltyStats() {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ List accounts = repository.getAccountRepository().getPenaltyAccounts();
+ return AccountPenaltyStats.fromAccounts(accounts);
+
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
@POST
@Path("/publicize")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
index 978183c0..25b968f1 100644
--- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java
+++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java
@@ -1128,7 +1128,7 @@ public class ArbitraryResource {
if (path == null) {
// See if we have a string instead
if (string != null) {
- File tempFile = File.createTempFile("qortal-", ".tmp");
+ File tempFile = File.createTempFile("qortal-", "");
tempFile.deleteOnExit();
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
writer.write(string);
@@ -1138,7 +1138,7 @@ public class ArbitraryResource {
}
// ... or base64 encoded raw data
else if (base64 != null) {
- File tempFile = File.createTempFile("qortal-", ".tmp");
+ File tempFile = File.createTempFile("qortal-", "");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), Base64.decode(base64));
path = tempFile.toPath().toString();
diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java
index 195b2ca4..15541802 100644
--- a/src/main/java/org/qortal/api/resource/BlocksResource.java
+++ b/src/main/java/org/qortal/api/resource/BlocksResource.java
@@ -634,13 +634,16 @@ public class BlocksResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
- public List getBlockRange(@PathParam("height") int height, @Parameter(
- ref = "count"
- ) @QueryParam("count") int count) {
+ public List getBlockRange(@PathParam("height") int height,
+ @Parameter(ref = "count") @QueryParam("count") int count,
+ @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
+ @QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
try (final Repository repository = RepositoryManager.getRepository()) {
List blocks = new ArrayList<>();
+ boolean shouldReverse = (reverse != null && reverse == true);
- for (/* count already set */; count > 0; --count, ++height) {
+ int i = 0;
+ while (i < count) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null) {
// Not found - try the archive
@@ -650,8 +653,14 @@ public class BlocksResource {
break;
}
}
+ if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
+ blockData.setOnlineAccountsSignatures(null);
+ }
blocks.add(blockData);
+
+ height = shouldReverse ? height - 1 : height + 1;
+ i++;
}
return blocks;
diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java
index ee2a8599..2601e938 100644
--- a/src/main/java/org/qortal/api/resource/ChatResource.java
+++ b/src/main/java/org/qortal/api/resource/ChatResource.java
@@ -70,6 +70,8 @@ public class ChatResource {
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("involving") List involvingAddresses,
@QueryParam("reference") String reference,
+ @QueryParam("chatreference") String chatReference,
+ @QueryParam("haschatreference") Boolean hasChatReference,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
@@ -92,12 +94,18 @@ public class ChatResource {
if (reference != null)
referenceBytes = Base58.decode(reference);
+ byte[] chatReferenceBytes = null;
+ if (chatReference != null)
+ chatReferenceBytes = Base58.decode(chatReference);
+
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getMessagesMatchingCriteria(
before,
after,
txGroupId,
referenceBytes,
+ chatReferenceBytes,
+ hasChatReference,
involvingAddresses,
limit, offset, reverse);
} catch (DataException e) {
diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
index 80d19804..dd967451 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
@@ -68,7 +68,7 @@ public class CrossChainBitcoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
- Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
+ Long balance = bitcoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
diff --git a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
index 57049639..31d51c73 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
@@ -68,7 +68,7 @@ public class CrossChainDigibyteResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
- Long balance = digibyte.getWalletBalanceFromTransactions(key58);
+ Long balance = digibyte.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
diff --git a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
index 189a53d3..28bebfb8 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
@@ -66,7 +66,7 @@ public class CrossChainDogecoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
- Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
+ Long balance = dogecoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
index 664b013a..45b92c7c 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
@@ -8,11 +8,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
-import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
@@ -25,7 +24,6 @@ import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.qortal.api.*;
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
-import org.qortal.controller.Controller;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@@ -586,98 +584,103 @@ public class CrossChainHtlcResource {
}
List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
- TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
- if (tradeBotData == null)
+ List tradeBotDataList = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).collect(Collectors.toList());
+ if (tradeBotDataList == null || tradeBotDataList.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
- int lockTime = tradeBotData.getLockTimeA();
+ // Loop through all matching entries for this AT address, as there might be more than one
+ for (TradeBotData tradeBotData : tradeBotDataList) {
- // We can't refund P2SH-A until lockTime-A has passed
- if (NTP.getTime() <= lockTime * 1000L)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
+ if (tradeBotData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
- int medianBlockTime = bitcoiny.getMedianBlockTime();
- if (medianBlockTime <= lockTime)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
+ Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
+ int lockTime = tradeBotData.getLockTimeA();
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
- long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ // We can't refund P2SH-A until lockTime-A has passed
+ if (NTP.getTime() <= lockTime * 1000L)
+ continue;
- // Create redeem script based on destination chain
- byte[] redeemScriptA;
- String p2shAddressA;
- BitcoinyHTLC.Status htlcStatusA;
- if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
- redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
- htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
- }
- else {
- redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
- htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
- }
- LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
+ // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
+ int medianBlockTime = bitcoiny.getMedianBlockTime();
+ if (medianBlockTime <= lockTime)
+ continue;
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-A to be funded...
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
+ long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // Too late!
- return false;
+ // Create redeem script based on destination chain
+ byte[] redeemScriptA;
+ String p2shAddressA;
+ BitcoinyHTLC.Status htlcStatusA;
+ if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
+ redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
+ htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+ } else {
+ redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
+ htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+ }
+ LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
- case FUNDED:{
- Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // Still waiting for P2SH-A to be funded...
+ continue;
- if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
- // Pirate Chain custom integration
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // Too late!
+ continue;
- PirateChain pirateChain = PirateChain.getInstance();
- String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
+ case FUNDED: {
+ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- // Get funding txid
- String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
- if (fundingTxidHex == null) {
- throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
+ if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
+ // Pirate Chain custom integration
+
+ PirateChain pirateChain = PirateChain.getInstance();
+ String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
+
+ // Get funding txid
+ String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+ if (fundingTxidHex == null) {
+ throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
+ }
+ String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
+
+ byte[] privateKey = tradeBotData.getTradePrivateKey();
+ String privateKey58 = Base58.encode(privateKey);
+ String redeemScript58 = Base58.encode(redeemScriptA);
+
+ String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
+ receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
+ LOGGER.info("Refund txid: {}", txid);
+ } else {
+ // ElectrumX coins
+
+ ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
+
+ // Validate the destination foreign blockchain address
+ Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
+ if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
+ fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
+
+ bitcoiny.broadcastTransaction(p2shRefundTransaction);
}
- String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
- byte[] privateKey = tradeBotData.getTradePrivateKey();
- String privateKey58 = Base58.encode(privateKey);
- String redeemScript58 = Base58.encode(redeemScriptA);
-
- String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
- receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
- LOGGER.info("Refund txid: {}", txid);
+ return true;
}
- else {
- // ElectrumX coins
-
- ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
-
- // Validate the destination foreign blockchain address
- Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
- if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
- fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
-
- bitcoiny.broadcastTransaction(p2shRefundTransaction);
- }
-
- return true;
}
}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
index 8ac0f9a0..d12dd94c 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
@@ -68,7 +68,7 @@ public class CrossChainLitecoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
- Long balance = litecoin.getWalletBalanceFromTransactions(key58);
+ Long balance = litecoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
diff --git a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
index 756b0bb5..97550392 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
@@ -68,7 +68,7 @@ public class CrossChainRavencoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
- Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
+ Long balance = ravencoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
diff --git a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
index 9760b7f0..76ed936c 100644
--- a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
+++ b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
@@ -47,6 +47,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
txGroupId,
null,
null,
+ null,
+ null,
null, null, null);
sendMessages(session, chatMessages);
@@ -74,6 +76,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
null,
null,
null,
+ null,
+ null,
involvingAddresses,
null, null, null);
diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataBuilder.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataBuilder.java
index 4f0e3835..b6b17ea5 100644
--- a/src/main/java/org/qortal/arbitrary/ArbitraryDataBuilder.java
+++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataBuilder.java
@@ -2,6 +2,7 @@ package org.qortal.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.qortal.arbitrary.exception.DataNotPublishedException;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
import org.qortal.arbitrary.misc.Service;
@@ -88,7 +89,7 @@ public class ArbitraryDataBuilder {
if (latestPut == null) {
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
this.name, this.service, this.identifierString());
- throw new DataException(message);
+ throw new DataNotPublishedException(message);
}
this.latestPutTransaction = latestPut;
diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
index 5d4b015c..d1a8b4f5 100644
--- a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
+++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java
@@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.qortal.arbitrary.exception.DataNotPublishedException;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
@@ -169,10 +170,18 @@ public class ArbitraryDataReader {
this.uncompress();
this.validate();
+ } catch (DataNotPublishedException e) {
+ if (e.getMessage() != null) {
+ // Log the message only, to avoid spamming the logs with a full stack trace
+ LOGGER.debug("DataNotPublishedException when trying to load QDN resource: {}", e.getMessage());
+ }
+ this.deleteWorkingDirectory();
+ throw e;
+
} catch (DataException e) {
LOGGER.info("DataException when trying to load QDN resource", e);
this.deleteWorkingDirectory();
- throw new DataException(e.getMessage());
+ throw e;
} finally {
this.postExecute();
diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java
index 616c9b03..2720e4b2 100644
--- a/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java
+++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataResource.java
@@ -3,6 +3,7 @@ package org.qortal.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
+import org.qortal.arbitrary.exception.DataNotPublishedException;
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
@@ -325,7 +326,7 @@ public class ArbitraryDataResource {
if (latestPut == null) {
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
this.resourceId, this.service, this.identifierString());
- throw new DataException(message);
+ throw new DataNotPublishedException(message);
}
this.latestPutTransaction = latestPut;
diff --git a/src/main/java/org/qortal/arbitrary/exception/DataNotPublishedException.java b/src/main/java/org/qortal/arbitrary/exception/DataNotPublishedException.java
new file mode 100644
index 00000000..4782826b
--- /dev/null
+++ b/src/main/java/org/qortal/arbitrary/exception/DataNotPublishedException.java
@@ -0,0 +1,22 @@
+package org.qortal.arbitrary.exception;
+
+import org.qortal.repository.DataException;
+
+public class DataNotPublishedException extends DataException {
+
+ public DataNotPublishedException() {
+ }
+
+ public DataNotPublishedException(String message) {
+ super(message);
+ }
+
+ public DataNotPublishedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public DataNotPublishedException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/src/main/java/org/qortal/arbitrary/misc/Service.java b/src/main/java/org/qortal/arbitrary/misc/Service.java
index 5dd8d94e..01419d2f 100644
--- a/src/main/java/org/qortal/arbitrary/misc/Service.java
+++ b/src/main/java/org/qortal/arbitrary/misc/Service.java
@@ -10,9 +10,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -20,9 +18,52 @@ import static java.util.stream.Collectors.toMap;
public enum Service {
AUTO_UPDATE(1, false, null, null),
ARBITRARY_DATA(100, false, null, null),
+ QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
+ @Override
+ public ValidationResult validate(Path path) throws IOException {
+ ValidationResult superclassResult = super.validate(path);
+ if (superclassResult != ValidationResult.OK) {
+ return superclassResult;
+ }
+
+ // Custom validation function to require a single file, with a whitelisted extension
+ int fileCount = 0;
+ File[] files = path.toFile().listFiles();
+ // If already a single file, replace the list with one that contains that file only
+ if (files == null && path.toFile().isFile()) {
+ files = new File[] { path.toFile() };
+ }
+ if (files != null) {
+ for (File file : files) {
+ if (file.getName().equals(".qortal")) {
+ continue;
+ }
+ if (file.isDirectory()) {
+ return ValidationResult.DIRECTORIES_NOT_ALLOWED;
+ }
+ final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
+ // We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
+ final List allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
+ if (extension == null || !allowedExtensions.contains(extension)) {
+ return ValidationResult.INVALID_FILE_EXTENSION;
+ }
+ fileCount++;
+ }
+ }
+ if (fileCount != 1) {
+ return ValidationResult.INVALID_FILE_COUNT;
+ }
+ return ValidationResult.OK;
+ }
+ },
WEBSITE(200, true, null, null) {
@Override
- public ValidationResult validate(Path path) {
+ public ValidationResult validate(Path path) throws IOException {
+ ValidationResult superclassResult = super.validate(path);
+ if (superclassResult != ValidationResult.OK) {
+ return superclassResult;
+ }
+
// Custom validation function to require an index HTML file in the root directory
List fileNames = ArbitraryDataRenderer.indexFiles();
String[] files = path.toFile().list();
@@ -53,12 +94,24 @@ public enum Service {
METADATA(1100, false, null, null),
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
@Override
- public ValidationResult validate(Path path) {
+ public ValidationResult validate(Path path) throws IOException {
+ ValidationResult superclassResult = super.validate(path);
+ if (superclassResult != ValidationResult.OK) {
+ return superclassResult;
+ }
+
// Custom validation function to require .gif files only, and at least 1
int gifCount = 0;
File[] files = path.toFile().listFiles();
+ // If already a single file, replace the list with one that contains that file only
+ if (files == null && path.toFile().isFile()) {
+ files = new File[] { path.toFile() };
+ }
if (files != null) {
for (File file : files) {
+ if (file.getName().equals(".qortal")) {
+ continue;
+ }
if (file.isDirectory()) {
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
}
@@ -143,7 +196,8 @@ public enum Service {
MISSING_INDEX_FILE(4),
DIRECTORIES_NOT_ALLOWED(5),
INVALID_FILE_EXTENSION(6),
- MISSING_DATA(7);
+ MISSING_DATA(7),
+ INVALID_FILE_COUNT(8);
public final int value;
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index 5e838458..3f306b93 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -136,7 +136,7 @@ public class Block {
}
/** Lazy-instantiated expanded info on block's online accounts. */
- private static class ExpandedAccount {
+ public static class ExpandedAccount {
private final RewardShareData rewardShareData;
private final int sharePercent;
private final boolean isRecipientAlsoMinter;
@@ -169,6 +169,13 @@ public class Block {
}
}
+ public Account getMintingAccount() {
+ return this.mintingAccount;
+ }
+ public Account getRecipientAccount() {
+ return this.recipientAccount;
+ }
+
/**
* Returns share bin for expanded account.
*
@@ -363,12 +370,26 @@ public class Block {
return null;
}
+ int height = parentBlockData.getHeight() + 1;
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
// Fetch our list of online accounts, removing any that are missing a nonce
List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
+
+ // After feature trigger, remove any online accounts that are level 0
+ if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
+ onlineAccounts.removeIf(a -> {
+ try {
+ return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
+ } catch (DataException e) {
+ // Something went wrong, so remove the account
+ return true;
+ }
+ });
+ }
+
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
@@ -435,7 +456,6 @@ public class Block {
int transactionCount = 0;
byte[] transactionsSignature = null;
- int height = parentBlockData.getHeight() + 1;
int atCount = 0;
long atFees = 0;
@@ -1029,6 +1049,15 @@ public class Block {
if (onlineRewardShares == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
+ // After feature trigger, require all online account minters to be greater than level 0
+ if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
+ List expandedAccounts = this.getExpandedAccounts();
+ for (ExpandedAccount account : expandedAccounts) {
+ if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
+ return ValidationResult.ONLINE_ACCOUNTS_INVALID;
+ }
+ }
+
// If block is past a certain age then we simply assume the signatures were correct
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
@@ -1434,6 +1463,9 @@ public class Block {
if (this.blockData.getHeight() == 212937)
// Apply fix for block 212937
Block212937.processFix(this);
+
+ else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
+ SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
}
// We're about to (test-)process a batch of transactions,
@@ -1490,19 +1522,23 @@ public class Block {
// Batch update in repository
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
+ // Keep track of level bumps in case we need to apply to other entries
+ Map bumpedAccounts = new HashMap<>();
+
// Local changes and also checks for level bump
for (AccountData accountData : allUniqueExpandedAccounts) {
// Adjust count locally (in Java)
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
- final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
+ final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
if (newLevel > accountData.getLevel()) {
// Account has increased in level!
accountData.setLevel(newLevel);
+ bumpedAccounts.put(accountData.getAddress(), newLevel);
repository.getAccountRepository().setLevel(accountData);
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
}
@@ -1510,6 +1546,25 @@ public class Block {
break;
}
}
+
+ // Also bump other entries if need be
+ if (!bumpedAccounts.isEmpty()) {
+ for (ExpandedAccount expandedAccount : expandedAccounts) {
+ Integer newLevel = bumpedAccounts.get(expandedAccount.mintingAccountData.getAddress());
+ if (newLevel != null && expandedAccount.mintingAccountData.getLevel() != newLevel) {
+ expandedAccount.mintingAccountData.setLevel(newLevel);
+ LOGGER.trace("Also bumped {} to level {}", expandedAccount.mintingAccountData.getAddress(), newLevel);
+ }
+
+ if (!expandedAccount.isRecipientAlsoMinter) {
+ newLevel = bumpedAccounts.get(expandedAccount.recipientAccountData.getAddress());
+ if (newLevel != null && expandedAccount.recipientAccountData.getLevel() != newLevel) {
+ expandedAccount.recipientAccountData.setLevel(newLevel);
+ LOGGER.trace("Also bumped {} to level {}", expandedAccount.recipientAccountData.getAddress(), newLevel);
+ }
+ }
+ }
+ }
}
protected void processBlockRewards() throws DataException {
@@ -1669,6 +1724,9 @@ public class Block {
// Revert fix for block 212937
Block212937.orphanFix(this);
+ else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
+ SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
+
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();
@@ -1797,7 +1855,7 @@ public class Block {
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
- final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
+ final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index 5e1f44f3..b96350e6 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -74,7 +74,11 @@ public class BlockChain {
transactionV5Timestamp,
transactionV6Timestamp,
disableReferenceTimestamp,
- increaseOnlineAccountsDifficultyTimestamp;
+ increaseOnlineAccountsDifficultyTimestamp,
+ onlineAccountMinterLevelValidationHeight,
+ selfSponsorshipAlgoV1Height,
+ feeValidationFixTimestamp,
+ chatReferenceTimestamp;
}
// Custom transaction fees
@@ -96,6 +100,13 @@ public class BlockChain {
/** Whether only one registered name is allowed per account. */
private boolean oneNamePerAccount = false;
+ /** Checkpoints */
+ public static class Checkpoint {
+ public int height;
+ public String signature;
+ }
+ private List checkpoints;
+
/** Block rewards by block height */
public static class RewardByHeight {
public int height;
@@ -196,6 +207,9 @@ public class BlockChain {
* featureTriggers because unit tests need to set this value via Reflection. */
private long onlineAccountsModulusV2Timestamp;
+ /** Snapshot timestamp for self sponsorship algo V1 */
+ private long selfSponsorshipAlgoV1SnapshotTimestamp;
+
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -356,6 +370,11 @@ public class BlockChain {
return this.onlineAccountsModulusV2Timestamp;
}
+ // Self sponsorship algo
+ public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
+ return this.selfSponsorshipAlgoV1SnapshotTimestamp;
+ }
+
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -369,6 +388,10 @@ public class BlockChain {
return this.oneNamePerAccount;
}
+ public List getCheckpoints() {
+ return this.checkpoints;
+ }
+
public List getBlockRewardsByHeight() {
return this.rewardsByHeight;
}
@@ -483,6 +506,22 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
}
+ public int getSelfSponsorshipAlgoV1Height() {
+ return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
+ }
+
+ public long getOnlineAccountMinterLevelValidationHeight() {
+ return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
+ }
+
+ public long getFeeValidationFixTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.feeValidationFixTimestamp.name()).longValue();
+ }
+
+ public long getChatReferenceTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
+ }
+
// More complex getters for aspects that change by height or timestamp
@@ -651,6 +690,7 @@ public class BlockChain {
boolean isTopOnly = Settings.getInstance().isTopOnly();
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
+ boolean isLite = Settings.getInstance().isLite();
boolean canBootstrap = Settings.getInstance().getBootstrap();
boolean needsArchiveRebuild = false;
BlockData chainTip;
@@ -671,22 +711,44 @@ public class BlockChain {
}
}
}
+
+ // Validate checkpoints
+ // Limited to topOnly nodes for now, in order to reduce risk, and to solve a real-world problem with divergent topOnly nodes
+ // TODO: remove the isTopOnly conditional below once this feature has had more testing time
+ if (isTopOnly && !isLite) {
+ List checkpoints = BlockChain.getInstance().getCheckpoints();
+ for (Checkpoint checkpoint : checkpoints) {
+ BlockData blockData = repository.getBlockRepository().fromHeight(checkpoint.height);
+ if (blockData == null) {
+ // Try the archive
+ blockData = repository.getBlockArchiveRepository().fromHeight(checkpoint.height);
+ }
+ if (blockData == null) {
+ LOGGER.trace("Couldn't find block for height {}", checkpoint.height);
+ // This is likely due to the block being pruned, so is safe to ignore.
+ // Continue, as there might be other blocks we can check more definitively.
+ continue;
+ }
+
+ byte[] signature = Base58.decode(checkpoint.signature);
+ if (!Arrays.equals(signature, blockData.getSignature())) {
+ LOGGER.info("Error: block at height {} with signature {} doesn't match checkpoint sig: {}. Bootstrapping...", checkpoint.height, Base58.encode(blockData.getSignature()), checkpoint.signature);
+ needsArchiveRebuild = true;
+ break;
+ }
+ LOGGER.info("Block at height {} matches checkpoint signature", blockData.getHeight());
+ }
+ }
+
}
- boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
+ // Check first block is Genesis Block
+ if (!isGenesisBlockValid() || needsArchiveRebuild) {
+ try {
+ rebuildBlockchain();
- if (isTopOnly && hasBlocks) {
- // Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned
- // It's best not to validate it, and there's no real need to
- } else {
- // Check first block is Genesis Block
- if (!isGenesisBlockValid() || needsArchiveRebuild) {
- try {
- rebuildBlockchain();
-
- } catch (InterruptedException e) {
- throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
- }
+ } catch (InterruptedException e) {
+ throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
}
}
@@ -695,9 +757,7 @@ public class BlockChain {
try (final Repository repository = RepositoryManager.getRepository()) {
repository.checkConsistency();
- // Set the number of blocks to validate based on the pruned state of the chain
- // If pruned, subtract an extra 10 to allow room for error
- int blocksToValidate = (isTopOnly || archiveEnabled) ? Settings.getInstance().getPruneBlockLimit() - 10 : 1440;
+ int blocksToValidate = Math.min(Settings.getInstance().getPruneBlockLimit() - 10, 1440);
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
new file mode 100644
index 00000000..a9a016b6
--- /dev/null
+++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
@@ -0,0 +1,133 @@
+package org.qortal.block;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.account.SelfSponsorshipAlgoV1;
+import org.qortal.api.model.AccountPenaltyStats;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.account.AccountData;
+import org.qortal.data.account.AccountPenaltyData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.utils.Base58;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Self Sponsorship AlgoV1 Block
+ *
+ * Selected block for the initial run on the "self sponsorship detection algorithm"
+ */
+public final class SelfSponsorshipAlgoV1Block {
+
+ private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
+
+
+ private SelfSponsorshipAlgoV1Block() {
+ /* Do not instantiate */
+ }
+
+ public static void processAccountPenalties(Block block) throws DataException {
+ LOGGER.info("Running algo for block processing - this will take a while...");
+ logPenaltyStats(block.repository);
+ long startTime = System.currentTimeMillis();
+ Set penalties = getAccountPenalties(block.repository, -5000000);
+ block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
+ long totalTime = System.currentTimeMillis() - startTime;
+ String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
+ LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
+ logPenaltyStats(block.repository);
+
+ int updatedCount = updateAccountLevels(block.repository, penalties);
+ LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
+ }
+
+ public static void orphanAccountPenalties(Block block) throws DataException {
+ LOGGER.info("Running algo for block orphaning - this will take a while...");
+ logPenaltyStats(block.repository);
+ long startTime = System.currentTimeMillis();
+ Set penalties = getAccountPenalties(block.repository, 5000000);
+ block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
+ long totalTime = System.currentTimeMillis() - startTime;
+ String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
+ LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
+ logPenaltyStats(block.repository);
+
+ int updatedCount = updateAccountLevels(block.repository, penalties);
+ LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
+ }
+
+ public static Set getAccountPenalties(Repository repository, int penalty) throws DataException {
+ final long snapshotTimestamp = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
+ Set penalties = new LinkedHashSet<>();
+ List addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
+ for (String address : addresses) {
+ //System.out.println(String.format("address: %s", address));
+ SelfSponsorshipAlgoV1 selfSponsorshipAlgoV1 = new SelfSponsorshipAlgoV1(repository, address, snapshotTimestamp, false);
+ selfSponsorshipAlgoV1.run();
+ //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV1.getPenaltyAddresses().size()));
+
+ for (String penaltyAddress : selfSponsorshipAlgoV1.getPenaltyAddresses()) {
+ penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
+ }
+ }
+ return penalties;
+ }
+
+ private static int updateAccountLevels(Repository repository, Set accountPenalties) throws DataException {
+ final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
+ final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
+
+ int updatedCount = 0;
+
+ for (AccountPenaltyData penaltyData : accountPenalties) {
+ AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
+ final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
+
+ // Shortcut for penalties
+ if (effectiveBlocksMinted < 0) {
+ accountData.setLevel(0);
+ repository.getAccountRepository().setLevel(accountData);
+ updatedCount++;
+ LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
+ continue;
+ }
+
+ for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
+ if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
+ accountData.setLevel(newLevel);
+ repository.getAccountRepository().setLevel(accountData);
+ updatedCount++;
+ LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
+ break;
+ }
+ }
+ }
+
+ return updatedCount;
+ }
+
+ private static void logPenaltyStats(Repository repository) {
+ try {
+ LOGGER.info(getPenaltyStats(repository));
+
+ } catch (DataException e) {}
+ }
+
+ private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
+ List accounts = repository.getAccountRepository().getPenaltyAccounts();
+ return AccountPenaltyStats.fromAccounts(accounts);
+ }
+
+ public static String getHash(List penaltyAddresses) {
+ if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
+ return null;
+ }
+ Collections.sort(penaltyAddresses);
+ return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 7e3b4b9e..185dd7cd 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -26,9 +26,6 @@ import org.qortal.data.block.CommonBlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
-import org.qortal.network.message.BlockSummariesV2Message;
-import org.qortal.network.message.HeightV2Message;
-import org.qortal.network.message.Message;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -38,6 +35,8 @@ import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
+import static org.junit.Assert.assertNotNull;
+
// Minting new blocks
public class BlockMinter extends Thread {
@@ -64,8 +63,8 @@ public class BlockMinter extends Thread {
public void run() {
Thread.currentThread().setName("BlockMinter");
- if (Settings.getInstance().isLite()) {
- // Lite nodes do not mint
+ if (Settings.getInstance().isTopOnly() || Settings.getInstance().isLite()) {
+ // Top only and lite nodes do not sign blocks
return;
}
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
@@ -511,6 +510,21 @@ public class BlockMinter extends Thread {
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
+ Block block = mintTestingBlockRetainingTimestamps(repository, mintingAccount);
+ assertNotNull("Minted block must not be null", block);
+
+ return block;
+ }
+
+ public static Block mintTestingBlockUnvalidated(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
+ if (!BlockChain.getInstance().isTestChain())
+ throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
+
+ // Ensure mintingAccount is 'online' so blocks can be minted
+ OnlineAccountsManager.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
+
+ PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
+
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
}
@@ -518,6 +532,8 @@ public class BlockMinter extends Thread {
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
+ if (newBlock == null)
+ return null;
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index bcd010e8..e9e1fcc2 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
+import org.qortal.account.Account;
import org.qortal.api.ApiService;
import org.qortal.api.DomainMapService;
import org.qortal.api.GatewayService;
@@ -756,6 +757,28 @@ public class Controller extends Thread {
return peer.isAtLeastVersion(minPeerVersion) == false;
};
+ public static final Predicate hasInvalidSigner = peer -> {
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
+ if (peerChainTipData == null)
+ return true;
+
+ try (Repository repository = RepositoryManager.getRepository()) {
+ return Account.getRewardShareEffectiveMintingLevel(repository, peerChainTipData.getMinterPublicKey()) == 0;
+ } catch (DataException e) {
+ return true;
+ }
+ };
+
+ public static final Predicate wasRecentlyTooDivergent = peer -> {
+ Long now = NTP.getTime();
+ Long peerLastTooDivergentTime = peer.getLastTooDivergentTime();
+ if (now == null || peerLastTooDivergentTime == null)
+ return false;
+
+ // Exclude any peers that were TOO_DIVERGENT in the last 5 mins
+ return (now - peerLastTooDivergentTime < 5 * 60 * 1000L);
+ };
+
private long getRandomRepositoryMaintenanceInterval() {
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index cd9483e9..2dad62e7 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -247,6 +247,9 @@ public class Synchronizer extends Thread {
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
peers.removeIf(Controller.hasInferiorChainTip);
+ // Disregard peers that have a block with an invalid signer
+ peers.removeIf(Controller.hasInvalidSigner);
+
final int peersBeforeComparison = peers.size();
// Request recent block summaries from the remaining peers, and locate our common block with each
@@ -1118,6 +1121,7 @@ public class Synchronizer extends Thread {
// If common block is too far behind us then we're on massively different forks so give up.
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
+ peer.setLastTooDivergentTime(NTP.getTime());
return SynchronizationResult.TOO_DIVERGENT;
}
@@ -1127,6 +1131,9 @@ public class Synchronizer extends Thread {
testHeight = Math.max(testHeight - step, 1);
}
+ // Peer not considered too divergent
+ peer.setLastTooDivergentTime(0L);
+
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
blockSummariesFromCommon.add(0, testBlockSummary);
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
index 30b0fcca..e2de1ae0 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
@@ -82,7 +82,7 @@ public class ArbitraryDataFileManager extends Thread {
try {
// Use a fixed thread pool to execute the arbitrary data file requests
- int threadCount = 10;
+ int threadCount = 5;
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
@@ -288,7 +288,7 @@ public class ArbitraryDataFileManager extends Thread {
// The ID needs to match that of the original request
message.setId(originalMessage.getId());
- if (!requestingPeer.sendMessage(message)) {
+ if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
requestingPeer.disconnect("failed to forward arbitrary data file");
}
@@ -564,13 +564,16 @@ public class ArbitraryDataFileManager extends Thread {
LOGGER.trace("Hash {} exists", hash58);
// We can serve the file directly as we already have it
+ LOGGER.debug("Sending file {}...", arbitraryDataFile);
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
arbitraryDataFileMessage.setId(message.getId());
- if (!peer.sendMessage(arbitraryDataFileMessage)) {
- LOGGER.debug("Couldn't sent file");
+ if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
+ LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
peer.disconnect("failed to send file");
}
- LOGGER.debug("Sent file {}", arbitraryDataFile);
+ else {
+ LOGGER.debug("Sent file {}", arbitraryDataFile);
+ }
}
else if (relayInfo != null) {
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
diff --git a/src/main/java/org/qortal/controller/repository/AtStatesPruner.java b/src/main/java/org/qortal/controller/repository/AtStatesPruner.java
index bd12f784..064fe0ea 100644
--- a/src/main/java/org/qortal/controller/repository/AtStatesPruner.java
+++ b/src/main/java/org/qortal/controller/repository/AtStatesPruner.java
@@ -42,6 +42,7 @@ public class AtStatesPruner implements Runnable {
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
+ repository.saveChanges();
while (!Controller.isStopping()) {
repository.discardChanges();
diff --git a/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java b/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java
index 69fa347c..6c026385 100644
--- a/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java
+++ b/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java
@@ -29,6 +29,7 @@ public class AtStatesTrimmer implements Runnable {
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
+ repository.saveChanges();
while (!Controller.isStopping()) {
repository.discardChanges();
diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
index a31a1a28..a4ae921e 100644
--- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+ // Do this in a new thread so caller doesn't have to wait for computeNonce()
+ // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
+ new Thread(() -> {
+ try (final Repository threadsRepository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
- messageTransaction.computeNonce();
- messageTransaction.sign(sender);
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
- // reset repository state to prevent deadlock
- repository.discardChanges();
- ValidationResult result = messageTransaction.importAsUnconfirmed();
+ // reset repository state to prevent deadlock
+ threadsRepository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
- if (result != ValidationResult.OK) {
- LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
- return ResponseResult.NETWORK_ISSUE;
- }
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ }
+ } catch (DataException e) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
+ }
+ }, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index 350779bc..c08bd91e 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -357,19 +357,33 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
- // It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
- return this.getWalletBalanceFromTransactions(key58);
+ Long balance = 0L;
-// Context.propagate(bitcoinjContext);
-//
-// Wallet wallet = walletFromDeterministicKey58(key58);
-// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
-//
-// Coin balance = wallet.getBalance();
-// if (balance == null)
-// return null;
-//
-// return balance.value;
+ List allUnspentOutputs = new ArrayList<>();
+ Set walletAddresses = this.getWalletAddresses(key58);
+ for (String address : walletAddresses) {
+ allUnspentOutputs.addAll(this.getUnspentOutputs(address));
+ }
+ for (TransactionOutput output : allUnspentOutputs) {
+ if (!output.isAvailableForSpending()) {
+ continue;
+ }
+ balance += output.getValue().value;
+ }
+ return balance;
+ }
+
+ public Long getWalletBalanceFromBitcoinj(String key58) {
+ Context.propagate(bitcoinjContext);
+
+ Wallet wallet = walletFromDeterministicKey58(key58);
+ wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
+
+ Coin balance = wallet.getBalance();
+ if (balance == null)
+ return null;
+
+ return balance.value;
}
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
@@ -464,6 +478,64 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
+ public Set getWalletAddresses(String key58) throws ForeignBlockchainException {
+ synchronized (this) {
+ Context.propagate(bitcoinjContext);
+
+ Wallet wallet = walletFromDeterministicKey58(key58);
+ DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
+
+ keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
+ keyChain.maybeLookAhead();
+
+ List keys = new ArrayList<>(keyChain.getLeafKeys());
+
+ Set keySet = new HashSet<>();
+
+ int unusedCounter = 0;
+ int ki = 0;
+ do {
+ boolean areAllKeysUnused = true;
+
+ for (; ki < keys.size(); ++ki) {
+ DeterministicKey dKey = keys.get(ki);
+
+ // Check for transactions
+ Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
+ keySet.add(address.toString());
+ byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
+
+ // Ask for transaction history - if it's empty then key has never been used
+ List historicTransactionHashes = this.getAddressTransactions(script, false);
+
+ if (!historicTransactionHashes.isEmpty()) {
+ areAllKeysUnused = false;
+ }
+ }
+
+ if (areAllKeysUnused) {
+ // No transactions
+ if (unusedCounter >= Settings.getInstance().getGapLimit()) {
+ // ... and we've hit our search limit
+ break;
+ }
+ // We haven't hit our search limit yet so increment the counter and keep looking
+ unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
+ } else {
+ // Some keys in this batch were used, so reset the counter
+ unusedCounter = 0;
+ }
+
+ // Generate some more keys
+ keys.addAll(generateMoreKeys(keyChain));
+
+ // Process new keys
+ } while (true);
+
+ return keySet;
+ }
+ }
+
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set keySet) {
long amount = 0;
long total = 0L;
diff --git a/src/main/java/org/qortal/crosschain/Digibyte.java b/src/main/java/org/qortal/crosschain/Digibyte.java
index 3ab5e78e..4358b3b3 100644
--- a/src/main/java/org/qortal/crosschain/Digibyte.java
+++ b/src/main/java/org/qortal/crosschain/Digibyte.java
@@ -134,6 +134,8 @@ public class Digibyte extends Bitcoiny {
Context bitcoinjContext = new Context(digibyteNet.getParams());
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
+
+ electrumX.setBlockchain(instance);
}
return instance;
diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java
index a2a42089..e1eb1963 100644
--- a/src/main/java/org/qortal/crosschain/ElectrumX.java
+++ b/src/main/java/org/qortal/crosschain/ElectrumX.java
@@ -40,7 +40,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
private static final int RESPONSE_TIME_READINGS = 5;
- private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
+ private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
public static class Server {
String hostname;
diff --git a/src/main/java/org/qortal/crosschain/PirateWallet.java b/src/main/java/org/qortal/crosschain/PirateWallet.java
index 6c6ed2a9..4b95d3cc 100644
--- a/src/main/java/org/qortal/crosschain/PirateWallet.java
+++ b/src/main/java/org/qortal/crosschain/PirateWallet.java
@@ -117,7 +117,7 @@ public class PirateWallet {
// Restore existing wallet
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
if (response != null && !response.contains("\"initalized\":true")) {
- LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
+ LOGGER.info("Unable to initialize Pirate Chain wallet at {}: {}", serverUri, response);
return false;
}
this.seedPhrase = inputSeedPhrase;
diff --git a/src/main/java/org/qortal/crosschain/Ravencoin.java b/src/main/java/org/qortal/crosschain/Ravencoin.java
index d65c0a13..7bf5b20f 100644
--- a/src/main/java/org/qortal/crosschain/Ravencoin.java
+++ b/src/main/java/org/qortal/crosschain/Ravencoin.java
@@ -138,6 +138,8 @@ public class Ravencoin extends Bitcoiny {
Context bitcoinjContext = new Context(ravencoinNet.getParams());
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
+
+ electrumX.setBlockchain(instance);
}
return instance;
diff --git a/src/main/java/org/qortal/data/account/AccountData.java b/src/main/java/org/qortal/data/account/AccountData.java
index 4d662f04..868d1bc1 100644
--- a/src/main/java/org/qortal/data/account/AccountData.java
+++ b/src/main/java/org/qortal/data/account/AccountData.java
@@ -18,6 +18,7 @@ public class AccountData {
protected int level;
protected int blocksMinted;
protected int blocksMintedAdjustment;
+ protected int blocksMintedPenalty;
// Constructors
@@ -25,7 +26,7 @@ public class AccountData {
protected AccountData() {
}
- public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment) {
+ public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment, int blocksMintedPenalty) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
@@ -34,10 +35,11 @@ public class AccountData {
this.level = level;
this.blocksMinted = blocksMinted;
this.blocksMintedAdjustment = blocksMintedAdjustment;
+ this.blocksMintedPenalty = blocksMintedPenalty;
}
public AccountData(String address) {
- this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0);
+ this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0, 0);
}
// Getters/Setters
@@ -102,6 +104,14 @@ public class AccountData {
this.blocksMintedAdjustment = blocksMintedAdjustment;
}
+ public int getBlocksMintedPenalty() {
+ return this.blocksMintedPenalty;
+ }
+
+ public void setBlocksMintedPenalty(int blocksMintedPenalty) {
+ this.blocksMintedPenalty = blocksMintedPenalty;
+ }
+
// Comparison
@Override
diff --git a/src/main/java/org/qortal/data/account/AccountPenaltyData.java b/src/main/java/org/qortal/data/account/AccountPenaltyData.java
new file mode 100644
index 00000000..61947a5f
--- /dev/null
+++ b/src/main/java/org/qortal/data/account/AccountPenaltyData.java
@@ -0,0 +1,52 @@
+package org.qortal.data.account;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+// All properties to be converted to JSON via JAXB
+@XmlAccessorType(XmlAccessType.FIELD)
+public class AccountPenaltyData {
+
+ // Properties
+ private String address;
+ private int blocksMintedPenalty;
+
+ // Constructors
+
+ // necessary for JAXB
+ protected AccountPenaltyData() {
+ }
+
+ public AccountPenaltyData(String address, int blocksMintedPenalty) {
+ this.address = address;
+ this.blocksMintedPenalty = blocksMintedPenalty;
+ }
+
+ // Getters/Setters
+
+ public String getAddress() {
+ return this.address;
+ }
+
+ public int getBlocksMintedPenalty() {
+ return this.blocksMintedPenalty;
+ }
+
+ public String toString() {
+ return String.format("%s has penalty %d", this.address, this.blocksMintedPenalty);
+ }
+
+ @Override
+ public boolean equals(Object b) {
+ if (!(b instanceof AccountPenaltyData))
+ return false;
+
+ return this.getAddress().equals(((AccountPenaltyData) b).getAddress());
+ }
+
+ @Override
+ public int hashCode() {
+ return address.hashCode();
+ }
+
+}
diff --git a/src/main/java/org/qortal/data/chat/ChatMessage.java b/src/main/java/org/qortal/data/chat/ChatMessage.java
index 26df1da4..5d16bb7c 100644
--- a/src/main/java/org/qortal/data/chat/ChatMessage.java
+++ b/src/main/java/org/qortal/data/chat/ChatMessage.java
@@ -27,6 +27,8 @@ public class ChatMessage {
private String recipientName;
+ private byte[] chatReference;
+
private byte[] data;
private boolean isText;
@@ -42,8 +44,8 @@ public class ChatMessage {
// For repository use
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
- String senderName, String recipient, String recipientName, byte[] data, boolean isText,
- boolean isEncrypted, byte[] signature) {
+ String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
+ boolean isText, boolean isEncrypted, byte[] signature) {
this.timestamp = timestamp;
this.txGroupId = txGroupId;
this.reference = reference;
@@ -52,6 +54,7 @@ public class ChatMessage {
this.senderName = senderName;
this.recipient = recipient;
this.recipientName = recipientName;
+ this.chatReference = chatReference;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
@@ -90,6 +93,10 @@ public class ChatMessage {
return this.recipientName;
}
+ public byte[] getChatReference() {
+ return this.chatReference;
+ }
+
public byte[] getData() {
return this.data;
}
diff --git a/src/main/java/org/qortal/data/transaction/ChatTransactionData.java b/src/main/java/org/qortal/data/transaction/ChatTransactionData.java
index 36ce6124..5a6adf7f 100644
--- a/src/main/java/org/qortal/data/transaction/ChatTransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/ChatTransactionData.java
@@ -26,6 +26,8 @@ public class ChatTransactionData extends TransactionData {
private String recipient; // can be null
+ private byte[] chatReference; // can be null
+
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
private byte[] data;
@@ -44,13 +46,14 @@ public class ChatTransactionData extends TransactionData {
}
public ChatTransactionData(BaseTransactionData baseTransactionData,
- String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
+ String sender, int nonce, String recipient, byte[] chatReference, byte[] data, boolean isText, boolean isEncrypted) {
super(TransactionType.CHAT, baseTransactionData);
this.senderPublicKey = baseTransactionData.creatorPublicKey;
this.sender = sender;
this.nonce = nonce;
this.recipient = recipient;
+ this.chatReference = chatReference;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
@@ -78,6 +81,14 @@ public class ChatTransactionData extends TransactionData {
return this.recipient;
}
+ public byte[] getChatReference() {
+ return this.chatReference;
+ }
+
+ public void setChatReference(byte[] chatReference) {
+ this.chatReference = chatReference;
+ }
+
public byte[] getData() {
return this.data;
}
diff --git a/src/main/java/org/qortal/data/transaction/DeployAtTransactionData.java b/src/main/java/org/qortal/data/transaction/DeployAtTransactionData.java
index 7a2ebdab..fed69cd5 100644
--- a/src/main/java/org/qortal/data/transaction/DeployAtTransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/DeployAtTransactionData.java
@@ -2,6 +2,7 @@ package org.qortal.data.transaction;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.transaction.Transaction.TransactionType;
@@ -90,4 +91,17 @@ public class DeployAtTransactionData extends TransactionData {
this.aTAddress = AtAddress;
}
+ // Re-expose creatorPublicKey for this transaction type for JAXB
+ @XmlElement(name = "creatorPublicKey")
+ @Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
+ public byte[] getAtCreatorPublicKey() {
+ return this.creatorPublicKey;
+ }
+
+ @XmlElement(name = "creatorPublicKey")
+ @Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
+ public void setAtCreatorPublicKey(byte[] creatorPublicKey) {
+ this.creatorPublicKey = creatorPublicKey;
+ }
+
}
diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java
index 060901f2..ec1139f4 100644
--- a/src/main/java/org/qortal/data/transaction/TransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/TransactionData.java
@@ -128,6 +128,10 @@ public abstract class TransactionData {
return this.txGroupId;
}
+ public void setTxGroupId(int txGroupId) {
+ this.txGroupId = txGroupId;
+ }
+
public byte[] getReference() {
return this.reference;
}
diff --git a/src/main/java/org/qortal/group/Group.java b/src/main/java/org/qortal/group/Group.java
index 1dbb18b0..465743a9 100644
--- a/src/main/java/org/qortal/group/Group.java
+++ b/src/main/java/org/qortal/group/Group.java
@@ -80,6 +80,9 @@ public class Group {
// Useful constants
public static final int NO_GROUP = 0;
+ // Null owner address corresponds with public key "11111111111111111111111111111111"
+ public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
+
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 32;
public static final int MAX_DESCRIPTION_SIZE = 128;
diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java
index 22354cc4..47752767 100644
--- a/src/main/java/org/qortal/network/Handshake.java
+++ b/src/main/java/org/qortal/network/Handshake.java
@@ -265,7 +265,7 @@ public enum Handshake {
private static final long PEER_VERSION_131 = 0x0100030001L;
/** Minimum peer version that we are allowed to communicate with */
- private static final String MIN_PEER_VERSION = "3.1.0";
+ private static final String MIN_PEER_VERSION = "3.8.2";
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index a187d29b..4c05d5b9 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -155,6 +155,11 @@ public class Peer {
*/
private CommonBlockData commonBlockData;
+ /**
+ * Last time we detected this peer as TOO_DIVERGENT
+ */
+ private Long lastTooDivergentTime;
+
// Message stats
private static class MessageStats {
@@ -383,6 +388,14 @@ public class Peer {
this.commonBlockData = commonBlockData;
}
+ public Long getLastTooDivergentTime() {
+ return this.lastTooDivergentTime;
+ }
+
+ public void setLastTooDivergentTime(Long lastTooDivergentTime) {
+ this.lastTooDivergentTime = lastTooDivergentTime;
+ }
+
public boolean isSyncInProgress() {
return this.syncInProgress;
}
diff --git a/src/main/java/org/qortal/network/message/AccountMessage.java b/src/main/java/org/qortal/network/message/AccountMessage.java
index d22ef879..453862b0 100644
--- a/src/main/java/org/qortal/network/message/AccountMessage.java
+++ b/src/main/java/org/qortal/network/message/AccountMessage.java
@@ -41,6 +41,8 @@ public class AccountMessage extends Message {
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
+ bytes.write(Ints.toByteArray(accountData.getBlocksMintedPenalty()));
+
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
@@ -80,7 +82,9 @@ public class AccountMessage extends Message {
int blocksMintedAdjustment = byteBuffer.getInt();
- AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
+ int blocksMintedPenalty = byteBuffer.getInt();
+
+ AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
return new AccountMessage(id, accountData);
}
diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java
index 281f34f1..1175337c 100644
--- a/src/main/java/org/qortal/repository/AccountRepository.java
+++ b/src/main/java/org/qortal/repository/AccountRepository.java
@@ -1,13 +1,9 @@
package org.qortal.repository;
import java.util.List;
+import java.util.Set;
-import org.qortal.data.account.AccountBalanceData;
-import org.qortal.data.account.AccountData;
-import org.qortal.data.account.EligibleQoraHolderData;
-import org.qortal.data.account.MintingAccountData;
-import org.qortal.data.account.QortFromQoraData;
-import org.qortal.data.account.RewardShareData;
+import org.qortal.data.account.*;
public interface AccountRepository {
@@ -19,6 +15,9 @@ public interface AccountRepository {
/** Returns accounts with any bit set in given mask. */
public List getFlaggedAccounts(int mask) throws DataException;
+ /** Returns accounts with a blockedMintedPenalty */
+ public List getPenaltyAccounts() throws DataException;
+
/** Returns account's last reference or null if not set or account not found. */
public byte[] getLastReference(String address) throws DataException;
@@ -100,6 +99,18 @@ public interface AccountRepository {
*/
public void modifyMintedBlockCounts(List addresses, int delta) throws DataException;
+ /** Returns account's block minted penalty count or null if account not found. */
+ public Integer getBlocksMintedPenaltyCount(String address) throws DataException;
+
+ /**
+ * Sets blocks minted penalties for given list of accounts.
+ * This replaces the existing values rather than modifying them by a delta.
+ *
+ * @param accountPenalties
+ * @throws DataException
+ */
+ public void updateBlocksMintedPenalties(Set accountPenalties) throws DataException;
+
/** Delete account from repository. */
public void delete(String address) throws DataException;
diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java
index 2ecd8a34..c4541907 100644
--- a/src/main/java/org/qortal/repository/ChatRepository.java
+++ b/src/main/java/org/qortal/repository/ChatRepository.java
@@ -14,8 +14,8 @@ public interface ChatRepository {
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
*/
public List getMessagesMatchingCriteria(Long before, Long after,
- Integer txGroupId, byte[] reference, List involving,
- Integer limit, Integer offset, Boolean reverse) throws DataException;
+ Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
+ List involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
diff --git a/src/main/java/org/qortal/repository/GroupRepository.java b/src/main/java/org/qortal/repository/GroupRepository.java
index bcee7d25..94c97992 100644
--- a/src/main/java/org/qortal/repository/GroupRepository.java
+++ b/src/main/java/org/qortal/repository/GroupRepository.java
@@ -131,7 +131,14 @@ public interface GroupRepository {
public GroupBanData getBan(int groupId, String member) throws DataException;
- public boolean banExists(int groupId, String offender) throws DataException;
+ /**
+ * IMPORTANT: when using banExists() as part of validation, the timestamp must be that of the transaction that
+ * is calling banExists() as part of its validation. It must NOT be the current time, unless this is being
+ * called outside of validation, as part of an on demand check for a ban existing (such as via an API call).
+ * This is because we need to evaluate a ban's status based on the time of the subsequent transaction, as
+ * validation will not occur at a fixed time for every node. For some, it could be months into the future.
+ */
+ public boolean banExists(int groupId, String offender, long timestamp) throws DataException;
public List getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java
index 4fb9bb12..105a317d 100644
--- a/src/main/java/org/qortal/repository/TransactionRepository.java
+++ b/src/main/java/org/qortal/repository/TransactionRepository.java
@@ -179,6 +179,15 @@ public interface TransactionRepository {
public List getAssetTransfers(long assetId, String address, Integer limit, Integer offset, Boolean reverse)
throws DataException;
+ /**
+ * Returns list of reward share transaction creators, excluding self shares.
+ * This uses confirmed transactions only.
+ *
+ * @return
+ * @throws DataException
+ */
+ public List getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
+
/**
* Returns list of transactions pending approval, with optional txGgroupId filtering.
*
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
index 9fdb0a3f..cb188502 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java
@@ -6,15 +6,11 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import org.qortal.asset.Asset;
-import org.qortal.data.account.AccountBalanceData;
-import org.qortal.data.account.AccountData;
-import org.qortal.data.account.EligibleQoraHolderData;
-import org.qortal.data.account.MintingAccountData;
-import org.qortal.data.account.QortFromQoraData;
-import org.qortal.data.account.RewardShareData;
+import org.qortal.data.account.*;
import org.qortal.repository.AccountRepository;
import org.qortal.repository.DataException;
@@ -30,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public AccountData getAccount(String address) throws DataException {
- String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment FROM Accounts WHERE account = ?";
+ String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty FROM Accounts WHERE account = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
@@ -43,8 +39,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
int level = resultSet.getInt(5);
int blocksMinted = resultSet.getInt(6);
int blocksMintedAdjustment = resultSet.getInt(7);
+ int blocksMintedPenalty = resultSet.getInt(8);
- return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
+ return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
@@ -52,7 +49,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public List getFlaggedAccounts(int mask) throws DataException {
- String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, account FROM Accounts WHERE BITAND(flags, ?) != 0";
+ String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE BITAND(flags, ?) != 0";
List accounts = new ArrayList<>();
@@ -68,9 +65,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
int level = resultSet.getInt(5);
int blocksMinted = resultSet.getInt(6);
int blocksMintedAdjustment = resultSet.getInt(7);
- String address = resultSet.getString(8);
+ int blocksMintedPenalty = resultSet.getInt(8);
+ String address = resultSet.getString(9);
- accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment));
+ accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
} while (resultSet.next());
return accounts;
@@ -79,6 +77,36 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
+ @Override
+ public List getPenaltyAccounts() throws DataException {
+ String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE blocks_minted_penalty != 0";
+
+ List accounts = new ArrayList<>();
+
+ try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
+ if (resultSet == null)
+ return accounts;
+
+ do {
+ byte[] reference = resultSet.getBytes(1);
+ byte[] publicKey = resultSet.getBytes(2);
+ int defaultGroupId = resultSet.getInt(3);
+ int flags = resultSet.getInt(4);
+ int level = resultSet.getInt(5);
+ int blocksMinted = resultSet.getInt(6);
+ int blocksMintedAdjustment = resultSet.getInt(7);
+ int blocksMintedPenalty = resultSet.getInt(8);
+ String address = resultSet.getString(9);
+
+ accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
+ } while (resultSet.next());
+
+ return accounts;
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch penalty accounts from repository", e);
+ }
+ }
+
@Override
public byte[] getLastReference(String address) throws DataException {
String sql = "SELECT reference FROM Accounts WHERE account = ?";
@@ -298,6 +326,39 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
+ @Override
+ public Integer getBlocksMintedPenaltyCount(String address) throws DataException {
+ String sql = "SELECT blocks_minted_penalty FROM Accounts WHERE account = ?";
+
+ try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
+ if (resultSet == null)
+ return null;
+
+ return resultSet.getInt(1);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch account's block minted penalty count from repository", e);
+ }
+ }
+ public void updateBlocksMintedPenalties(Set accountPenalties) throws DataException {
+ // Nothing to do?
+ if (accountPenalties == null || accountPenalties.isEmpty())
+ return;
+
+ // Map balance changes into SQL bind params, filtering out no-op changes
+ List