voteWeights = voteWeightMap.entrySet().stream()
+ .map(entry -> new PollVotes.OptionWeight(entry.getKey(), entry.getValue()))
+ .collect(Collectors.toList());
if (onlyCounts != null && onlyCounts) {
- return new PollVotes(null, totalVotes, voteCounts);
+ return new PollVotes(null, totalVotes, totalWeight, voteCounts, voteWeights);
} else {
- return new PollVotes(votes, totalVotes, voteCounts);
+ return new PollVotes(votes, totalVotes, totalWeight, voteCounts, voteWeights);
}
} catch (ApiException e) {
throw e;
diff --git a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
index f0e045ed..837288e5 100644
--- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
+++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
@@ -24,8 +24,9 @@ import org.qortal.api.model.ActivitySummary;
import org.qortal.api.model.NodeInfo;
import org.qortal.api.model.NodeStatus;
import org.qortal.block.BlockChain;
-import org.qortal.controller.AutoUpdate;
+import org.qortal.controller.BootstrapNode;
import org.qortal.controller.Controller;
+import org.qortal.controller.RestartNode;
import org.qortal.controller.Synchronizer;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.controller.repository.BlockArchiveRebuilder;
@@ -250,7 +251,38 @@ public class AdminResource {
// Not important
}
- AutoUpdate.attemptRestart();
+ RestartNode.attemptToRestart();
+
+ }).start();
+
+ return "true";
+ }
+
+ @GET
+ @Path("/bootstrap")
+ @Operation(
+ summary = "Bootstrap",
+ description = "Delete and download new database archive",
+ responses = {
+ @ApiResponse(
+ description = "\"true\"",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ @SecurityRequirement(name = "apiKey")
+ public String bootstrap(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
+ Security.checkApiCallAllowed(request);
+
+ new Thread(() -> {
+ // Short sleep to allow HTTP response body to be emitted
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Not important
+ }
+
+ BootstrapNode.attemptToBootstrap();
}).start();
diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java
index 93dac568..276116fc 100644
--- a/src/main/java/org/qortal/at/QortalATAPI.java
+++ b/src/main/java/org/qortal/at/QortalATAPI.java
@@ -522,6 +522,10 @@ public class QortalATAPI extends API {
/** Returns AT account's lastReference */
private byte[] getLastReference() {
+ // If we have transactions already, then use signature from last transaction
+ if (!this.transactions.isEmpty())
+ return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
+
try {
// Look up AT's account's last reference from repository
Account atAccount = this.getATAccount();
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index 41faf51b..caaa0c76 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -1061,8 +1061,10 @@ public class Block {
return ValidationResult.MINTER_NOT_ACCEPTED;
long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey(), minterLevel);
- if (this.blockData.getTimestamp() != expectedTimestamp)
+ if (this.blockData.getTimestamp() != expectedTimestamp) {
+ LOGGER.debug(String.format("timestamp mismatch! block had %s but we expected %s", this.blockData.getTimestamp(), expectedTimestamp));
return ValidationResult.TIMESTAMP_INCORRECT;
+ }
return ValidationResult.OK;
}
@@ -1309,6 +1311,9 @@ public class Block {
if (!transaction.isConfirmable()) {
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
}
+ if (!transaction.isConfirmableAtHeight(this.blockData.getHeight())) {
+ return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
+ }
}
// Check transaction isn't already included in a block
@@ -1545,12 +1550,22 @@ public class Block {
processBlockRewards();
}
- if (this.blockData.getHeight() == 212937)
+ if (this.blockData.getHeight() == 212937) {
// Apply fix for block 212937
Block212937.processFix(this);
+ }
- else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
+ }
+
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
+ SelfSponsorshipAlgoV2Block.processAccountPenalties(this);
+ }
+
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
+ SelfSponsorshipAlgoV3Block.processAccountPenalties(this);
+ }
}
// We're about to (test-)process a batch of transactions,
@@ -1835,13 +1850,23 @@ public class Block {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
- if (this.blockData.getHeight() == 212937)
+ if (this.blockData.getHeight() == 212937) {
// Revert fix for block 212937
Block212937.orphanFix(this);
+ }
- else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
+ }
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) {
+ SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this);
+ }
+
+ if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) {
+ SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this);
+ }
+
// Account levels and block rewards are only processed/orphaned on block reward distribution blocks
if (this.isRewardDistributionBlock()) {
// Block rewards, including transaction fees, removed after transactions undone
@@ -2088,7 +2113,7 @@ public class Block {
return Block.isOnlineAccountsBlock(this.getBlockData().getHeight());
}
- private static boolean isOnlineAccountsBlock(int height) {
+ public static boolean isOnlineAccountsBlock(int height) {
// After feature trigger, only certain blocks contain online accounts
if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) {
final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
@@ -2539,5 +2564,4 @@ public class Block {
LOGGER.info(() -> String.format("Unable to log block debugging info: %s", e.getMessage()));
}
}
-
}
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index aa2ab9bb..dc9dfe4c 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -73,9 +73,14 @@ public class BlockChain {
increaseOnlineAccountsDifficultyTimestamp,
onlineAccountMinterLevelValidationHeight,
selfSponsorshipAlgoV1Height,
+ selfSponsorshipAlgoV2Height,
+ selfSponsorshipAlgoV3Height,
feeValidationFixTimestamp,
chatReferenceTimestamp,
- arbitraryOptionalFeeTimestamp;
+ arbitraryOptionalFeeTimestamp,
+ unconfirmableRewardSharesHeight,
+ disableTransferPrivsTimestamp,
+ enableTransferPrivsTimestamp
}
// Custom transaction fees
@@ -198,6 +203,7 @@ public class BlockChain {
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
+
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
@@ -208,6 +214,15 @@ public class BlockChain {
/** Snapshot timestamp for self sponsorship algo V1 */
private long selfSponsorshipAlgoV1SnapshotTimestamp;
+ /** Snapshot timestamp for self sponsorship algo V2 */
+ private long selfSponsorshipAlgoV2SnapshotTimestamp;
+
+ /** Snapshot timestamp for self sponsorship algo V3 */
+ private long selfSponsorshipAlgoV3SnapshotTimestamp;
+
+ /** Reference timestamp for self sponsorship algo V1 block height */
+ private long referenceTimestampBlock;
+
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
private long mempowTransactionUpdatesTimestamp;
@@ -224,6 +239,8 @@ public class BlockChain {
* data and to base online accounts decisions on. */
private int blockRewardBatchAccountsBlockCount;
+ private String penaltyFixHash;
+
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -266,7 +283,7 @@ public class BlockChain {
try {
// Create JAXB context aware of Settings
jc = JAXBContextFactory.createContext(new Class[] {
- BlockChain.class, GenesisBlock.GenesisInfo.class
+ BlockChain.class, GenesisBlock.GenesisInfo.class
}, null);
// Create unmarshaller
@@ -394,12 +411,29 @@ public class BlockChain {
return this.blockRewardBatchAccountsBlockCount;
}
+ public String getPenaltyFixHash() {
+ return this.penaltyFixHash;
+ }
- // Self sponsorship algo
+ // Self sponsorship algo V1
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
}
+ // Self sponsorship algo V2
+ public long getSelfSponsorshipAlgoV2SnapshotTimestamp() {
+ return this.selfSponsorshipAlgoV2SnapshotTimestamp;
+ }
+
+ // Self sponsorship algo V3
+ public long getSelfSponsorshipAlgoV3SnapshotTimestamp() {
+ return this.selfSponsorshipAlgoV3SnapshotTimestamp;
+ }
+
+ // Self sponsorship algo V3
+ public long getReferenceTimestampBlock() {
+ return this.referenceTimestampBlock;
+ }
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
public long getMemPoWTransactionUpdatesTimestamp() {
return this.mempowTransactionUpdatesTimestamp;
@@ -540,6 +574,14 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
}
+ public int getSelfSponsorshipAlgoV2Height() {
+ return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV2Height.name()).intValue();
+ }
+
+ public int getSelfSponsorshipAlgoV3Height() {
+ return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV3Height.name()).intValue();
+ }
+
public long getOnlineAccountMinterLevelValidationHeight() {
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
}
@@ -556,6 +598,17 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue();
}
+ public int getUnconfirmableRewardSharesHeight() {
+ return this.featureTriggers.get(FeatureTrigger.unconfirmableRewardSharesHeight.name()).intValue();
+ }
+
+ public long getDisableTransferPrivsTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.disableTransferPrivsTimestamp.name()).longValue();
+ }
+
+ public long getEnableTransferPrivsTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.enableTransferPrivsTimestamp.name()).longValue();
+ }
// More complex getters for aspects that change by height or timestamp
@@ -742,7 +795,7 @@ public class BlockChain {
/**
* Some sort of start-up/initialization/checking method.
- *
+ *
* @throws SQLException
*/
public static void validate() throws DataException {
diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
index c3c374d1..27b50a81 100644
--- a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
+++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
@@ -28,7 +28,6 @@ public final class SelfSponsorshipAlgoV1Block {
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
-
private SelfSponsorshipAlgoV1Block() {
/* Do not instantiate */
}
@@ -133,4 +132,4 @@ public final class SelfSponsorshipAlgoV1Block {
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
new file mode 100644
index 00000000..7957de6a
--- /dev/null
+++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java
@@ -0,0 +1,143 @@
+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.SelfSponsorshipAlgoV2;
+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 AlgoV2 Block
+ *
+ * Selected block for the initial run on the "self sponsorship detection algorithm"
+ */
+public final class SelfSponsorshipAlgoV2Block {
+
+ private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV2Block.class);
+
+ private SelfSponsorshipAlgoV2Block() {
+ /* Do not instantiate */
+ }
+
+ public static void processAccountPenalties(Block block) throws DataException {
+ LOGGER.info("Process Self Sponsorship Algo V2 - 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("Orphan Self Sponsorship Algo V2 - 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);
+ }
+
+ private static Set getAccountPenalties(Repository repository, int penalty) throws DataException {
+ Set penalties = new LinkedHashSet<>();
+ List penalizedAddresses = repository.getAccountRepository().getPenaltyAccounts();
+ List assetAddresses = repository.getTransactionRepository().getConfirmedTransferAssetCreators();
+
+ for (AccountData penalizedAddress : penalizedAddresses) {
+ //System.out.println(String.format("address: %s", address));
+ SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, penalizedAddress.getAddress(), false);
+ selfSponsorshipAlgoV2.run();
+ //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
+ for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) {
+ penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
+ }
+ }
+
+ for (String assetAddress : assetAddresses) {
+ //System.out.println(String.format("address: %s", address));
+ SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, assetAddress, true);
+ selfSponsorshipAlgoV2.run();
+ //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size()));
+ for (String penaltyAddress : selfSponsorshipAlgoV2.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)));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
new file mode 100644
index 00000000..dec8957d
--- /dev/null
+++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java
@@ -0,0 +1,136 @@
+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.SelfSponsorshipAlgoV3;
+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.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Self Sponsorship AlgoV3 Block
+ *
+ * Selected block for the initial run on the "self sponsorship detection algorithm"
+ */
+public final class SelfSponsorshipAlgoV3Block {
+
+ private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV3Block.class);
+
+ private SelfSponsorshipAlgoV3Block() {
+ /* Do not instantiate */
+ }
+
+ public static void processAccountPenalties(Block block) throws DataException {
+ LOGGER.info("Process Self Sponsorship Algo V3 - 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("Orphan Self Sponsorship Algo V3 - 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 snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
+ final long snapshotTimestampV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3SnapshotTimestamp();
+ Set penalties = new LinkedHashSet<>();
+ List addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
+ for (String address : addresses) {
+ //System.out.println(String.format("address: %s", address));
+ SelfSponsorshipAlgoV3 selfSponsorshipAlgoV3 = new SelfSponsorshipAlgoV3(repository, address, snapshotTimestampV1, snapshotTimestampV3, false);
+ selfSponsorshipAlgoV3.run();
+ //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV3.getPenaltyAddresses().size()));
+
+ for (String penaltyAddress : selfSponsorshipAlgoV3.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)));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java
index bc232e1b..4b315651 100644
--- a/src/main/java/org/qortal/controller/AutoUpdate.java
+++ b/src/main/java/org/qortal/controller/AutoUpdate.java
@@ -291,78 +291,4 @@ public class AutoUpdate extends Thread {
return true; // repo was okay, even if applying update failed
}
}
-
- public static boolean attemptRestart() {
- LOGGER.info(String.format("Restarting node..."));
-
- // Give repository a chance to backup in case things go badly wrong (if enabled)
- if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
- try {
- // Timeout if the database isn't ready for backing up after 60 seconds
- long timeout = 60 * 1000L;
- RepositoryManager.backup(true, "backup", timeout);
-
- } catch (TimeoutException e) {
- LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
- // Continue with the node restart anyway...
- }
- }
-
- // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
- String javaHome = System.getProperty("java.home");
- LOGGER.debug(String.format("Java home: %s", javaHome));
-
- Path javaBinary = Paths.get(javaHome, "bin", "java");
- LOGGER.debug(String.format("Java binary: %s", javaBinary));
-
- try {
- List javaCmd = new ArrayList<>();
- // Java runtime binary itself
- javaCmd.add(javaBinary.toString());
-
- // JVM arguments
- javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
-
- // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
- javaCmd = javaCmd.stream()
- .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
- .collect(Collectors.toList());
-
- // Remove JNI options as they won't be supported by command-line 'java'
- // These are typically added by the AdvancedInstaller Java launcher EXE
- javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
-
- // Call ApplyUpdate using JAR
- javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
-
- // Add command-line args saved from start-up
- String[] savedArgs = Controller.getInstance().getSavedArgs();
- if (savedArgs != null)
- javaCmd.addAll(Arrays.asList(savedArgs));
-
- LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
-
- SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO
- Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO
- MessageType.INFO);
-
- ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
-
- // New process will inherit our stdout and stderr
- processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
- processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
-
- Process process = processBuilder.start();
-
- // Nothing to pipe to new process, so close output stream (process's stdin)
- process.getOutputStream().close();
-
- return true; // restarting node OK
- } catch (Exception e) {
- LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
-
- return true; // repo was okay, even if applying update failed
- }
- }
-
}
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 15bcb1d7..49831cba 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -474,6 +474,7 @@ public class BlockMinter extends Thread {
Iterator unconfirmedTransactionsIterator = unconfirmedTransactions.iterator();
final long newBlockTimestamp = newBlock.getBlockData().getTimestamp();
+ final int newBlockHeight = newBlock.getBlockData().getHeight();
while (unconfirmedTransactionsIterator.hasNext()) {
TransactionData transactionData = unconfirmedTransactionsIterator.next();
@@ -481,6 +482,12 @@ public class BlockMinter extends Thread {
// Ignore transactions that have expired before this block - they will be cleaned up later
if (transactionData.getTimestamp() > newBlockTimestamp || Transaction.getDeadline(transactionData) <= newBlockTimestamp)
unconfirmedTransactionsIterator.remove();
+
+ // Ignore transactions that are unconfirmable at this block height
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+ if (!transaction.isConfirmableAtHeight(newBlockHeight)) {
+ unconfirmedTransactionsIterator.remove();
+ }
}
// Sign to create block's signature, needed by Block.isValid()
diff --git a/src/main/java/org/qortal/controller/BootstrapNode.java b/src/main/java/org/qortal/controller/BootstrapNode.java
new file mode 100644
index 00000000..9d0f8b36
--- /dev/null
+++ b/src/main/java/org/qortal/controller/BootstrapNode.java
@@ -0,0 +1,103 @@
+package org.qortal.controller;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.ApplyBootstrap;
+import org.qortal.globalization.Translator;
+import org.qortal.gui.SysTray;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+
+import java.awt.TrayIcon.MessageType;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */
+
+public class BootstrapNode {
+
+ public static final String JAR_FILENAME = "qortal.jar";
+ public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
+
+ private static final Logger LOGGER = LogManager.getLogger(BootstrapNode.class);
+
+ public static boolean attemptToBootstrap() {
+ LOGGER.info(String.format("Bootstrapping node..."));
+
+ // Give repository a chance to backup in case things go badly wrong (if enabled)
+ if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
+ try {
+ // Timeout if the database isn't ready for backing up after 60 seconds
+ long timeout = 60 * 1000L;
+ RepositoryManager.backup(true, "backup", timeout);
+
+ } catch (TimeoutException e) {
+ LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
+ // Continue with the bootstrap anyway...
+ }
+ }
+
+ // Call ApplyBootstrap to end this process
+ String javaHome = System.getProperty("java.home");
+ LOGGER.debug(String.format("Java home: %s", javaHome));
+
+ Path javaBinary = Paths.get(javaHome, "bin", "java");
+ LOGGER.debug(String.format("Java binary: %s", javaBinary));
+
+ try {
+ List javaCmd = new ArrayList<>();
+
+ // Java runtime binary itself
+ javaCmd.add(javaBinary.toString());
+
+ // JVM arguments
+ javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+
+ // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
+ javaCmd = javaCmd.stream()
+ .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
+ .collect(Collectors.toList());
+
+ // Remove JNI options as they won't be supported by command-line 'java'
+ // These are typically added by the AdvancedInstaller Java launcher EXE
+ javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
+
+ // Call ApplyBootstrap using JAR
+ javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyBootstrap.class.getCanonicalName()));
+
+ // Add command-line args saved from start-up
+ String[] savedArgs = Controller.getInstance().getSavedArgs();
+ if (savedArgs != null)
+ javaCmd.addAll(Arrays.asList(savedArgs));
+
+ LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
+
+ SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "BOOTSTRAP_NODE"),
+ Translator.INSTANCE.translate("SysTray", "APPLYING_BOOTSTRAP_AND_RESTARTING"),
+ MessageType.INFO);
+
+ ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
+
+ // New process will inherit our stdout and stderr
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+ processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = processBuilder.start();
+
+ // Nothing to pipe to new process, so close output stream (process's stdin)
+ process.getOutputStream().close();
+
+ return true; // restarting node OK
+ } catch (Exception e) {
+ LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
+
+ return true; // repo was okay, even if applying bootstrap failed
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 6d2562ab..c1e0e279 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -31,6 +31,7 @@ import org.qortal.globalization.Translator;
import org.qortal.gui.Gui;
import org.qortal.gui.SysTray;
import org.qortal.network.Network;
+import org.qortal.network.RNSNetwork;
import org.qortal.network.Peer;
import org.qortal.network.message.*;
import org.qortal.repository.*;
@@ -115,6 +116,7 @@ public class Controller extends Thread {
private long repositoryCheckpointTimestamp = startTime; // ms
private long prunePeersTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
+ private long pruneRNSPeersTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
/** Whether we can mint new blocks, as reported by BlockMinter. */
@@ -481,6 +483,15 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error
}
+ LOGGER.info("Starting Reticulum");
+ try {
+ RNSNetwork rns = RNSNetwork.getInstance();
+ rns.start();
+ LOGGER.debug("Reticulum instance: {}", rns.toString());
+ } catch (IOException | DataException e) {
+ LOGGER.error("Unable to start Reticulum", e);
+ }
+
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
@@ -582,6 +593,8 @@ public class Controller extends Thread {
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
+ //final long pruneRNSPeersInterval = 5 * 60 * 1000L; // Every 5 minutes
+ final long pruneRNSPeersInterval = 1 * 60 * 1000L; // Every 1 minute (during development)
// Start executor service for trimming or pruning
PruneManager.getInstance().start();
@@ -690,6 +703,18 @@ public class Controller extends Thread {
}
}
+ // Q: Do we need global pruning?
+ if (now >= pruneRNSPeersTimestamp + pruneRNSPeersInterval) {
+ pruneRNSPeersTimestamp = now + pruneRNSPeersInterval;
+
+ try {
+ LOGGER.debug("Pruning Reticulum peers...");
+ RNSNetwork.getInstance().prunePeers();
+ } catch (DataException e) {
+ LOGGER.warn(String.format("Repository issue when trying to prune Reticulum peers: %s", e.getMessage()));
+ }
+ }
+
// Delete expired transactions
if (now >= deleteExpiredTimestamp) {
deleteExpiredTimestamp = now + DELETE_EXPIRED_INTERVAL;
@@ -988,6 +1013,9 @@ public class Controller extends Thread {
LOGGER.info("Shutting down networking");
Network.getInstance().shutdown();
+ LOGGER.info("Shutting down Reticulum");
+ RNSNetwork.getInstance().shutdown();
+
LOGGER.info("Shutting down controller");
this.interrupt();
try {
diff --git a/src/main/java/org/qortal/controller/RestartNode.java b/src/main/java/org/qortal/controller/RestartNode.java
new file mode 100644
index 00000000..b8d81b85
--- /dev/null
+++ b/src/main/java/org/qortal/controller/RestartNode.java
@@ -0,0 +1,102 @@
+package org.qortal.controller;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.ApplyRestart;;
+import org.qortal.globalization.Translator;
+import org.qortal.gui.SysTray;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+
+import java.awt.TrayIcon.MessageType;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */
+
+public class RestartNode {
+
+ public static final String JAR_FILENAME = "qortal.jar";
+ public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
+
+ private static final Logger LOGGER = LogManager.getLogger(RestartNode.class);
+
+ public static boolean attemptToRestart() {
+ LOGGER.info(String.format("Restarting node..."));
+
+ // Give repository a chance to backup in case things go badly wrong (if enabled)
+ if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
+ try {
+ // Timeout if the database isn't ready for backing up after 60 seconds
+ long timeout = 60 * 1000L;
+ RepositoryManager.backup(true, "backup", timeout);
+
+ } catch (TimeoutException e) {
+ LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
+ // Continue with the node restart anyway...
+ }
+ }
+
+ // Call ApplyRestart to end this process
+ String javaHome = System.getProperty("java.home");
+ LOGGER.debug(String.format("Java home: %s", javaHome));
+
+ Path javaBinary = Paths.get(javaHome, "bin", "java");
+ LOGGER.debug(String.format("Java binary: %s", javaBinary));
+
+ try {
+ List javaCmd = new ArrayList<>();
+ // Java runtime binary itself
+ javaCmd.add(javaBinary.toString());
+
+ // JVM arguments
+ javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+
+ // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
+ javaCmd = javaCmd.stream()
+ .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
+ .collect(Collectors.toList());
+
+ // Remove JNI options as they won't be supported by command-line 'java'
+ // These are typically added by the AdvancedInstaller Java launcher EXE
+ javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
+
+ // Call ApplyRestart using JAR
+ javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyRestart.class.getCanonicalName()));
+
+ // Add command-line args saved from start-up
+ String[] savedArgs = Controller.getInstance().getSavedArgs();
+ if (savedArgs != null)
+ javaCmd.addAll(Arrays.asList(savedArgs));
+
+ LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
+
+ SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "RESTARTING_NODE"),
+ Translator.INSTANCE.translate("SysTray", "APPLYING_RESTARTING_NODE"),
+ MessageType.INFO);
+
+ ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
+
+ // New process will inherit our stdout and stderr
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+ processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = processBuilder.start();
+
+ // Nothing to pipe to new process, so close output stream (process's stdin)
+ process.getOutputStream().close();
+
+ return true; // restarting node OK
+ } catch (Exception e) {
+ LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
+
+ return true; // repo was okay, even if applying restart failed
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java
index 801b141c..79ab38af 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoin.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoin.java
@@ -7,7 +7,7 @@ import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@@ -22,8 +22,6 @@ public class Bitcoin extends Bitcoiny {
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
// Temporary values until a dynamic fee system is written.
- private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
- private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
private static final long NEW_FEE_AMOUNT = 6_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
@@ -46,74 +44,62 @@ public class Bitcoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
- new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
+ new Server("104.198.149.61", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
- new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
- new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
- new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
- new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
- new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
- new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
- new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
- new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
- new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
- new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
- new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
- new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
- new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
- new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
- new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
- new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
- new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
+ new Server("260.whyza.net", Server.ConnectionType.SSL, 50002),
+ new Server("34.136.93.37", Server.ConnectionType.SSL, 50002),
+ new Server("34.67.22.216", Server.ConnectionType.SSL, 50002),
+ new Server("34.68.133.78", Server.ConnectionType.SSL, 50002),
+ new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
- new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
+ new Server("btc.aftrek.org", Server.ConnectionType.SSL, 50002),
+ new Server("btc.hodler.ninja", Server.ConnectionType.SSL, 50002),
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
+ new Server("d762li0k0g.d.firewalla.org", Server.ConnectionType.SSL, 50002),
+ new Server("de.poiuty.com", Server.ConnectionType.SSL, 50002),
+ new Server("dijon.anties.org", Server.ConnectionType.SSL, 50002),
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
+ new Server("electrum.bitrefill.com", Server.ConnectionType.SSL, 50002),
+ new Server("electrum.brainshome.de", Server.ConnectionType.SSL, 50002),
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
- new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
+ new Server("electrum.kcicom.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
- new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
- new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
+ new Server("electrum-btc.leblancnet.us", Server.ConnectionType.SSL, 50002),
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
- new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
- new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
+ new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20000),
+ new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20000),
+ new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20000),
+ new Server("electrumx.blockfinance-eco.li", Server.ConnectionType.SSL, 50002),
+ new Server("electrumx.indoor.app", Server.ConnectionType.SSL, 50002),
+ new Server("electrumx.iodata.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
- new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
- new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
- new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
+ new Server("exs.dyshek.org", Server.ConnectionType.SSL, 50002),
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
- new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
- new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
+ new Server("hodl.artyomk13.me", Server.ConnectionType.SSL, 50002),
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
+ new Server("kittyserver.ddnsfree.com", Server.ConnectionType.SSL, 50002),
+ new Server("lille.anties.org", Server.ConnectionType.SSL, 50002),
+ new Server("marseille.anties.org", Server.ConnectionType.SSL, 50002),
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
- new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
- new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
- new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
- new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
+ new Server("paris.anties.org", Server.ConnectionType.SSL, 50002),
+ new Server("ragtor.duckdns.org", Server.ConnectionType.SSL, 50002),
+ new Server("stavver.dyshek.org", Server.ConnectionType.SSL, 50002),
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
- new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
- new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("xtrum.com", Server.ConnectionType.SSL, 50002)
);
}
@@ -125,11 +111,7 @@ public class Bitcoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
- return OLD_FEE_AMOUNT;
-
- return NEW_FEE_AMOUNT;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -141,12 +123,17 @@ public class Bitcoin extends Bitcoiny {
@Override
public Collection getServers() {
return Arrays.asList(
- new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
- new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
- new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
- new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
+ new Server("bitcoin.devmole.eu", Server.ConnectionType.TCP, 5000),
+ new Server("bitcoin.stagemole.eu", Server.ConnectionType.TCP, 5000),
+ new Server("blockstream.info", Server.ConnectionType.SSL, 993),
+ new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
+ new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10068),
+ new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10068),
+ new Server("electrum3.cipig.net", Server.ConnectionType.TCP, 10068),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
- new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)
+ new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012),
+ new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
+ new Server("v22019051929289916.bestsrv.de", Server.ConnectionType.SSL, 50002)
);
}
@@ -186,6 +173,16 @@ public class Bitcoin extends Bitcoiny {
}
};
+ private long feeCeiling = NEW_FEE_AMOUNT;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -199,7 +196,7 @@ public class Bitcoin extends Bitcoiny {
// Constructors and instance
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
this.bitcoinNet = bitcoinNet;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
@@ -244,6 +241,16 @@ public class Bitcoin extends Bitcoiny {
return this.bitcoinNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.bitcoinNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.bitcoinNet.setFeeCeiling( fee );
+ }
/**
* Returns bitcoinj transaction sending amount to recipient using 20 sat/byte fee.
*
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index 1749cee9..1ae70fe9 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -11,6 +11,7 @@ import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.DeterministicKeyChain;
+import org.bitcoinj.wallet.KeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.api.model.SimpleForeignTransaction;
@@ -52,12 +53,15 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
+ protected Coin feePerKb;
+
// Constructors and instance
- protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
+ protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) {
this.blockchainProvider = blockchainProvider;
this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
+ this.feePerKb = feePerKb;
this.params = this.bitcoinjContext.getParams();
}
@@ -166,7 +170,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
public Coin getFeePerKb() {
- return this.bitcoinjContext.getFeePerKb();
+ return this.feePerKb;
+ }
+
+ public void setFeePerKb(Coin feePerKb) {
+ this.feePerKb = feePerKb;
}
/** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */
@@ -504,7 +512,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List candidates = this.getSpendingCandidateAddresses(key58);
- for(DeterministicKey key : getWalletKeys(key58)) {
+ for(DeterministicKey key : getOldWalletKeys(key58)) {
infos.add(buildAddressInfo(key, candidates));
}
@@ -591,11 +599,23 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
- private List getWalletKeys(String key58) throws ForeignBlockchainException {
+ /**
+ * Get Old Wallet Keys
+ *
+ * Get wallet keys using the old key generation algorithm. This is used for diagnosing and repairing wallets
+ * created before 2024.
+ *
+ * @param masterPrivateKey
+ *
+ * @return the keys
+ *
+ * @throws ForeignBlockchainException
+ */
+ private List getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
- Wallet wallet = walletFromDeterministicKey58(key58);
+ Wallet wallet = walletFromDeterministicKey58(masterPrivateKey);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
@@ -720,7 +740,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
/**
- * Returns first unused receive address given 'm' BIP32 key.
+ * Returns first unused receive address given a BIP32 key.
*
* @param key58 BIP32/HD extended Bitcoin private/public key
* @return P2PKH address
@@ -732,68 +752,22 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Wallet wallet = walletFromDeterministicKey58(key58);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
- keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
- keyChain.maybeLookAhead();
-
- final int keyChainPathSize = keyChain.getAccountPath().size();
- List keys = new ArrayList<>(keyChain.getLeafKeys());
-
- int ki = 0;
do {
- for (; ki < keys.size(); ++ki) {
- DeterministicKey dKey = keys.get(ki);
- List dKeyPath = dKey.getPath();
+ // the next receive funds address
+ Address address = Address.fromKey(this.params, keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS), ScriptType.P2PKH);
- // If keyChain is based on 'm', then make sure dKey is m/0/ki - i.e. a 'receive' address, not 'change' (m/1/ki)
- if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
- continue;
+ // if zero transactions, return address
+ if( 0 == getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).size() )
+ return address.toString();
- // Check unspent
- Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
- byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
-
- List unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
-
- /*
- * If there are no unspent outputs then either:
- * a) all the outputs have been spent
- * b) address has never been used
- *
- * For case (a) we want to remember not to check this address (key) again.
- */
-
- if (unspentOutputs.isEmpty()) {
- // If this is a known key that has been spent before, then we can skip asking for transaction history
- if (this.spentKeys.contains(dKey)) {
- wallet.getActiveKeyChain().markKeyAsUsed(dKey);
- continue;
- }
-
- // Ask for transaction history - if it's empty then key has never been used
- List historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
-
- if (!historicTransactionHashes.isEmpty()) {
- // Fully spent key - case (a)
- this.spentKeys.add(dKey);
- wallet.getActiveKeyChain().markKeyAsUsed(dKey);
- continue;
- }
-
- // Key never been used - case (b)
- return address.toString();
- }
-
- // Key has unspent outputs, hence used, so no good to us
- this.spentKeys.remove(dKey);
- }
-
- // Generate some more keys
- keys.addAll(generateMoreKeys(keyChain));
-
- // Process new keys
+ // else try the next receive funds address
} while (true);
}
+ public abstract long getFeeCeiling();
+
+ public abstract void setFeeCeiling(long fee);
+
// UTXOProvider support
static class WalletAwareUTXOProvider implements UTXOProvider {
@@ -1047,4 +1021,52 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
}
+ /**
+ * Repair Wallet
+ *
+ * Repair wallets generated before 2024 by moving all the address balances to the first address.
+ *
+ * @param privateMasterKey
+ *
+ * @return the transaction Id of the spend operation that moves the balances or the exception name if an exception
+ * is thrown
+ *
+ * @throws ForeignBlockchainException
+ */
+ public String repairOldWallet(String privateMasterKey) throws ForeignBlockchainException {
+
+ // create a deterministic wallet to satisfy the bitcoinj API
+ Wallet wallet = Wallet.createDeterministic(this.bitcoinjContext, ScriptType.P2PKH);
+
+ // use the blockchain resources of this instance for UTXO provision
+ wallet.setUTXOProvider(new BitcoinyUTXOProvider( this ));
+
+ // import in each that is generated using the old key generation algorithm
+ List walletKeys = getOldWalletKeys(privateMasterKey);
+
+ for( DeterministicKey key : walletKeys) {
+ wallet.importKey(ECKey.fromPrivate(key.getPrivKey()));
+ }
+
+ // get the primary receive address
+ Address firstAddress = Address.fromKey(this.params, walletKeys.get(0), ScriptType.P2PKH);
+
+ // send all the imported coins to the primary receive address
+ SendRequest sendRequest = SendRequest.emptyWallet(firstAddress);
+ sendRequest.feePerKb = this.getFeePerKb();
+
+ try {
+ // allow the wallet to build the send request transaction and broadcast
+ wallet.completeTx(sendRequest);
+ broadcastTransaction(sendRequest.tx);
+
+ // return the transaction Id
+ return sendRequest.tx.getTxId().toString();
+ }
+ catch( Exception e ) {
+ // log error and return exception name
+ LOGGER.error(e.getMessage(), e);
+ return e.getClass().getSimpleName();
+ }
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
index d8b4f653..238eff38 100644
--- a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
+++ b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
@@ -3,6 +3,7 @@ package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.CompactBlock;
import java.util.List;
+import java.util.Set;
public abstract class BitcoinyBlockchainProvider {
@@ -59,4 +60,11 @@ public abstract class BitcoinyBlockchainProvider {
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
public abstract void broadcastTransaction(byte[] rawTransaction) throws ForeignBlockchainException;
+ public abstract Set getServers();
+
+ public abstract List getRemainingServers();
+
+ public abstract Set getUselessServers();
+
+ public abstract ChainableServer getCurrentServer();
}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java
new file mode 100644
index 00000000..df596de4
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java
@@ -0,0 +1,80 @@
+package org.qortal.crosschain;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.script.Script;
+import org.bitcoinj.script.ScriptBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class BitcoinyUTXOProvider
+ *
+ * Uses Bitcoiny resources for UTXO provision.
+ */
+public class BitcoinyUTXOProvider implements UTXOProvider {
+
+ private Bitcoiny bitcoiny;
+
+ public BitcoinyUTXOProvider(Bitcoiny bitcoiny) {
+ this.bitcoiny = bitcoiny;
+ }
+
+ @Override
+ public List getOpenTransactionOutputs(List keys) throws UTXOProviderException {
+ try {
+ List utxos = new ArrayList<>();
+
+ for( ECKey key : keys) {
+ Address address = Address.fromKey(this.bitcoiny.params, key, Script.ScriptType.P2PKH);
+ byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
+
+ // collection UTXO's for all confirmed unspent outputs
+ for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) {
+ utxos.add(toUTXO(output));
+ }
+ }
+ return utxos;
+ } catch (ForeignBlockchainException e) {
+ throw new UTXOProviderException(e);
+ }
+ }
+
+ /**
+ * Convert Unspent Output to a UTXO
+ *
+ * @param unspentOutput
+ *
+ * @return the UTXO
+ *
+ * @throws ForeignBlockchainException
+ */
+ private UTXO toUTXO(UnspentOutput unspentOutput) throws ForeignBlockchainException {
+ List transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
+
+ TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
+
+ return new UTXO(
+ Sha256Hash.wrap(unspentOutput.hash),
+ unspentOutput.index,
+ Coin.valueOf(unspentOutput.value),
+ unspentOutput.height,
+ false,
+ transactionOutput.getScriptPubKey()
+ );
+ }
+
+ @Override
+ public int getChainHeadHeight() throws UTXOProviderException {
+ try {
+ return this.bitcoiny.blockchainProvider.getCurrentHeight();
+ } catch (ForeignBlockchainException e) {
+ throw new UTXOProviderException(e);
+ }
+ }
+
+ @Override
+ public NetworkParameters getParams() {
+ return this.bitcoiny.params;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/crosschain/ChainableServer.java b/src/main/java/org/qortal/crosschain/ChainableServer.java
new file mode 100644
index 00000000..ac29e5f9
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/ChainableServer.java
@@ -0,0 +1,15 @@
+package org.qortal.crosschain;
+
+public interface ChainableServer {
+ public void addResponseTime(long responseTime);
+
+ public long averageResponseTime();
+
+ public String getHostName();
+
+ public int getPort();
+
+ public ConnectionType getConnectionType();
+
+ public enum ConnectionType {TCP, SSL}
+}
diff --git a/src/main/java/org/qortal/crosschain/Digibyte.java b/src/main/java/org/qortal/crosschain/Digibyte.java
index 4e725e89..f0a31087 100644
--- a/src/main/java/org/qortal/crosschain/Digibyte.java
+++ b/src/main/java/org/qortal/crosschain/Digibyte.java
@@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.DigibyteMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@@ -46,10 +46,6 @@ public class Digibyte extends Bitcoiny {
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002),
- new Server("electrum1-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum2-dgb.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum3-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
- new Server("electrum4-dgb.qortal.online", Server.ConnectionType.SSL, 40002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20059)
@@ -63,8 +59,7 @@ public class Digibyte extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- return MAINNET_FEE;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -114,6 +109,16 @@ public class Digibyte extends Bitcoiny {
}
};
+ private long feeCeiling = MAINNET_FEE;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -127,7 +132,7 @@ public class Digibyte extends Bitcoiny {
// Constructors and instance
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.digibyteNet = digibyteNet;
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
@@ -156,11 +161,6 @@ public class Digibyte extends Bitcoiny {
// Actual useful methods for use by other classes
- @Override
- public Coin getFeePerKb() {
- return DEFAULT_FEE_PER_KB;
- }
-
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@@ -177,4 +177,14 @@ public class Digibyte extends Bitcoiny {
return this.digibyteNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.digibyteNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.digibyteNet.setFeeCeiling( fee );
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/Dogecoin.java b/src/main/java/org/qortal/crosschain/Dogecoin.java
index 93941c41..dff98b1c 100644
--- a/src/main/java/org/qortal/crosschain/Dogecoin.java
+++ b/src/main/java/org/qortal/crosschain/Dogecoin.java
@@ -6,7 +6,7 @@ import org.bitcoinj.core.NetworkParameters;
import org.libdohj.params.DogecoinMainNetParams;
import org.libdohj.params.DogecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@@ -45,11 +45,8 @@ public class Dogecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
+ new Server("dogecoin.stackwallet.com", Server.ConnectionType.SSL, 50022),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
- new Server("electrum1-doge.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum2-doge.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum3-doge.qortal.online", Server.ConnectionType.SSL, 30002),
- new Server("electrum4-doge.qortal.online", Server.ConnectionType.SSL, 30002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20060),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20060)
@@ -63,8 +60,7 @@ public class Dogecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- return MAINNET_FEE;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -114,6 +110,16 @@ public class Dogecoin extends Bitcoiny {
}
};
+ private long feeCeiling = MAINNET_FEE;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -127,7 +133,7 @@ public class Dogecoin extends Bitcoiny {
// Constructors and instance
private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.dogecoinNet = dogecoinNet;
LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
@@ -156,11 +162,6 @@ public class Dogecoin extends Bitcoiny {
// Actual useful methods for use by other classes
- @Override
- public Coin getFeePerKb() {
- return DEFAULT_FEE_PER_KB;
- }
-
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@@ -177,4 +178,14 @@ public class Dogecoin extends Bitcoiny {
return this.dogecoinNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.dogecoinNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.dogecoinNet.setFeeCeiling( fee );
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java
index 5915ba04..27e140e2 100644
--- a/src/main/java/org/qortal/crosschain/ElectrumX.java
+++ b/src/main/java/org/qortal/crosschain/ElectrumX.java
@@ -43,12 +43,11 @@ 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 = 1000L; // ms
+ private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms
- public static class Server {
+ public static class Server implements ChainableServer {
String hostname;
- public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@@ -60,6 +59,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.port = port;
}
+ @Override
public void addResponseTime(long responseTime) {
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
this.responseTimes.remove(0);
@@ -67,6 +67,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.responseTimes.add(responseTime);
}
+ @Override
public long averageResponseTime() {
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
// Not enough readings yet
@@ -79,6 +80,21 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return 0L;
}
+ @Override
+ public String getHostName() {
+ return this.hostname;
+ }
+
+ @Override
+ public int getPort() {
+ return this.port;
+ }
+
+ @Override
+ public ConnectionType getConnectionType() {
+ return this.connectionType;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this)
@@ -104,9 +120,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
- private Set servers = new HashSet<>();
- private List remainingServers = new ArrayList<>();
- private Set uselessServers = Collections.synchronizedSet(new HashSet<>());
+ private Set servers = new HashSet<>();
+ private List remainingServers = new ArrayList<>();
+ private Set uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
@@ -114,7 +130,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private Bitcoiny blockchain;
private final Object serverLock = new Object();
- private Server currentServer;
+ private ChainableServer currentServer;
private Socket socket;
private Scanner scanner;
private int nextId = 1;
@@ -638,7 +654,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
- LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
+ LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
this.closeServer();
break;
}
@@ -663,20 +679,20 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return true;
while (!this.remainingServers.isEmpty()) {
- Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
+ ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
- SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port);
+ SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort());
int timeout = 5000; // ms
this.socket = new Socket();
this.socket.connect(endpoint, timeout);
this.socket.setTcpNoDelay(true);
- if (server.connectionType == Server.ConnectionType.SSL) {
+ if (server.getConnectionType() == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
- this.socket = factory.createSocket(this.socket, server.hostname, server.port, true);
+ this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true);
}
this.scanner = new Scanner(this.socket.getInputStream());
@@ -832,7 +848,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
* Closes connection to server if it is currently connected server.
* @param server
*/
- private void closeServer(Server server) {
+ private void closeServer(ChainableServer server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server))
return;
@@ -857,4 +873,24 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
}
}
+ @Override
+ public Set getServers() {
+ LOGGER.info("getting servers");
+ return servers;
+ }
+
+ @Override
+ public List getRemainingServers() {
+ return remainingServers;
+ }
+
+ @Override
+ public Set getUselessServers() {
+ return uselessServers;
+ }
+
+ @Override
+ public ChainableServer getCurrentServer() {
+ return currentServer;
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java
index a0f7c1cb..f13c1043 100644
--- a/src/main/java/org/qortal/crosschain/Litecoin.java
+++ b/src/main/java/org/qortal/crosschain/Litecoin.java
@@ -7,7 +7,7 @@ import org.libdohj.params.LitecoinMainNetParams;
import org.libdohj.params.LitecoinRegTestParams;
import org.libdohj.params.LitecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@@ -45,13 +45,9 @@ public class Litecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
- new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
- new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum2-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum3-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
- new Server("electrum4-ltc.qortal.online", Server.ConnectionType.SSL, 20002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
+ new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
@@ -67,8 +63,7 @@ public class Litecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- return MAINNET_FEE;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -80,9 +75,7 @@ public class Litecoin extends Bitcoiny {
@Override
public Collection getServers() {
return Arrays.asList(
- new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001),
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002),
- new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)
);
}
@@ -123,6 +116,16 @@ public class Litecoin extends Bitcoiny {
}
};
+ private long feeCeiling = MAINNET_FEE;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -136,7 +139,7 @@ public class Litecoin extends Bitcoiny {
// Constructors and instance
private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.litecoinNet = litecoinNet;
LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name()));
@@ -165,12 +168,6 @@ public class Litecoin extends Bitcoiny {
// Actual useful methods for use by other classes
- /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
- @Override
- public Coin getFeePerKb() {
- return DEFAULT_FEE_PER_KB;
- }
-
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@@ -187,4 +184,14 @@ public class Litecoin extends Bitcoiny {
return this.litecoinNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.litecoinNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.litecoinNet.setFeeCeiling( fee );
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java
index 6587baca..5475c929 100644
--- a/src/main/java/org/qortal/crosschain/PirateChain.java
+++ b/src/main/java/org/qortal/crosschain/PirateChain.java
@@ -13,7 +13,7 @@ import org.libdohj.params.PirateChainMainNetParams;
import org.qortal.api.model.crosschain.PirateChainSendRequest;
import org.qortal.controller.PirateChainWalletController;
import org.qortal.crosschain.PirateLightClient.Server;
-import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.transform.TransformationException;
@@ -51,12 +51,12 @@ public class PirateChain extends Bitcoiny {
public Collection getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
+ new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr1.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr2.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr3.qortal.online", Server.ConnectionType.SSL, 443),
new Server("wallet-arrr4.qortal.online", Server.ConnectionType.SSL, 443),
- new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443),
- new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443)
+ new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443)
);
}
@@ -67,8 +67,7 @@ public class PirateChain extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- return MAINNET_FEE;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -118,6 +117,16 @@ public class PirateChain extends Bitcoiny {
}
};
+ private long feeCeiling = MAINNET_FEE;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -131,7 +140,7 @@ public class PirateChain extends Bitcoiny {
// Constructors and instance
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.pirateChainNet = pirateChainNet;
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
@@ -160,12 +169,6 @@ public class PirateChain extends Bitcoiny {
// Actual useful methods for use by other classes
- /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
- @Override
- public Coin getFeePerKb() {
- return DEFAULT_FEE_PER_KB;
- }
-
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@@ -182,6 +185,16 @@ public class PirateChain extends Bitcoiny {
return this.pirateChainNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.pirateChainNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.pirateChainNet.setFeeCeiling( fee );
+ }
/**
* Returns confirmed balance, based on passed payment script.
*
diff --git a/src/main/java/org/qortal/crosschain/PirateLightClient.java b/src/main/java/org/qortal/crosschain/PirateLightClient.java
index be4370a0..ae7c3cc1 100644
--- a/src/main/java/org/qortal/crosschain/PirateLightClient.java
+++ b/src/main/java/org/qortal/crosschain/PirateLightClient.java
@@ -30,10 +30,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
private static final int RESPONSE_TIME_READINGS = 5;
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
- public static class Server {
+ public static class Server implements ChainableServer{
String hostname;
- public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@@ -64,6 +63,21 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return 0L;
}
+ @Override
+ public String getHostName() {
+ return this.hostname;
+ }
+
+ @Override
+ public int getPort() {
+ return this.port;
+ }
+
+ @Override
+ public ChainableServer.ConnectionType getConnectionType() {
+ return this.connectionType;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this)
@@ -89,9 +103,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
- private Set servers = new HashSet<>();
- private List remainingServers = new ArrayList<>();
- private Set uselessServers = Collections.synchronizedSet(new HashSet<>());
+ private Set servers = new HashSet<>();
+ private List remainingServers = new ArrayList<>();
+ private Set uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
@@ -99,7 +113,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
private Bitcoiny blockchain;
private final Object serverLock = new Object();
- private Server currentServer;
+ private ChainableServer currentServer;
private ManagedChannel channel;
private int nextId = 1;
@@ -525,6 +539,24 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
}
+ @Override
+ public Set getServers() {
+ return this.servers;
+ }
+
+ @Override
+ public List getRemainingServers() {
+ return this.remainingServers;
+ }
+
+ @Override
+ public Set getUselessServers() {
+ return this.uselessServers;
+ }
+
+ @Override
+ public ChainableServer getCurrentServer() { return this.currentServer; }
+
// Class-private utility methods
@@ -544,7 +576,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
- LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
+ LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName());
this.closeServer();
continue;
}
@@ -568,11 +600,11 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
return true;
while (!this.remainingServers.isEmpty()) {
- Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
+ ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
- this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build();
+ this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build();
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
@@ -604,7 +636,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
* Closes connection to server if it is currently connected server.
* @param server
*/
- private void closeServer(Server server) {
+ private void closeServer(ChainableServer server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
return;
diff --git a/src/main/java/org/qortal/crosschain/Ravencoin.java b/src/main/java/org/qortal/crosschain/Ravencoin.java
index 6b267a00..cd98fb69 100644
--- a/src/main/java/org/qortal/crosschain/Ravencoin.java
+++ b/src/main/java/org/qortal/crosschain/Ravencoin.java
@@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.RavencoinMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.crosschain.ChainableServer.ConnectionType;
import org.qortal.settings.Settings;
import java.util.Arrays;
@@ -46,10 +46,6 @@ public class Ravencoin extends Bitcoiny {
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002),
- new Server("electrum1-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum2-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum3-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
- new Server("electrum4-rvn.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20051),
@@ -65,8 +61,7 @@ public class Ravencoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- return MAINNET_FEE;
+ return this.getFeeCeiling();
}
},
TEST3 {
@@ -116,6 +111,16 @@ public class Ravencoin extends Bitcoiny {
}
};
+ private long feeCeiling = MAINNET_FEE;
+
+ public long getFeeCeiling() {
+ return feeCeiling;
+ }
+
+ public void setFeeCeiling(long feeCeiling) {
+ this.feeCeiling = feeCeiling;
+ }
+
public abstract NetworkParameters getParams();
public abstract Collection getServers();
public abstract String getGenesisHash();
@@ -129,7 +134,7 @@ public class Ravencoin extends Bitcoiny {
// Constructors and instance
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
+ super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.ravencoinNet = ravencoinNet;
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
@@ -158,11 +163,6 @@ public class Ravencoin extends Bitcoiny {
// Actual useful methods for use by other classes
- @Override
- public Coin getFeePerKb() {
- return DEFAULT_FEE_PER_KB;
- }
-
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
@@ -179,4 +179,14 @@ public class Ravencoin extends Bitcoiny {
return this.ravencoinNet.getP2shFee(timestamp);
}
+ @Override
+ public long getFeeCeiling() {
+ return this.ravencoinNet.getFeeCeiling();
+ }
+
+ @Override
+ public void setFeeCeiling(long fee) {
+
+ this.ravencoinNet.setFeeCeiling( fee );
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java b/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java
new file mode 100644
index 00000000..89674ba5
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java
@@ -0,0 +1,60 @@
+package org.qortal.crosschain;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import java.util.List;
+import java.util.Objects;
+
+@XmlAccessorType(XmlAccessType.FIELD)
+public class ServerConfigurationInfo {
+
+ private List servers;
+ private List remainingServers;
+ private List uselessServers;
+
+ public ServerConfigurationInfo() {
+ }
+
+ public ServerConfigurationInfo(
+ List servers,
+ List remainingServers,
+ List uselessServers) {
+ this.servers = servers;
+ this.remainingServers = remainingServers;
+ this.uselessServers = uselessServers;
+ }
+
+ public List getServers() {
+ return servers;
+ }
+
+ public List getRemainingServers() {
+ return remainingServers;
+ }
+
+ public List getUselessServers() {
+ return uselessServers;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ServerConfigurationInfo that = (ServerConfigurationInfo) o;
+ return Objects.equals(servers, that.servers) && Objects.equals(remainingServers, that.remainingServers) && Objects.equals(uselessServers, that.uselessServers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(servers, remainingServers, uselessServers);
+ }
+
+ @Override
+ public String toString() {
+ return "ServerConfigurationInfo{" +
+ "servers=" + servers +
+ ", remainingServers=" + remainingServers +
+ ", uselessServers=" + uselessServers +
+ '}';
+ }
+}
diff --git a/src/main/java/org/qortal/crosschain/ServerInfo.java b/src/main/java/org/qortal/crosschain/ServerInfo.java
new file mode 100644
index 00000000..0efe510b
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/ServerInfo.java
@@ -0,0 +1,74 @@
+package org.qortal.crosschain;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import java.util.Objects;
+
+@XmlAccessorType(XmlAccessType.FIELD)
+public class ServerInfo {
+
+ private long averageResponseTime;
+
+ private String hostName;
+
+ private int port;
+
+ private String connectionType;
+
+ private boolean isCurrent;
+
+ public ServerInfo() {
+ }
+
+ public ServerInfo(long averageResponseTime, String hostName, int port, String connectionType, boolean isCurrent) {
+ this.averageResponseTime = averageResponseTime;
+ this.hostName = hostName;
+ this.port = port;
+ this.connectionType = connectionType;
+ this.isCurrent = isCurrent;
+ }
+
+ public long getAverageResponseTime() {
+ return averageResponseTime;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getConnectionType() {
+ return connectionType;
+ }
+
+ public boolean isCurrent() {
+ return isCurrent;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ServerInfo that = (ServerInfo) o;
+ return averageResponseTime == that.averageResponseTime && port == that.port && isCurrent == that.isCurrent && Objects.equals(hostName, that.hostName) && Objects.equals(connectionType, that.connectionType);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(averageResponseTime, hostName, port, connectionType, isCurrent);
+ }
+
+ @Override
+ public String toString() {
+ return "ServerInfo{" +
+ "averageResponseTime=" + averageResponseTime +
+ ", hostName='" + hostName + '\'' +
+ ", port=" + port +
+ ", connectionType='" + connectionType + '\'' +
+ ", isCurrent=" + isCurrent +
+ '}';
+ }
+}
diff --git a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
index f14efae8..3643e552 100644
--- a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
+++ b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java
@@ -1,33 +1,33 @@
package org.qortal.crypto;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
+import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public abstract class TrustlessSSLSocketFactory {
- // Create a trust manager that does not validate certificate chains
+ /**
+ * Creates a SSLSocketFactory that ignore certificate chain validation because ElectrumX servers use mostly
+ * self signed certificates.
+ */
private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] {
new X509TrustManager() {
- public java.security.cert.X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
}
-
- public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
+ public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
-
- public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
+ public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
- // Install the all-trusting trust manager
+ /**
+ * Install the all-trusting trust manager.
+ */
private static final SSLContext sc;
static {
try {
- sc = SSLContext.getInstance("TLSv1.3");
+ sc = SSLContext.getInstance("SSL");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);
@@ -37,5 +37,4 @@ public abstract class TrustlessSSLSocketFactory {
public static SSLSocketFactory getSocketFactory() {
return sc.getSocketFactory();
}
-
}
diff --git a/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java
new file mode 100644
index 00000000..04c7a2a9
--- /dev/null
+++ b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java
@@ -0,0 +1,32 @@
+package org.qortal.data.crosschain;
+
+import org.qortal.crosschain.BitcoinyTransaction;
+import org.qortal.crosschain.TransactionHash;
+
+import java.util.List;
+import java.util.Map;
+
+public class AtomicTransactionData {
+ public final TransactionHash hash;
+ public final Integer timestamp;
+ public final List inputs;
+ public final Map, Long> valueByAddress;
+ public final long totalAmount;
+ public final int size;
+
+ public AtomicTransactionData(
+ TransactionHash hash,
+ Integer timestamp,
+ List inputs,
+ Map, Long> valueByAddress,
+ long totalAmount,
+ int size) {
+
+ this.hash = hash;
+ this.timestamp = timestamp;
+ this.inputs = inputs;
+ this.valueByAddress = valueByAddress;
+ this.totalAmount = totalAmount;
+ this.size = size;
+ }
+}
diff --git a/src/main/java/org/qortal/data/crosschain/TransactionSummary.java b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java
new file mode 100644
index 00000000..ac67a2f6
--- /dev/null
+++ b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java
@@ -0,0 +1,106 @@
+package org.qortal.data.crosschain;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+@XmlAccessorType(XmlAccessType.FIELD)
+public class TransactionSummary {
+
+ private String atAddress;
+ private String p2shValue;
+ private String p2shAddress;
+ private String lockingHash;
+ private Integer lockingTimestamp;
+ private long lockingTotalAmount;
+ private long lockingFee;
+ private int lockingSize;
+ private String unlockingHash;
+ private Integer unlockingTimestamp;
+ private long unlockingTotalAmount;
+ private long unlockingFee;
+ private int unlockingSize;
+
+ public TransactionSummary(){}
+
+ public TransactionSummary(
+ String atAddress,
+ String p2shValue,
+ String p2shAddress,
+ String lockingHash,
+ Integer lockingTimestamp,
+ long lockingTotalAmount,
+ long lockingFee,
+ int lockingSize,
+ String unlockingHash,
+ Integer unlockingTimestamp,
+ long unlockingTotalAmount,
+ long unlockingFee,
+ int unlockingSize) {
+
+ this.atAddress = atAddress;
+ this.p2shValue = p2shValue;
+ this.p2shAddress = p2shAddress;
+ this.lockingHash = lockingHash;
+ this.lockingTimestamp = lockingTimestamp;
+ this.lockingTotalAmount = lockingTotalAmount;
+ this.lockingFee = lockingFee;
+ this.lockingSize = lockingSize;
+ this.unlockingHash = unlockingHash;
+ this.unlockingTimestamp = unlockingTimestamp;
+ this.unlockingTotalAmount = unlockingTotalAmount;
+ this.unlockingFee = unlockingFee;
+ this.unlockingSize = unlockingSize;
+ }
+
+ public String getAtAddress() {
+ return atAddress;
+ }
+
+ public String getP2shValue() {
+ return p2shValue;
+ }
+
+ public String getP2shAddress() {
+ return p2shAddress;
+ }
+
+ public String getLockingHash() {
+ return lockingHash;
+ }
+
+ public Integer getLockingTimestamp() {
+ return lockingTimestamp;
+ }
+
+ public long getLockingTotalAmount() {
+ return lockingTotalAmount;
+ }
+
+ public long getLockingFee() {
+ return lockingFee;
+ }
+
+ public int getLockingSize() {
+ return lockingSize;
+ }
+
+ public String getUnlockingHash() {
+ return unlockingHash;
+ }
+
+ public Integer getUnlockingTimestamp() {
+ return unlockingTimestamp;
+ }
+
+ public long getUnlockingTotalAmount() {
+ return unlockingTotalAmount;
+ }
+
+ public long getUnlockingFee() {
+ return unlockingFee;
+ }
+
+ public int getUnlockingSize() {
+ return unlockingSize;
+ }
+}
diff --git a/src/main/java/org/qortal/gui/SysTray.java b/src/main/java/org/qortal/gui/SysTray.java
index abd433f3..74a68618 100644
--- a/src/main/java/org/qortal/gui/SysTray.java
+++ b/src/main/java/org/qortal/gui/SysTray.java
@@ -5,7 +5,6 @@ import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.globalization.Translator;
import org.qortal.settings.Settings;
-import org.qortal.utils.RandomizeList;
import org.qortal.utils.URLViewer;
import javax.swing.*;
@@ -140,14 +139,6 @@ public class SysTray {
}
});
- /* JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI"));
- openUi.addActionListener(actionEvent -> {
- destroyHiddenDialog();
-
- new OpenUiWorker().execute();
- });
- menu.add(openUi); */
-
JMenuItem openTimeCheck = new JMenuItem(Translator.INSTANCE.translate("SysTray", "CHECK_TIME_ACCURACY"));
openTimeCheck.addActionListener(actionEvent -> {
destroyHiddenDialog();
@@ -190,48 +181,6 @@ public class SysTray {
return menu;
}
- static class OpenUiWorker extends SwingWorker {
- @Override
- protected Void doInBackground() {
- List uiServers = new ArrayList<>();
-
- String[] remoteUiServers = Settings.getInstance().getRemoteUiServers();
- uiServers.addAll(Arrays.asList(remoteUiServers));
- // Randomize remote servers
- uiServers = RandomizeList.randomize(uiServers);
-
- // Prepend local servers
- String[] localUiServers = Settings.getInstance().getLocalUiServers();
- uiServers.addAll(0, Arrays.asList(localUiServers));
-
- // Check each server in turn before opening browser tab
- int uiPort = Settings.getInstance().getUiServerPort();
- for (String uiServer : uiServers) {
- InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort);
-
- // If we couldn't resolve try next
- if (socketAddress.isUnresolved())
- continue;
-
- try (SocketChannel socketChannel = SocketChannel.open()) {
- socketChannel.socket().connect(socketAddress, 100);
-
- // If we reach here, then socket connected to UI server!
- URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort)));
-
- return null;
- } catch (IOException e) {
- // try next server
- } catch (Exception e) {
- LOGGER.error("Unable to open UI website in browser");
- return null;
- }
- }
-
- return null;
- }
- }
-
static class SynchronizeClockWorker extends SwingWorker {
@Override
protected Void doInBackground() {
diff --git a/src/main/java/org/qortal/network/RNSNetwork.java b/src/main/java/org/qortal/network/RNSNetwork.java
new file mode 100644
index 00000000..2472c891
--- /dev/null
+++ b/src/main/java/org/qortal/network/RNSNetwork.java
@@ -0,0 +1,425 @@
+package org.qortal.network;
+
+import java.io.IOException;
+//import java.nio.channels.SelectionKey;
+//import java.io.Paths;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.io.File;
+import java.util.*;
+//import java.util.function.BiConsumer;
+//import java.util.function.Consumer;
+//import java.util.function.Function;
+//import java.util.concurrent.*;
+//import java.util.concurrent.atomic.AtomicLong;
+
+//import org.qortal.data.network.PeerData;
+import org.qortal.repository.DataException;
+//import org.qortal.settings.Settings;
+import org.qortal.settings.Settings;
+//import org.qortal.utils.NTP;
+
+//import com.fasterxml.jackson.annotation.JsonGetter;
+
+import org.apache.commons.codec.binary.Hex;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+import io.reticulum.Reticulum;
+import io.reticulum.Transport;
+import io.reticulum.destination.Destination;
+import io.reticulum.destination.DestinationType;
+import io.reticulum.destination.Direction;
+import io.reticulum.identity.Identity;
+import io.reticulum.interfaces.ConnectionInterface;
+import io.reticulum.destination.ProofStrategy;
+import io.reticulum.transport.AnnounceHandler;
+import static io.reticulum.constant.ReticulumConstant.CONFIG_FILE_NAME;
+//import static io.reticulum.identity.IdentityKnownDestination.recall;
+//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
+//import static io.reticulum.destination.Direction.OUT;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.Synchronized;
+import io.reticulum.link.Link;
+import io.reticulum.link.LinkStatus;
+//import io.reticulum.packet.PacketReceipt;
+import io.reticulum.packet.Packet;
+
+//import static io.reticulum.link.LinkStatus.ACTIVE;
+import static io.reticulum.link.LinkStatus.CLOSED;
+import static io.reticulum.link.LinkStatus.PENDING;
+import static io.reticulum.link.LinkStatus.STALE;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+//import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+
+//import org.qortal.network.Network.NetworkProcessor;
+//import org.qortal.utils.ExecuteProduceConsume;
+//import org.qortal.utils.NamedThreadFactory;
+
+//import java.time.Instant;
+
+//import org.qortal.network.RNSPeer;
+
+@Slf4j
+public class RNSNetwork {
+
+ static final String APP_NAME = "qortal";
+ private Reticulum reticulum;
+ private Identity server_identity;
+ private Destination baseDestination; // service base (initially: anything node2node)
+ //private Destination dataDestination; // qdn services (eg. files like music, videos etc)
+ //private Destination liveDestination; // live/dynamic peer list (eg. video conferencing)
+ // the following should be retrieved from settings
+ private static Integer MAX_PEERS = 3;
+ private static Integer MIN_DESIRED_PEERS = 3;
+ //private final Integer MAX_PEERS = Settings.getInstance().getMaxReticulumPeers();
+ //private final Integer MIN_DESIRED_PEERS = Settings.getInstance().getMinDesiredReticulumPeers();
+ static final String defaultConfigPath = new String(".reticulum"); // if empty will look in Reticulums default paths
+ //private final String defaultConfigPath = Settings.getInstance().getDefaultConfigPathForReticulum();
+
+ //private static final Logger logger = LoggerFactory.getLogger(RNSNetwork.class);
+
+ //private final List linkedPeers = Collections.synchronizedList(new ArrayList<>());
+ //private List immutableLinkedPeers = Collections.emptyList();
+ private final List linkedPeers = Collections.synchronizedList(new ArrayList<>());
+
+ //private final ExecuteProduceConsume rnsNetworkEPC;
+ private static final long NETWORK_EPC_KEEPALIVE = 1000L; // 1 second
+ private volatile boolean isShuttingDown = false;
+ private int totalThreadCount = 0;
+
+ // TODO: settings - MaxReticulumPeers, MaxRNSNetworkThreadPoolSize (if needed)
+
+ // Constructor
+ private RNSNetwork () {
+ try {
+ initConfig(defaultConfigPath);
+ reticulum = new Reticulum(defaultConfigPath);
+ log.info("reticulum instance created: {}", reticulum.toString());
+ } catch (IOException e) {
+ log.error("unable to create Reticulum network", e);
+ }
+
+ // Settings.getInstance().getMaxRNSNetworkThreadPoolSize(), // statically set to 5 below
+ //ExecutorService RNSNetworkExecutor = new ThreadPoolExecutor(1,
+ // 5,
+ // NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
+ // new SynchronousQueue(),
+ // new NamedThreadFactory("RNSNetwork-EPC"));
+ //rnsNetworkEPC = new RNSNetworkProcessor(RNSNetworkExecutor);
+ }
+
+ // Note: potentially create persistent server_identity (utility rnid) and load it from file
+ public void start() throws IOException, DataException {
+
+ // create identity either from file or new (creating new keys)
+ var serverIdentityPath = reticulum.getStoragePath().resolve(APP_NAME);
+ if (Files.isReadable(serverIdentityPath)) {
+ server_identity = Identity.fromFile(serverIdentityPath);
+ log.info("server identity loaded from file {}", serverIdentityPath.toString());
+ } else {
+ server_identity = new Identity();
+ log.info("new server identity created dynamically.");
+ }
+ log.debug("Server Identity: {}", server_identity.toString());
+
+ // show the ifac_size of the configured interfaces (debug code)
+ for (ConnectionInterface i: Transport.getInstance().getInterfaces() ) {
+ log.info("interface {}, length: {}", i.getInterfaceName(), i.getIfacSize());
+ }
+
+ baseDestination = new Destination(
+ server_identity,
+ Direction.IN,
+ DestinationType.SINGLE,
+ APP_NAME,
+ "core"
+ );
+ //// ideas for other entry points
+ //dataDestination = new Destination(
+ // server_identity,
+ // Direction.IN,
+ // DestinationType.SINGLE,
+ // APP_NAME,
+ // "core",
+ // "qdn"
+ //);
+ //liveDestination = new Destination(
+ // server_identity,
+ // Direction.IN,
+ // DestinationType.SINGLE,
+ // APP_NAME,
+ // "core",
+ // "live"
+ //);
+ log.info("Destination "+Hex.encodeHexString(baseDestination.getHash())+" "+baseDestination.getName()+" running.");
+ //log.info("Destination "+Hex.encodeHexString(dataDestination.getHash())+" "+dataDestination.getName()+" running.");
+
+ baseDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
+ //dataDestination.setProofStrategy(ProofStrategy.PROVE_ALL);
+
+ baseDestination.setAcceptLinkRequests(true);
+ //dataDestination.setAcceptLinkRequests(true);
+ //baseDestination.setLinkEstablishedCallback(this::linkExtabishedCallback);
+ baseDestination.setPacketCallback(this::packetCallback);
+ //baseDestination.setPacketCallback((message, packet) -> {
+ // log.info("xyz - Message raw {}", message);
+ // log.info("xyz - Packet {}", packet.toString());
+ //});
+
+ Transport.getInstance().registerAnnounceHandler(new QAnnounceHandler());
+ log.info("announceHandlers: {}", Transport.getInstance().getAnnounceHandlers());
+
+ baseDestination.announce();
+ //dataDestination.announce();
+ log.info("Sent initial announce from {} ({})", Hex.encodeHexString(baseDestination.getHash()), baseDestination.getName());
+
+ // Start up first networking thread (the "server loop")
+ //rnsNetworkEPC.start();
+ }
+
+ public void shutdown() {
+ isShuttingDown = true;
+ log.info("shutting down Reticulum");
+
+ // Stop processing threads (the "server loop")
+ //try {
+ // if (!this.rnsNetworkEPC.shutdown(5000)) {
+ // logger.warn("Network threads failed to terminate");
+ // }
+ //} catch (InterruptedException e) {
+ // logger.warn("Interrupted while waiting for networking threads to terminate");
+ //}
+
+ // Disconnect peers and terminate Reticulum
+ for (RNSPeer p : linkedPeers) {
+ if (nonNull(p.getLink())) {
+ p.getLink().teardown();
+ }
+ }
+ reticulum.exitHandler();
+ }
+
+ private void initConfig(String configDir) throws IOException {
+ File configDir1 = new File(defaultConfigPath);
+ if (!configDir1.exists()) {
+ configDir1.mkdir();
+ }
+ var configPath = Path.of(configDir1.getAbsolutePath());
+ Path configFile = configPath.resolve(CONFIG_FILE_NAME);
+
+ if (Files.notExists(configFile)) {
+ var defaultConfig = this.getClass().getClassLoader().getResourceAsStream("reticulum_default_config.yml");
+ Files.copy(defaultConfig, configFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private void packetCallback(byte[] message, Packet packet) {
+ log.info("xyz - Message raw {}", message);
+ log.info("xyz - Packet {}", packet.toString());
+ }
+
+ //public void announceBaseDestination () {
+ // getBaseDestination().announce();
+ //}
+
+ //public Consumer clientConnected(Link link) {
+ // log.info("Client connected");
+ // link.setLinkClosedCallback(clientDisconnected(link));
+ // link.setPacketCallback(null);
+ //}
+
+ //public void clientDisconnected(Link link) {
+ // log.info("Client disconnected");
+ // linkedPeers.remove(link);
+ //}
+
+ // client part
+ //@Slf4j
+ private static class QAnnounceHandler implements AnnounceHandler {
+ @Override
+ public String getAspectFilter() {
+ // handle all announces
+ return null;
+ }
+
+ @Override
+ @Synchronized
+ public void receivedAnnounce(byte[] destinationHash, Identity announcedIdentity, byte[] appData) {
+ var peerExists = false;
+
+ log.info("Received an announce from {}", Hex.encodeHexString(destinationHash));
+ //log.info("aspect: {}", getAspectFilter());
+ //log.info("destinationhash: {}, announcedIdentity: {}, appData: {}", destinationHash, announcedIdentity, appData);
+
+ if (nonNull(appData)) {
+ log.debug("The announce contained the following app data: {}", new String(appData, UTF_8));
+ }
+
+ // add to peer list if we can use more peers
+ //synchronized (this) {
+ List lps = RNSNetwork.getInstance().getLinkedPeers();
+ if (lps.size() < MAX_PEERS) {
+ for (RNSPeer p : lps) {
+ //log.info("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
+ if (Arrays.equals(p.getDestinationLink().getDestination().getHash(), destinationHash)) {
+ peerExists = true;
+ log.debug("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash);
+ break;
+ }
+ }
+ if (!peerExists) {
+ //log.info("announce handler - cerate new peer: **announcedIdentity**: {}, **recall**: {}", announcedIdentity, recall(destinationHash));
+ RNSPeer newPeer = new RNSPeer(destinationHash);
+ lps.add(newPeer);
+ log.info("added new RNSPeer, Destination - {}, Link: {}", newPeer.getDestinationHash(), newPeer.getDestinationLink());
+ }
+ }
+ //}
+ }
+ }
+
+ // Main thread
+
+ //class RNSNetworkProcessor extends ExecuteProduceConsume {
+ //
+ // //private final Logger logger = LoggerFactory.getLogger(RNSNetworkProcessor.class);
+ //
+ // private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
+ // private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
+ //
+ // private Iterator channelIterator = null;
+ //
+ // RNSNetworkProcessor(ExecutorService executor) {
+ // super(executor);
+ // }
+ //
+ // @Override
+ // protected void onSpawnFailure() {
+ // // For debugging:
+ // // ExecutorDumper.dump(this.executor, 3, ExecuteProduceConsume.class);
+ // }
+ //
+ // @Override
+ // protected Task produceTask(boolean canBlock) throws InterruptedException {
+ // Task task;
+ //
+ // //task = maybeProducePeerMessageTask();
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //final Long now = NTP.getTime();
+ // //
+ // //task = maybeProducePeerPingTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //task = maybeProduceConnectPeerTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // //task = maybeProduceBroadcastTask(now);
+ // //if (task != null) {
+ // // return task;
+ // //}
+ // //
+ // // Only this method can block to reduce CPU spin
+ // //return maybeProduceChannelTask(canBlock);
+ //
+ // // TODO: flesh out the tasks handled by Reticulum
+ // return null;
+ // }
+ // //...TODO: implement abstract methods...
+ //}
+
+
+ // getter / setter
+ private static class SingletonContainer {
+ private static final RNSNetwork INSTANCE = new RNSNetwork();
+ }
+
+ public static RNSNetwork getInstance() {
+ return SingletonContainer.INSTANCE;
+ }
+
+ public List getLinkedPeers() {
+ synchronized(this.linkedPeers) {
+ //return new ArrayList<>(this.linkedPeers);
+ return this.linkedPeers;
+ }
+ }
+
+ public Integer getTotalPeers() {
+ synchronized (this) {
+ return linkedPeers.size();
+ }
+ }
+
+ public Destination getBaseDestination() {
+ return baseDestination;
+ }
+
+ // maintenance
+
+ //private static class AnnounceTimer {
+ // //public void main(String[] args) throws InterruptedException
+ // public void main(String[] args) throws InterruptedException
+ // {
+ // Timer timer = new Timer();
+ // // run timer every 10s (10000ms)
+ // timer.schedule(new TimerTask() {
+ // @Override
+ // public void run() {
+ // System.out.println("AnnounceTimer: " + new java.util.Date());
+ // }
+ // }, 0, 10000);
+ // }
+ //}
+
+ @Synchronized
+ public void prunePeers() throws DataException {
+ // run periodically (by the Controller)
+ //log.info("Peer list (linkedPeers): {}",this.linkedPeers.toString());
+ //synchronized(this) {
+ //List linkList = getLinkedPeers();
+ List peerList = this.linkedPeers;
+ log.info("List of RNSPeers: {}", this.linkedPeers);
+ //log.info("number of links (linkedPeers) before prunig: {}", this.linkedPeers.size());
+ Link pLink;
+ LinkStatus lStatus;
+ for (RNSPeer p: peerList) {
+ pLink = p.getLink();
+ lStatus = pLink.getStatus();
+ //log.debug("link status: "+lStatus.toString());
+ // lStatus in: PENDING, HANDSHAKE, ACTIVE, STALE, CLOSED
+ if (lStatus == CLOSED) {
+ p.resetPeer();
+ peerList.remove(p);
+ } else if (lStatus == STALE) {
+ pLink.teardown();
+ p.resetPeer();
+ peerList.remove(p);
+ } else if (lStatus == PENDING) {
+ log.info("prunePeers - link state still {}", lStatus);
+ // TODO: can we help the Link along somehow?
+ }
+ }
+ log.info("number of links (linkedPeers) after prunig: {}", this.linkedPeers.size());
+ //}
+ maybeAnnounce(getBaseDestination());
+ }
+
+ public void maybeAnnounce(Destination d) {
+ if (getLinkedPeers().size() < MIN_DESIRED_PEERS) {
+ d.announce();
+ }
+ }
+
+}
+
diff --git a/src/main/java/org/qortal/network/RNSPeer.java b/src/main/java/org/qortal/network/RNSPeer.java
new file mode 100644
index 00000000..871bb347
--- /dev/null
+++ b/src/main/java/org/qortal/network/RNSPeer.java
@@ -0,0 +1,110 @@
+package org.qortal.network;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.isNull;
+
+import org.qortal.network.RNSNetwork;
+import io.reticulum.link.Link;
+import io.reticulum.packet.Packet;
+import io.reticulum.identity.Identity;
+import io.reticulum.channel.Channel;
+import io.reticulum.destination.Destination;
+import io.reticulum.destination.DestinationType;
+import io.reticulum.destination.Direction;
+
+import static io.reticulum.identity.IdentityKnownDestination.recall;
+//import static io.reticulum.identity.IdentityKnownDestination.recallAppData;
+import lombok.extern.slf4j.Slf4j;
+import lombok.Setter;
+import lombok.Data;
+import lombok.AccessLevel;
+
+@Data
+@Slf4j
+public class RNSPeer {
+
+ private byte[] destinationHash;
+ private Link destinationLink;
+ private Identity destinationIdentity;
+ @Setter(AccessLevel.PACKAGE) private long creationTimestamp;
+ private Long lastAccessTimestamp;
+
+ // constructors
+ public RNSPeer (byte[] dhash) {
+ this.destinationHash = dhash;
+ this.destinationIdentity = recall(dhash);
+ Link newLink = new Link(
+ new Destination(
+ this.destinationIdentity,
+ Direction.OUT,
+ DestinationType.SINGLE,
+ RNSNetwork.APP_NAME,
+ "core"
+ )
+ );
+ this.destinationLink = newLink;
+ destinationLink.setPacketCallback(this::packetCallback);
+ }
+
+ public RNSPeer (Link newLink) {
+ this.destinationHash = newLink.getDestination().getHash();
+ this.destinationLink = newLink;
+ this.destinationIdentity = newLink.getRemoteIdentity();
+ setCreationTimestamp(System.currentTimeMillis());
+ this.lastAccessTimestamp = null;
+ destinationLink.setPacketCallback(this::packetCallback);
+ }
+
+ public RNSPeer () {
+ this.destinationHash = null;
+ this.destinationLink = null;
+ this.destinationIdentity = null;
+ setCreationTimestamp(System.currentTimeMillis());
+ this.lastAccessTimestamp = null;
+ }
+
+ // utilities (change Link type, call tasks, ...)
+ //...
+
+ private void packetCallback(byte[] message, Packet packet) {
+ log.debug("Message raw {}", message);
+ log.debug("Packet {}", packet.toString());
+ // ...
+ }
+
+ public Link getLink() {
+ if (isNull(getDestinationLink())) {
+ Link newLink = new Link(
+ new Destination(
+ this.destinationIdentity,
+ Direction.OUT,
+ DestinationType.SINGLE,
+ RNSNetwork.APP_NAME,
+ "core"
+ )
+ );
+ this.destinationLink = newLink;
+ return newLink;
+ }
+ return getDestinationLink();
+ }
+
+ public Channel getChannel() {
+ if (isNull(getDestinationLink())) {
+ log.warn("link is null.");
+ return null;
+ }
+ setLastAccessTimestamp(System.currentTimeMillis());
+ return getDestinationLink().getChannel();
+ }
+
+ public void resetPeer () {
+ this.destinationHash = null;
+ this.destinationLink = null;
+ this.destinationIdentity = null;
+ this.lastAccessTimestamp = null;
+ }
+
+}
diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java
index e007586e..d4517485 100644
--- a/src/main/java/org/qortal/repository/TransactionRepository.java
+++ b/src/main/java/org/qortal/repository/TransactionRepository.java
@@ -205,6 +205,15 @@ public interface TransactionRepository {
*/
public List getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
+ /**
+ * Returns list of transfer asset transaction creators.
+ * This uses confirmed transactions only.
+ *
+ * @return
+ * @throws DataException
+ */
+ public List getConfirmedTransferAssetCreators() throws DataException;
+
/**
* Returns list of transactions pending approval, with optional txGgroupId filtering.
*
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
index 829f7aab..54af22e9 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -1047,6 +1047,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ArbitraryIdentifierIndex ON ArbitraryTransactions (identifier)");
break;
+ case 49:
+ // Update blocks minted penalty
+ stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0");
+ break;
+
default:
// nothing to do
return false;
diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
index 5b41a85d..fe0b4d0b 100644
--- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -1043,6 +1043,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
+ public List getConfirmedTransferAssetCreators() throws DataException {
+ List transferAssetCreators = new ArrayList<>();
+
+ String sql = "SELECT account "
+ + "FROM TransferAssetTransactions "
+ + "JOIN Accounts ON Accounts.public_key = TransferAssetTransactions.sender "
+ + "JOIN Transactions ON Transactions.signature = TransferAssetTransactions.signature "
+ + "WHERE block_height IS NOT NULL AND TransferAssetTransactions.recipient != Accounts.account "
+ + "GROUP BY account "
+ + "ORDER BY account";
+
+ try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
+ if (resultSet == null)
+ return transferAssetCreators;
+
+ do {
+ String address = resultSet.getString(1);
+
+ transferAssetCreators.add(address);
+ } while (resultSet.next());
+
+ return transferAssetCreators;
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch transfer asset from repository", e);
+ }
+ }
+
@Override
public List getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index ba90208b..df59c037 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -62,16 +62,6 @@ public class Settings {
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
private String bindAddressFallback = "0.0.0.0"; // Some systems are unable to bind using IPv6
- // UI servers
- private int uiPort = 12388;
- private String[] uiLocalServers = new String[] {
- "localhost", "127.0.0.1"
- };
- private String[] uiRemoteServers = new String[] {
- "node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
- "node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org"
- };
-
// API-related
private boolean apiEnabled = true;
private Integer apiPort;
@@ -138,11 +128,11 @@ public class Settings {
private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default
/** Whether to show a notification when we perform repository 'checkpoint'. */
private boolean showCheckpointNotification = false;
- /* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare */
- private int blockCacheSize = 10;
+ /* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare - increased to 100 */
+ private int blockCacheSize = 100;
/** Maximum number of transactions for the block minter to include in a block */
- private int maxTransactionsPerBlock = 50;
+ private int maxTransactionsPerBlock = 100;
/** How long to keep old, full, AT state data (ms). */
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
@@ -164,7 +154,7 @@ public class Settings {
private boolean lite = false;
/** Whether we should prune old data to reduce database size
- * This prevents the node from being able to serve older blocks */
+ * This prevents the node from being able to serve older blocks - No longer used */
private boolean topOnly = false;
/** The amount of recent blocks we should keep when pruning */
private int pruneBlockLimit = 6000;
@@ -205,13 +195,13 @@ public class Settings {
/** Minimum number of peers to allow block minting / synchronization. */
private int minBlockchainPeers = 3;
/** Target number of outbound connections to peers we should make. */
- private int minOutboundPeers = 16;
+ private int minOutboundPeers = 32;
/** Maximum number of peer connections we allow. */
- private int maxPeers = 40;
+ private int maxPeers = 60;
/** Number of slots to reserve for short-lived QDN data transfers */
- private int maxDataPeers = 4;
+ private int maxDataPeers = 5;
/** Maximum number of threads for network engine. */
- private int maxNetworkThreadPoolSize = 120;
+ private int maxNetworkThreadPoolSize = 620;
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
private int networkPoWComputePoolSize = 2;
/** Maximum number of retry attempts if a peer fails to respond with the requested data */
@@ -221,7 +211,7 @@ public class Settings {
public long recoveryModeTimeout = 9999999999999L;
/** Minimum peer version number required in order to sync with them */
- private String minPeerVersion = "4.3.2";
+ private String minPeerVersion = "4.5.0";
/** Whether to allow connections with peers below minPeerVersion
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
* If false, sync will be blocked both ways, and they will not appear in the peers list */
@@ -269,7 +259,7 @@ public class Settings {
/** Repository storage path. */
private String repositoryPath = "db";
/** Repository connection pool size. Needs to be a bit bigger than maxNetworkThreadPoolSize */
- private int repositoryConnectionPoolSize = 240;
+ private int repositoryConnectionPoolSize = 1920;
private List fixedNetwork;
// Export/import
@@ -282,8 +272,7 @@ public class Settings {
private String[] bootstrapHosts = new String[] {
"http://bootstrap.qortal.org",
"http://bootstrap2.qortal.org",
- "http://bootstrap3.qortal.org",
- "http://bootstrap.qortal.online"
+ "http://bootstrap3.qortal.org"
};
// Auto-update sources
@@ -370,7 +359,7 @@ public class Settings {
/** Whether to allow public (decryptable) data to be stored */
private boolean publicDataEnabled = true;
/** Whether to allow private (non-decryptable) data to be stored */
- private boolean privateDataEnabled = false;
+ private boolean privateDataEnabled = true;
/** Maximum total size of hosted data, in bytes. Unlimited if null */
private Long maxStorageCapacity = null;
@@ -620,18 +609,6 @@ public class Settings {
return this.localeLang;
}
- public int getUiServerPort() {
- return this.uiPort;
- }
-
- public String[] getLocalUiServers() {
- return this.uiLocalServers;
- }
-
- public String[] getRemoteUiServers() {
- return this.uiRemoteServers;
- }
-
public boolean isApiEnabled() {
return this.apiEnabled;
}
diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java
index 79976199..a89e60c0 100644
--- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java
+++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java
@@ -54,6 +54,10 @@ public class RegisterNameTransaction extends Transaction {
Account registrant = getRegistrant();
String name = this.registerNameTransactionData.getName();
+ int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
+ final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180;
+ final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
+
// Check name size bounds
int nameLength = Utf8.encodedLength(name);
if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE)
@@ -76,6 +80,10 @@ public class RegisterNameTransaction extends Transaction {
if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee())
return ValidationResult.NO_BALANCE;
+ // Check if we are on algo runs
+ if (blockchainHeight >= start && blockchainHeight <= end)
+ return ValidationResult.TEMPORARY_DISABLED;
+
return ValidationResult.OK;
}
diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java
index ab66dec6..635c8c8a 100644
--- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java
+++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java
@@ -3,6 +3,7 @@ package org.qortal.transaction;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
+import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.RewardShareData;
@@ -180,6 +181,33 @@ public class RewardShareTransaction extends Transaction {
// Nothing to do
}
+ @Override
+ public boolean isConfirmableAtHeight(int height) {
+ final int startV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 15;
+ final int startV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() - 15;
+ final int endV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() + 10;
+ final int endV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() + 10;
+
+ if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) {
+ // Not confirmable in online accounts or distribution blocks
+ if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) {
+ return false;
+ }
+ }
+
+ if (height >= startV2 && height <= endV2) {
+ // Not confirmable on algo V2 run
+ return false;
+ }
+
+ if (height >= startV3 && height <= endV3) {
+ // Not confirmable on algo V3 run
+ return false;
+ }
+
+ return true;
+ }
+
@Override
public void process() throws DataException {
PublicKeyAccount mintingAccount = getMintingAccount();
diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java
index 61b78ade..5e5b9fba 100644
--- a/src/main/java/org/qortal/transaction/Transaction.java
+++ b/src/main/java/org/qortal/transaction/Transaction.java
@@ -173,6 +173,7 @@ public abstract class Transaction {
INVALID_OPTION_LENGTH(20),
DUPLICATE_OPTION(21),
POLL_ALREADY_EXISTS(22),
+ POLL_ALREADY_HAS_VOTES(23),
POLL_DOES_NOT_EXIST(24),
POLL_OPTION_DOES_NOT_EXIST(25),
ALREADY_VOTED_FOR_THAT_OPTION(26),
@@ -246,6 +247,8 @@ public abstract class Transaction {
NAME_BLOCKED(97),
GROUP_APPROVAL_REQUIRED(98),
ACCOUNT_NOT_TRANSFERABLE(99),
+ TRANSFER_PRIVS_DISABLED(100),
+ TEMPORARY_DISABLED(101),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000),
NOT_SUPPORTED(1001);
@@ -904,6 +907,15 @@ public abstract class Transaction {
return true;
}
+ /**
+ * Returns whether transaction is confirmable in a block at a given height.
+ * @return
+ */
+ public boolean isConfirmableAtHeight(int height) {
+ /* To be optionally overridden */
+ return true;
+ }
+
/**
* Returns whether transaction can be added to the blockchain.
*
diff --git a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java
index 50c6f24a..2144e4b9 100644
--- a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java
+++ b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java
@@ -1,6 +1,7 @@
package org.qortal.transaction;
import org.qortal.account.Account;
+import org.qortal.block.BlockChain;
import org.qortal.data.PaymentData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
@@ -51,6 +52,14 @@ public class TransferAssetTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
+ int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
+ final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height();
+ final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height();
+
+ // Check if we are on algo runs
+ if (blockchainHeight >= start && blockchainHeight <= end)
+ return ValidationResult.ASSET_NOT_SPENDABLE;
+
// Wrap asset transfer as a payment and delegate final payment checks to Payment class
return new Payment(this.repository).isValid(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee());
}
diff --git a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java
index de3038d8..588f0d09 100644
--- a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java
+++ b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
+import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@@ -72,6 +73,13 @@ public class TransferPrivsTransaction extends Transaction {
if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0)
return ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
+ // Disable Transfer Privs (start - end) from feature trigger
+ long transactionTimestamp = this.transferPrivsTransactionData.getTimestamp();
+ final long startTimestamp = BlockChain.getInstance().getDisableTransferPrivsTimestamp();
+ final long endTimestamp = BlockChain.getInstance().getEnableTransferPrivsTimestamp();
+ if (transactionTimestamp > startTimestamp && transactionTimestamp < endTimestamp)
+ return ValidationResult.TRANSFER_PRIVS_DISABLED;
+
return ValidationResult.OK;
}
@@ -80,6 +88,17 @@ public class TransferPrivsTransaction extends Transaction {
// Nothing to do
}
+ @Override
+ public boolean isConfirmableAtHeight(int height) {
+ if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) {
+ // Not confirmable in online accounts or distribution blocks
+ if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
@Override
public void process() throws DataException {
Account sender = this.getSender();
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index 2fc69347..8b8373a8 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -30,6 +30,9 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
+ "selfSponsorshipAlgoV2SnapshotTimestamp": 1708360200000,
+ "selfSponsorshipAlgoV3SnapshotTimestamp": 1708432200000,
+ "referenceTimestampBlock": 1670684455220,
"mempowTransactionUpdatesTimestamp": 1693558800000,
"blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000,
@@ -93,9 +96,14 @@
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"onlineAccountMinterLevelValidationHeight": 1092000,
"selfSponsorshipAlgoV1Height": 1092400,
+ "selfSponsorshipAlgoV2Height": 1611200,
+ "selfSponsorshipAlgoV3Height": 1612200,
"feeValidationFixTimestamp": 1671918000000,
"chatReferenceTimestamp": 1674316800000,
- "arbitraryOptionalFeeTimestamp": 1680278400000
+ "arbitraryOptionalFeeTimestamp": 1680278400000,
+ "unconfirmableRewardSharesHeight": 1575500,
+ "disableTransferPrivsTimestamp": 1706745000000,
+ "enableTransferPrivsTimestamp": 1709251200000
},
"checkpoints": [
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
diff --git a/src/main/resources/i18n/ApiError_he.properties b/src/main/resources/i18n/ApiError_he.properties
new file mode 100644
index 00000000..5ce597f4
--- /dev/null
+++ b/src/main/resources/i18n/ApiError_he.properties
@@ -0,0 +1,83 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# Keys are from api.ApiError enum
+
+# "localeLang": "he",
+
+### Common ###
+JSON = × ×›×©×œ ×‘× ×™×ª×•×— הודעת JSON
+
+INSUFFICIENT_BALANCE = יתרה ×œ× ×ž×¡×¤×§×ª
+
+UNAUTHORIZED = קרי×ת API ×œ× ×ž×•×¨×©×™×ª
+
+REPOSITORY_ISSUE = שגי×ת מ×גר
+
+NON_PRODUCTION = קרי×ת API זו ××™× ×” מותרת עבור מערכות ייצור
+
+BLOCKCHAIN_NEEDS_SYNC = הבלוקצ'יין צריך ×œ×”×¡×ª× ×›×¨×Ÿ תחילה
+
+NO_TIME_SYNC = עדיין ×ין ×¡× ×›×¨×•×Ÿ שעון
+
+### Validation ###
+INVALID_SIGNATURE = חתימה ×œ× ×—×•×§×™×ª
+
+INVALID_ADDRESS = כתובת ×œ× ×—×•×§×™×ª
+
+INVALID_PUBLIC_KEY = מפתח ציבורי ×œ× ×—×•×§×™
+
+INVALID_DATA = × ×ª×•× ×™× ×œ× ×—×•×§×™×™×
+
+INVALID_NETWORK_ADDRESS = כתובת רשת ×œ× ×—×•×§×™×ª
+
+ADDRESS_UNKNOWN = כתובת חשבון ×œ× ×™×“×•×¢×”
+
+INVALID_CRITERIA = ×§×¨×™×˜×¨×™×•× ×™ חיפוש ×œ× ×—×•×§×™×™×
+
+INVALID_REFERENCE = ×”×¤× ×™×” ×œ× ×—×•×§×™×ª
+
+TRANSFORMATION_ERROR = ×œ× ×”×¦×œ×™×— להפוך ×ת JSON לעסקה
+
+INVALID_PRIVATE_KEY = מפתח פרטי ×œ× ×—×•×§×™
+
+INVALID_HEIGHT = גובה בלוק ×œ× ×—×•×§×™
+
+CANNOT_MINT = החשבון ×œ× ×™×›×•×œ להטביע
+
+### Blocks ###
+BLOCK_UNKNOWN = בלוק ×œ× ×™×“×•×¢
+
+### Transactions ###
+TRANSACTION_UNKNOWN = עסקה ×œ× ×™×“×•×¢×”
+
+PUBLIC_KEY_NOT_FOUND = מפתח ציבורי ×œ× × ×ž×¦×
+
+# this one is special in that caller expected to pass two additional strings, hence the two %s
+TRANSACTION_INVALID = עסקה ×œ× ×—×•×§×™×ª: %s (%s)
+
+### Naming ###
+NAME_UNKNOWN = ×©× ×œ× ×™×“×•×¢
+
+### Asset ###
+INVALID_ASSET_ID = מזהה × ×›×¡ ×œ× ×—×•×§×™
+
+INVALID_ORDER_ID = מזהה ×”×–×ž× ×ª × ×›×¡ ×œ× ×—×•×§×™
+
+ORDER_UNKNOWN = מזהה ×”×–×ž× ×ª × ×›×¡ ×œ× ×™×“×•×¢
+
+### Groups ###
+GROUP_UNKNOWN = קבוצה ×œ× ×™×“×•×¢×”
+
+### Foreign Blockchain ###
+FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = בעיה זרה בלוקצ'יין ×ו ElectrumX ברשת
+
+FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = יתרה ×œ× ×ž×¡×¤×§×ª בבלוקצ'יין זר
+
+FOREIGN_BLOCKCHAIN_TOO_SOON = ×ž×•×§×“× ×ž×“×™ לשדר עסקת בלוקצ'יין זרה (זמן × ×¢×™×œ×”/זמן חסימה ×—×¦×™×•× ×™)
+
+### Trade Portal ###
+ORDER_SIZE_TOO_SMALL = כמות ×”×”×–×ž× ×” × ×ž×•×›×” מדי
+
+### Data ###
+FILE_NOT_FOUND = הקובץ ×œ× × ×ž×¦×
+
+NO_REPLY = עמית ×œ× ×”×©×™×‘ בזמן המותר
diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties
index c92130f1..c6815879 100644
--- a/src/main/resources/i18n/SysTray_de.properties
+++ b/src/main/resources/i18n/SysTray_de.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = Münzprägung inaktiv
MINTING_ENABLED = \u2714 Münzprägung aktiv
-OPEN_UI = Öffne Benutzeroberfläche
-
PERFORMING_DB_CHECKPOINT = Speichere unerfasste Datenbankänderungen...
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung wird durchgeführt...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit
SYNCHRONIZING_BLOCKCHAIN = Synchronisiere
SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert
+
+RESTARTING_NODE = Knoten wird neu gestartet
+
+APPLYING_RESTARTING_NODE = Neustart des knotens wird angewendet. Bitte haben Sie Geduld...
+
+BOOTSTRAP_NODE = Bootstrapping-Knoten
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap anwenden und Knoten neu starten. Bitte haben Sie Geduld...
diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties
index 39940be0..302cc8d3 100644
--- a/src/main/resources/i18n/SysTray_en.properties
+++ b/src/main/resources/i18n/SysTray_en.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = NOT minting
MINTING_ENABLED = \u2714 Minting
-OPEN_UI = Open UI
-
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronize clock
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
SYNCHRONIZING_CLOCK = Synchronizing clock
+
+RESTARTING_NODE = Restarting Node
+
+APPLYING_RESTARTING_NODE = Applying restarting node. Please be patient...
+
+BOOTSTRAP_NODE = Bootstrapping Node
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Applying bootstrap and restarting node. Please be patient...
diff --git a/src/main/resources/i18n/SysTray_es.properties b/src/main/resources/i18n/SysTray_es.properties
index 36cbb22c..8f2eec7d 100644
--- a/src/main/resources/i18n/SysTray_es.properties
+++ b/src/main/resources/i18n/SysTray_es.properties
@@ -33,8 +33,6 @@ MINTING_DISABLED = Acuñación NO habilitada
MINTING_ENABLED = \u2714 Acuñación habilitada
-OPEN_UI = IU abierta
-
PERFORMING_DB_CHECKPOINT = Guardando cambios de base de datos no confirmados...
PERFORMING_DB_MAINTENANCE = Realizando mantenimiento programado...
@@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Sincronizar reloj
SYNCHRONIZING_BLOCKCHAIN = Sincronizando
SYNCHRONIZING_CLOCK = Sincronizando reloj
+
+RESTARTING_NODE = Reiniciando el nodo
+
+APPLYING_RESTARTING_NODE = Aplicando el nodo de reinicio. Por favor sea paciente...
+
+BOOTSTRAP_NODE = Nodo de arranque
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Aplicando bootstrap y reiniciando el nodo. Por favor sea paciente...
diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties
index 4038d615..8c810880 100644
--- a/src/main/resources/i18n/SysTray_fi.properties
+++ b/src/main/resources/i18n/SysTray_fi.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = EI lyö rahaa
MINTING_ENABLED = \u2714 Lyö rahaa
-OPEN_UI = Avaa UI
-
PERFORMING_DB_CHECKPOINT = Tallentaa kommittoidut tietokantamuutokset...
PERFORMING_DB_MAINTENANCE = Suoritetaan määräaikaishuoltoa...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synkronisoi kello
SYNCHRONIZING_BLOCKCHAIN = Synkronisoi
SYNCHRONIZING_CLOCK = Synkronisoi kelloa
+
+RESTARTING_NODE = Käynnistetään uudelleen solmu
+
+APPLYING_RESTARTING_NODE = Käytetään uudelleenkäynnistyssolmua. Olkaa kärsivällisiä...
+
+BOOTSTRAP_NODE = Käynnistyssolmu
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Käynnistetään ja käynnistetään solmu uudelleen. Olkaa kärsivällisiä...
diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties
index 2e376842..2ab71c72 100644
--- a/src/main/resources/i18n/SysTray_fr.properties
+++ b/src/main/resources/i18n/SysTray_fr.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = NE mint PAS
MINTING_ENABLED = \u2714 Minting
-OPEN_UI = Ouvrir l'interface
-
PERFORMING_DB_CHECKPOINT = Enregistrement des modifications de base de données non validées...
PERFORMING_DB_MAINTENANCE = Entrain d'effectuer la maintenance programmée...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Mettre l'heure à jour
SYNCHRONIZING_BLOCKCHAIN = Synchronisation
SYNCHRONIZING_CLOCK = Synchronisation de l'heure
+
+RESTARTING_NODE = Redémarrage du nœud
+
+APPLYING_RESTARTING_NODE = Application du redémarrage du nœud. S'il vous plaît, soyez patient...
+
+BOOTSTRAP_NODE = Nœud d'amorçage
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Application du bootstrap et redémarrage du nœud. S'il vous plaît, soyez patient...
diff --git a/src/main/resources/i18n/SysTray_he.properties b/src/main/resources/i18n/SysTray_he.properties
new file mode 100644
index 00000000..6de7b7cc
--- /dev/null
+++ b/src/main/resources/i18n/SysTray_he.properties
@@ -0,0 +1,54 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# SysTray pop-up menu
+
+APPLYING_UPDATE_AND_RESTARTING = מחיל עדכון ×וטומטי ומפעיל מחדש...
+
+AUTO_UPDATE = עדכון ×וטומטי
+
+BLOCK_HEIGHT = גובה
+
+BLOCKS_REMAINING = × ×•×ª×¨×• בלוקי×
+
+BUILD_VERSION = גרסת ×‘× ×™×™×”
+
+CHECK_TIME_ACCURACY = בדוק ×ת דיוק הזמן
+
+CONNECTING = מתחבר
+
+CONNECTION = חיבור
+
+CONNECTIONS = חיבורי×
+
+CREATING_BACKUP_OF_DB_FILES = יוצר גיבוי של קבצי מסד × ×ª×•× ×™×...
+
+DB_BACKUP = גיבוי מסד × ×ª×•× ×™×
+
+DB_CHECKPOINT = × ×§×•×“×ª ביקורת של מסד × ×ª×•× ×™×
+
+DB_MAINTENANCE = תחזוקת מסד × ×ª×•× ×™×
+
+EXIT = יצי××”
+
+LITE_NODE = Lite Node
+
+MINTING_DISABLED = כרייה מבוטלת
+
+MINTING_ENABLED = \u2714 הטבעה
+
+PERFORMING_DB_CHECKPOINT = שומר ×©×™× ×•×™×™× ×œ× ×ž×—×•×™×‘×™× ×‘×ž×¡×“ ×”× ×ª×•× ×™×...
+
+PERFORMING_DB_MAINTENANCE = מבצע תחזוקה ×ž×ª×•×–×ž× ×ª...
+
+SYNCHRONIZE_CLOCK = ×¡× ×›×¨×Ÿ שעון
+
+SYNCHRONIZING_BLOCKCHAIN ​​= ×ž×¡× ×›×¨×Ÿ
+
+SYNCHRONIZING_CLOCK = ×ž×¡× ×›×¨×Ÿ שעון
+
+RESTARTING_NODE = הפעלה מחדש של צומת
+
+APPLYING_RESTARTING_NODE = החלת צומת הפעלה מחדש. ×× × ×”×ª×זר ×‘×¡×‘×œ× ×•×ª...
+
+BOOTSTRAP_NODE = צומת ×תחול
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = החלת ×תחול והפעלת צומת
diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties
index 74ab21ac..da3a7209 100644
--- a/src/main/resources/i18n/SysTray_hu.properties
+++ b/src/main/resources/i18n/SysTray_hu.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban
MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban
-OPEN_UI = Felhasználói felület megnyitása
-
PERFORMING_DB_CHECKPOINT = Mentetlen adatbázis-módosÃtások mentése...
PERFORMING_DB_MAINTENANCE = Ütemezett karbantartás...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Óra-szinkronizálás megkezdése
SYNCHRONIZING_BLOCKCHAIN = Szinkronizálás
SYNCHRONIZING_CLOCK = Óraszinkronizálás folyamatban
+
+RESTARTING_NODE = Csomópont újraindÃtása
+
+APPLYING_RESTARTING_NODE = ÚjraindÃtó csomópont alkalmazása. Kérjük várjon...
+
+BOOTSTRAP_NODE = RendszerindÃtási csomópont
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap alkalmazása és csomópont újraindÃtása. Kérjük várjon...
diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties
index d966d825..c35feebc 100644
--- a/src/main/resources/i18n/SysTray_it.properties
+++ b/src/main/resources/i18n/SysTray_it.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = Conio disabilitato
MINTING_ENABLED = \u2714 Conio abilitato
-OPEN_UI = Apri UI
-
PERFORMING_DB_CHECKPOINT = Salvataggio delle modifiche del database non salvate...
PERFORMING_DB_MAINTENANCE = Manutenzione programmata dell'efficienza...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Sincronizzare l'orologio
SYNCHRONIZING_BLOCKCHAIN = Sincronizzazione della blockchain
SYNCHRONIZING_CLOCK = Sincronizzazione orologio
+
+RESTARTING_NODE = Riavvio del nodo
+
+APPLYING_RESTARTING_NODE = Applicazione del nodo di riavvio. Per favore sii paziente...
+
+BOOTSTRAP_NODE = Nodo di bootstrap
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Applicazione del bootstrap e riavvio del nodo. Per favore sii paziente...
diff --git a/src/main/resources/i18n/SysTray_jp.properties b/src/main/resources/i18n/SysTray_jp.properties
index c4cccb5b..d3cf13ad 100644
--- a/src/main/resources/i18n/SysTray_jp.properties
+++ b/src/main/resources/i18n/SysTray_jp.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = ミント一時ä¸æ¢ä¸
MINTING_ENABLED = \u2714 ミント
-OPEN_UI = UIã‚’é–‹ã
-
PERFORMING_DB_CHECKPOINT = コミットã•ã‚Œã¦ã„ãªã„データベースã®å¤‰æ›´ã‚’ä¿å˜ä¸...
PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行ä¸...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 時刻をåŒæœŸ
SYNCHRONIZING_BLOCKCHAIN = ブãƒãƒƒã‚¯ãƒã‚§ãƒ¼ãƒ³ã‚’åŒæœŸä¸
SYNCHRONIZING_CLOCK = 時刻をåŒæœŸä¸
+
+RESTARTING_NODE = ノードをå†èµ·å‹•ã—ã¦ã„ã¾ã™
+
+APPLYING_RESTARTING_NODE = å†èµ·å‹•ãƒŽãƒ¼ãƒ‰ã‚’é©ç”¨ã—ã¦ã„ã¾ã™ã€‚ ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„...
+
+BOOTSTRAP_NODE = ブートストラップ ノード
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = ブートストラップをé©ç”¨ã—ã€ãƒŽãƒ¼ãƒ‰ã‚’å†èµ·å‹•ã—ã¾ã™ã€‚ ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„...
diff --git a/src/main/resources/i18n/SysTray_ko.properties b/src/main/resources/i18n/SysTray_ko.properties
index dc6cb69b..5e165da3 100644
--- a/src/main/resources/i18n/SysTray_ko.properties
+++ b/src/main/resources/i18n/SysTray_ko.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = ë¯¼íŒ…ì¤‘ì´ ì•„ë‹˜
MINTING_ENABLED = \u2714 민팅
-OPEN_UI = UI 열기
-
PERFORMING_DB_CHECKPOINT = 커밋ë˜ì§€ ì•Šì€ ë°ì´í„°ë² ì´ìŠ¤ 변경 ë‚´ìš©ì„ ì €ìž¥í•˜ëŠ” 중...
PERFORMING_DB_MAINTENANCE = ì˜ˆì•½ëœ ìœ ì§€ 관리 수행 중...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 시간 ë™ê¸°í™”
SYNCHRONIZING_BLOCKCHAIN = ë™ê¸°í™”중
SYNCHRONIZING_CLOCK = 시간 ë™ê¸°í™”
+
+RESTARTING_NODE = 노드 다시 시작 중
+
+APPLYING_RESTARTING_NODE = 노드 ìž¬ì‹œìž‘ì„ ì 용합니다. ê¸°ë‹¤ë ¤ì£¼ì‹ì‹œì˜¤...
+
+BOOTSTRAP_NODE = 부트스트래핑 노드
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = ë¶€íŠ¸ìŠ¤íŠ¸ëž©ì„ ì ìš©í•˜ê³ ë…¸ë“œë¥¼ 다시 시작합니다. ê¸°ë‹¤ë ¤ì£¼ì‹ì‹œì˜¤...
diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties
index 3d7de024..f42d3e96 100644
--- a/src/main/resources/i18n/SysTray_nl.properties
+++ b/src/main/resources/i18n/SysTray_nl.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = Minten is uitgeschakeld
MINTING_ENABLED = \u2714 Minten is actief
-OPEN_UI = Open UI
-
PERFORMING_DB_CHECKPOINT = De database wordt bijgewerkt...
PERFORMING_DB_MAINTENANCE = Bezig met gepland onderhoud...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronizeer klok
SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren
SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd
+
+RESTARTING_NODE = Knooppunt opnieuw starten
+
+APPLYING_RESTARTING_NODE = Herstartknooppunt toepassen. Wees alstublieft geduldig...
+
+BOOTSTRAP_NODE = Opstartknooppunt
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en knooppunt opnieuw starten. Wees alstublieft geduldig...
diff --git a/src/main/resources/i18n/SysTray_pl.properties b/src/main/resources/i18n/SysTray_pl.properties
index 84740da0..12e49bda 100644
--- a/src/main/resources/i18n/SysTray_pl.properties
+++ b/src/main/resources/i18n/SysTray_pl.properties
@@ -33,8 +33,6 @@ MINTING_DISABLED = Mennica zamknięta
MINTING_ENABLED = \u2714 Mennica aktywna
-OPEN_UI = Otwórz interfejs użytkownika
-
PERFORMING_DB_CHECKPOINT = Zapisywanie niezaksięgowanych zmian w bazie danych...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
@@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synchronizuj zegar
SYNCHRONIZING_BLOCKCHAIN = Synchronizacja
SYNCHRONIZING_CLOCK = Synchronizacja zegara
+
+RESTARTING_NODE = Ponowne uruchamianie węzła
+
+APPLYING_RESTARTING_NODE = Stosuję ponowne uruchomienie węzła. Proszę być cierpliwym...
+
+BOOTSTRAP_NODE = Węzeł ładowania początkowego
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Stosowanie ładowania początkowego i ponowne uruchamianie węzła. Proszę być cierpliwym...
diff --git a/src/main/resources/i18n/SysTray_ro.properties b/src/main/resources/i18n/SysTray_ro.properties
index 4130bbcb..7e87af8d 100644
--- a/src/main/resources/i18n/SysTray_ro.properties
+++ b/src/main/resources/i18n/SysTray_ro.properties
@@ -35,14 +35,20 @@ MINTING_DISABLED = nu produce moneda
MINTING_ENABLED = \u2714 Minting
-OPEN_UI = Deschidere interfata utilizator IU
-
PERFORMING_DB_CHECKPOINT = Salvarea modificarilor nerealizate ale bazei de date...
-PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate…
+PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate�
SYNCHRONIZE_CLOCK = Sincronizare ceas
SYNCHRONIZING_BLOCKCHAIN = Sincronizare
SYNCHRONIZING_CLOCK = Se sincronizeaza ceasul
+
+RESTARTING_NODE = Repornirea nodului
+
+APPLYING_RESTARTING_NODE = Se aplica nodul de repornire. Te rog fii rabdator...
+
+BOOTSTRAP_NODE = Nod de bootstrap
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Se aplica bootstrap si se reporneste nodul. Te rog fii rabdator...
diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties
index c8615f73..8c2b50eb 100644
--- a/src/main/resources/i18n/SysTray_ru.properties
+++ b/src/main/resources/i18n/SysTray_ru.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = Чеканка отключена
MINTING_ENABLED = \u2714 Чеканка активна
-OPEN_UI = Открыть пользовательÑкий интерфейÑ
-
PERFORMING_DB_CHECKPOINT = Сохранение незафикÑированных изменений базы данных...
PERFORMING_DB_MAINTENANCE = Выполнение планового техничеÑкого обÑлуживаниÑ...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Синхронизировать времÑ
SYNCHRONIZING_BLOCKCHAIN = Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ñ†ÐµÐ¿Ð¸
SYNCHRONIZING_CLOCK = Проверка времени
+
+RESTARTING_NODE = ПерезапуÑк узла
+
+APPLYING_RESTARTING_NODE = Применение перезапуÑка узла. ПожалуйÑта, будьте терпеливы...
+
+BOOTSTRAP_NODE = Узел начальной загрузки
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Применение начальной загрузки и перезапуÑк узла. ПожалуйÑта, будьте терпеливы...
diff --git a/src/main/resources/i18n/SysTray_sv.properties b/src/main/resources/i18n/SysTray_sv.properties
index 96f291b5..8fcd3193 100644
--- a/src/main/resources/i18n/SysTray_sv.properties
+++ b/src/main/resources/i18n/SysTray_sv.properties
@@ -33,8 +33,6 @@ MINTING_DISABLED = Präglar INTE
MINTING_ENABLED = \u2714 Präglar
-OPEN_UI = Öppna UI
-
PERFORMING_DB_CHECKPOINT = Sparar oengagerade databasändringar...
PERFORMING_DB_MAINTENANCE = Utför schemalagt underhåll...
@@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synkronisera klockan
SYNCHRONIZING_BLOCKCHAIN = Synkroniserar
SYNCHRONIZING_CLOCK = Synkroniserar klockan
+
+RESTARTING_NODE = Mimitian deui Node
+
+APPLYING_RESTARTING_NODE = Nerapkeun titik ngamimitian deui. Mangga sing sabar...
+
+BOOTSTRAP_NODE = Bootstrapping Node
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = Nerapkeun bootstrap sareng hurungkeun deui titik. Mangga sing sabar...
diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties
index d6848a7c..4a4e7ce6 100644
--- a/src/main/resources/i18n/SysTray_zh_CN.properties
+++ b/src/main/resources/i18n/SysTray_zh_CN.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = 没有铸å¸
MINTING_ENABLED = \u2714 铸å¸
-OPEN_UI = å¼€å¯Qortalç•Œé¢
-
PERFORMING_DB_CHECKPOINT = æ£åœ¨ä¿å˜æœªæ交的数æ®åº“修订...
PERFORMING_DB_MAINTENANCE = æ£åœ¨æ‰§è¡Œå®šæœŸæ•°æ®åº“维护...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = åŒæ¥æ—¶é’Ÿ
SYNCHRONIZING_BLOCKCHAIN = æ£åœ¨åŒæ¥åŒºå—链
SYNCHRONIZING_CLOCK = æ£åœ¨åŒæ¥æ—¶é’Ÿ
+
+RESTARTING_NODE = é‡æ–°å¯åŠ¨èŠ‚点
+
+APPLYING_RESTARTING_NODE = 应用é‡æ–°å¯åŠ¨èŠ‚点。 请è€å¿ƒç‰å¾…...
+
+BOOTSTRAP_NODE = 引导节点
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = 应用引导程åºå¹¶é‡æ–°å¯åŠ¨èŠ‚点。 请è€å¿ƒç‰å¾…...
diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties
index eabdbb63..5dc07fe2 100644
--- a/src/main/resources/i18n/SysTray_zh_TW.properties
+++ b/src/main/resources/i18n/SysTray_zh_TW.properties
@@ -35,8 +35,6 @@ MINTING_DISABLED = 沒有鑄幣
MINTING_ENABLED = \u2714 é‘„å¹£
-OPEN_UI = é–‹å•“Qortalç•Œé¢
-
PERFORMING_DB_CHECKPOINT = æ£åœ¨ä¿å˜æœªæ交的數據庫修訂...
PERFORMING_DB_MAINTENANCE = æ£åœ¨åŸ·è¡Œæ•¸æ“šåº«å®šæœŸç¶è·...
@@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = åŒæ¥æ™‚é˜
SYNCHRONIZING_BLOCKCHAIN = æ£åœ¨åŒæ¥å€å¡Šéˆ
SYNCHRONIZING_CLOCK = æ£åœ¨åŒæ¥æ™‚é˜
+
+RESTARTING_NODE = é‡æ–°å•Ÿå‹•ç¯€é»ž
+
+APPLYING_RESTARTING_NODE = 應用é‡æ–°å•Ÿå‹•ç¯€é»žã€‚ è«‹è€å¿ƒç‰å¾…...
+
+BOOTSTRAP_NODE = 引導節點
+
+APPLYING_BOOTSTRAP_AND_RESTARTING = 應用引導程å¼ä¸¦é‡æ–°å•Ÿå‹•ç¯€é»žã€‚ è«‹è€å¿ƒç‰å¾…...
diff --git a/src/main/resources/i18n/TransactionValidity_de.properties b/src/main/resources/i18n/TransactionValidity_de.properties
index eab7fb9e..a2019670 100644
--- a/src/main/resources/i18n/TransactionValidity_de.properties
+++ b/src/main/resources/i18n/TransactionValidity_de.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = Transaktion existiert bereits
TRANSACTION_UNKNOWN = Transaktion unbekannt
TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein
+
+TRANSFER_PRIVS_DISABLED = Ãœbertragungsberechtigungen deaktiviert
+
+TEMPORARY_DISABLED = Namensregistrierung vorübergehend deaktiviert
diff --git a/src/main/resources/i18n/TransactionValidity_en.properties b/src/main/resources/i18n/TransactionValidity_en.properties
index 3f33771d..a93db3da 100644
--- a/src/main/resources/i18n/TransactionValidity_en.properties
+++ b/src/main/resources/i18n/TransactionValidity_en.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaction already exists
TRANSACTION_UNKNOWN = transaction unknown
TX_GROUP_ID_MISMATCH = transaction's group ID does not match
+
+TRANSFER_PRIVS_DISABLED = transfer privileges disabled
+
+TEMPORARY_DISABLED = Name registration temporary disabled
diff --git a/src/main/resources/i18n/TransactionValidity_es.properties b/src/main/resources/i18n/TransactionValidity_es.properties
index 7c357009..8ac7ccf4 100644
--- a/src/main/resources/i18n/TransactionValidity_es.properties
+++ b/src/main/resources/i18n/TransactionValidity_es.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transacción ya existe
TRANSACTION_UNKNOWN = transacción desconocida
TX_GROUP_ID_MISMATCH = el ID de grupo de la transacción no coincide
+
+TRANSFER_PRIVS_DISABLED = privilegios de transferencia deshabilitados
+
+TEMPORARY_DISABLED = Registro de nombre temporalmente deshabilitado
diff --git a/src/main/resources/i18n/TransactionValidity_fi.properties b/src/main/resources/i18n/TransactionValidity_fi.properties
index ec658bc1..a7bc9c0a 100644
--- a/src/main/resources/i18n/TransactionValidity_fi.properties
+++ b/src/main/resources/i18n/TransactionValidity_fi.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktio on jo olemassa
TRANSACTION_UNKNOWN = tuntematon transaktio
TX_GROUP_ID_MISMATCH = transaktion ryhmä-ID:n vastaavuusvirhe
+
+TRANSFER_PRIVS_DISABLED = siirtooikeudet poistettu käytöstä
+
+TEMPORARY_DISABLED = Nimen rekisteröinti tilapäisesti poistettu käytöstä
diff --git a/src/main/resources/i18n/TransactionValidity_fr.properties b/src/main/resources/i18n/TransactionValidity_fr.properties
index e030bc0d..55ae9082 100644
--- a/src/main/resources/i18n/TransactionValidity_fr.properties
+++ b/src/main/resources/i18n/TransactionValidity_fr.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transaction existe déjÃ
TRANSACTION_UNKNOWN = transaction inconnue
TX_GROUP_ID_MISMATCH = l'identifiant du groupe de transaction ne correspond pas
+
+TRANSFER_PRIVS_DISABLED = privilèges de transfert désactivés
+
+TEMPORARY_DISABLED = Enregistrement du nom temporairement désactivé
diff --git a/src/main/resources/i18n/TransactionValidity_he.properties b/src/main/resources/i18n/TransactionValidity_he.properties
new file mode 100644
index 00000000..2f9338f0
--- /dev/null
+++ b/src/main/resources/i18n/TransactionValidity_he.properties
@@ -0,0 +1,199 @@
+#
+
+ACCOUNT_ALREADY_EXISTS = חשבון כבר קיי×
+
+ACCOUNT_CANNOT_REWARD_SHARE = ​​חשבון ×œ× ×™×›×•×œ לחלוק תגמולי×
+
+ADDRESS_ABOVE_RATE_LIMIT = הכתובת ×”×’×™×¢×” למגבלת התעריף ×©×¦×•×™× ×”
+
+ADDRESS_BLOCKED = כתובת זו חסומה
+
+ALREADY_GROUP_ADMIN = כבר ×ž× ×”×œ קבוצה
+
+ALREADY_GROUP_MEMBER = כבר חבר בקבוצה
+
+ALREADY_VOTED_FOR_THAT_OPTION = כבר הצביע עבור ×פשרות זו
+
+ASSET_ALREADY_EXISTS = ×”× ×›×¡ כבר קיי×
+
+ASSET_DOES_NOT_EXIST = ×”× ×›×¡ ××™× ×• קיי×
+
+ASSET_DOES_NOT_MATCH_AT = ×”× ×›×¡ ××™× ×• תו×× ×œ× ×›×¡ של AT
+
+ASSET_NOT_SPENDABLE = ×”× ×›×¡ ××™× ×• × ×™×ª×Ÿ לבזבז
+
+AT_ALREADY_EXISTS = AT כבר קיי×
+
+AT_IS_FINISHED = AT הסתיי×
+
+AT_UNKNOWN = AT ×œ× ×™×“×•×¢
+
+BAN_EXISTS = החסימה כבר קיימת
+
+BAN_UNKNOWN = ×יסור ×œ× ×™×“×•×¢
+
+BANNED_FROM_GROUP = ×—×¡×•× ×ž×”×§×‘×•×¦×”
+
+BUYER_ALREADY_OWNER = ×”×§×•× ×” כבר הבעלי×
+
+CLOCK_NOT_SYNCED = שעון ×œ× ×ž×¡×•× ×›×¨×Ÿ
+
+DUPLICATE_MESSAGE = כתובת ×©× ×©×œ×—×” הודעה כפולה
+
+DUPLICATE_OPTION = ×פשרות שכפול
+
+GROUP_ALREADY_EXISTS = הקבוצה כבר קיימת
+
+GROUP_APPROVAL_DECIDED = ×ישור הקבוצה כבר הוחלט
+
+GROUP_APPROVAL_NOT_REQUIRED = ×ין צורך ב×ישור קבוצתי
+
+GROUP_DOES_NOT_EXIST = קבוצה ×œ× ×§×™×™×ž×ª
+
+GROUP_ID_MISMATCH = ××™ הת×מה של מזהה הקבוצה
+
+GROUP_OWNER_CANNOT_LEAVE = בעל הקבוצה ×œ× ×™×›×•×œ לעזוב ×ת הקבוצה
+
+HAVE_EQUALS_WANT = have-asset זהה ל-want-asset
+
+INCORRECT_NONCE = ×œ× ×ª×§×™×Ÿ של PoW
+
+INSUFFICIENT_FEE = עמלה ×œ× ×ž×¡×¤×§×ª
+
+INVALID_ADDRESS = כתובת ×œ× ×—×•×§×™×ª
+
+INVALID_AMOUNT = ×¡×›×•× ×œ× ×—×•×§×™
+
+INVALID_ASSET_OWNER = בעל × ×›×¡ ×œ× ×—×•×§×™
+
+INVALID_AT_TRANSACTION = עסקת AT ×œ× ×—×•×§×™×ª
+
+INVALID_AT_TYPE_LENGTH = ×ורך AT 'סוג' ×œ× ×—×•×§×™
+
+INVALID_BUT_OK = ×œ× ×—×•×§×™ ×בל בסדר
+
+INVALID_CREATION_BYTES = ×‘×ª×™× ×œ× ×—×•×§×™×™× ×©×œ יצירה
+
+INVALID_DATA_LENGTH = ×ורך × ×ª×•× ×™× ×œ× ×—×•×§×™
+
+INVALID_DESCRIPTION_LENGTH = ×ורך תי×ור ×œ× ×—×•×§×™
+
+INVALID_GROUP_APPROVAL_THRESHOLD = סף ×œ× ×—×•×§×™ ל×ישור קבוצה
+
+INVALID_GROUP_BLOCK_DELAY = עיכוב חסימת ×ישור קבוצה ×œ× ×—×•×§×™
+
+INVALID_GROUP_ID = מזהה קבוצה ×œ× ×—×•×§×™
+
+INVALID_GROUP_OWNER = בעל קבוצה ×œ× ×—×•×§×™
+
+INVALID_LIFETIME = משך ×—×™×™× ×œ× ×—×•×§×™
+
+INVALID_NAME_LENGTH = ×ורך ×©× ×œ× ×—×•×§×™
+
+INVALID_NAME_OWNER = בעל ×©× ×œ× ×—×•×§×™
+
+INVALID_OPTION_LENGTH = ×ורך ×פשרויות ×œ× ×—×•×§×™
+
+INVALID_OPTIONS_COUNT = ספירת ×פשרויות ×œ× ×—×•×§×™×•×ª
+
+INVALID_ORDER_CREATOR = יוצר ×”×–×ž× ×” ×œ× ×—×•×§×™
+
+INVALID_PAYMENTS_COUNT = ספירת ×ª×©×œ×•×ž×™× ×œ× ×—×•×§×™×™×
+
+INVALID_PUBLIC_KEY = מפתח ציבורי ×œ× ×—×•×§×™
+
+INVALID_QUANTITY = כמות ×œ× ×—×•×§×™×ª
+
+INVALID_REFERENCE = ×”×¤× ×™×” ×œ× ×—×•×§×™×ª
+
+INVALID_RETURN = החזרה ×œ× ×—×•×§×™×ª
+
+INVALID_REWARD_SHARE_PERCENT = ×חוז חלוקת ×ª×’×ž×•×œ×™× ×œ× ×—×•×§×™
+
+INVALID_SELLER = מוכר ×œ× ×—×•×§×™
+
+INVALID_TAGS_LENGTH = ×ורך 'תגי×' ×œ× ×—×•×§×™
+
+INVALID_TIMESTAMP_SIGNATURE = חתימת חותמת זמן ×œ× ×—×•×§×™×ª
+
+INVALID_TX_GROUP_ID = מזהה קבוצת עסק×ות ×œ× ×—×•×§×™
+
+INVALID_VALUE_LENGTH = ×ורך 'ערך' ×œ× ×—×•×§×™
+
+INVITE_UNKNOWN = ×”×–×ž× ×” לקבוצה ×œ× ×™×“×•×¢×”
+
+JOIN_REQUEST_EXISTS = בקשת הצטרפות לקבוצה כבר קיימת
+
+MAXIMUM_REWARD_SHARES = כבר במספר המרבי של שיתופי תגמול עבור חשבון זה
+
+MISSING_CREATOR = חסר יוצר
+
+MULTIPLE_NAMES_FORBIDDEN = ×סור להשתמש במספר שמות ×¨×©×•×ž×™× ×œ×›×œ חשבון
+
+NAME_ALREADY_FOR_SALE = ×©× ×›×‘×¨ למכירה
+
+NAME_ALREADY_REGISTERED = ×”×©× ×›×‘×¨ רשו×
+
+NAME_BLOCKED = ×”×©× ×”×–×” חסו×
+
+NAME_DOES_NOT_EXIST = ×©× ×œ× ×§×™×™×
+
+NAME_NOT_FOR_SALE = ×”×©× ××™× ×• למכירה
+
+NAME_NOT_NORMALIZED = ×©× ×œ× ×‘×¦×•×¨×ª Unicode '×ž× ×•×¨×ž×œ×ª'
+
+NEGATIVE_AMOUNT = ×¡×›×•× ×œ× ×—×•×§×™/שלילי
+
+NEGATIVE_FEE = עמלה ×œ× ×—×•×§×™×ª/שלילית
+
+NEGATIVE_PRICE = מחיר ×œ× ×—×•×§×™/שלילי
+
+NO_BALANCE = ×יזון ×œ× ×ž×¡×¤×™×§
+
+NO_BLOCKCHAIN_LOCK = הבלוקצ'יין של הצומת תפוס כעת
+
+NO_FLAG_PERMISSION = לחשבון ×ין הרש××” זו
+
+NOT_GROUP_ADMIN = החשבון ××™× ×• ×ž× ×”×œ קבוצה
+
+NOT_GROUP_MEMBER = החשבון ××™× ×• חבר בקבוצה
+
+NOT_MINTING_ACCOUNT = החשבון ××™× ×• יכול להטביע
+
+NOT_YET_RELEASED = ×ª×›×•× ×” עדיין ×œ× ×©×•×—×¨×¨×”
+
+OK = בסדר
+
+ORDER_ALREADY_CLOSED = ×”×–×ž× ×ª סחר ×‘× ×›×¡ כבר סגורה
+
+ORDER_DOES_NOT_EXIST = הור×ת סחר ×‘× ×›×¡ ×œ× ×§×™×™×ž×ª
+
+POLL_ALREADY_EXISTS = סקר כבר קיי×
+
+POLL_DOES_NOT_EXIST = סקר ××™× ×• קיי×
+
+POLL_OPTION_DOES_NOT_EXIST = ×פשרות סקר ×œ× ×§×™×™×ž×ª
+
+PUBLIC_KEY_UNKNOWN = מפתח ציבורי ×œ× ×™×“×•×¢
+
+REWARD_SHARE_UNKNOWN = חלוקת פרס ×œ× ×™×“×•×¢
+
+SELF_SHARE_EXISTS = שיתוף עצמי (שיתוף תגמול) כבר קיי×
+
+TIMESTAMP_TOO_NEW = חותמת זמן חדשה מדי
+
+TIMESTAMP_TOO_OLD = חותמת זמן ×™×©× ×” מדי
+
+TOO_MANY_UNCONFIRMED = בחשבון יש יותר מדי עסק×ות ×œ× ×ž×ושרות ×‘×”×ž×ª× ×”
+
+TRANSACTION_ALREADY_CONFIRMED = העסקה כבר ×ושרה
+
+TRANSACTION_ALREADY_EXISTS = עסקה כבר קיימת
+
+TRANSACTION_UNKNOWN = עסקה ×œ× ×™×“×•×¢×”
+
+TX_GROUP_ID_MISMATCH = מזהה הקבוצה של העסקה ××™× ×• תו××
+
+TRANSFER_PRIVS_DISABLED = הרש×ות העברה מושבתות
+
+TEMPORARY_DISABLED = ×¨×™×©×•× ×©×ž×•×ª מושבת ×–×ž× ×™×ª
diff --git a/src/main/resources/i18n/TransactionValidity_hu.properties b/src/main/resources/i18n/TransactionValidity_hu.properties
index 1b2558bb..2dbd9fd0 100644
--- a/src/main/resources/i18n/TransactionValidity_hu.properties
+++ b/src/main/resources/i18n/TransactionValidity_hu.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = ez a tranzakció már létezik
TRANSACTION_UNKNOWN = ismeretlen tranzakció
TX_GROUP_ID_MISMATCH = a tranzakció csoportazonosÃtója nem egyezik
+
+TRANSFER_PRIVS_DISABLED = átviteli jogosultságok letiltva
+
+TEMPORARY_DISABLED = A névregisztráció ideiglenesen le van tiltva
diff --git a/src/main/resources/i18n/TransactionValidity_it.properties b/src/main/resources/i18n/TransactionValidity_it.properties
index 390f914a..a520d5ae 100644
--- a/src/main/resources/i18n/TransactionValidity_it.properties
+++ b/src/main/resources/i18n/TransactionValidity_it.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transazione già esiste
TRANSACTION_UNKNOWN = transazione sconosciuta
TX_GROUP_ID_MISMATCH = identificazione di gruppo della transazione non corrisponde
+
+TRANSFER_PRIVS_DISABLED = privilegi di trasferimento disabilitati
+
+TEMPORARY_DISABLED = Registrazione del nome temporaneamente disabilitata
diff --git a/src/main/resources/i18n/TransactionValidity_jp.properties b/src/main/resources/i18n/TransactionValidity_jp.properties
index 9540372a..3827635c 100644
--- a/src/main/resources/i18n/TransactionValidity_jp.properties
+++ b/src/main/resources/i18n/TransactionValidity_jp.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = æ—¢ã«ãƒˆãƒ©ãƒ³ã‚¶ã‚¯ã‚·ãƒ§ãƒ³ã¯å˜åœ¨ã—ã¾ã™
TRANSACTION_UNKNOWN = ä¸æ˜Žãªãƒˆãƒ©ãƒ³ã‚¶ã‚¯ã‚·ãƒ§ãƒ³
TX_GROUP_ID_MISMATCH = トランザクションã®ã‚°ãƒ«ãƒ¼ãƒ—IDãŒä¸€è‡´ã—ã¾ã›ã‚“
+
+TRANSFER_PRIVS_DISABLED = 転é€æ¨©é™ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™
+
+TEMPORARY_DISABLED = åå‰ã®ç™»éŒ²ãŒä¸€æ™‚çš„ã«ç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™
diff --git a/src/main/resources/i18n/TransactionValidity_ko.properties b/src/main/resources/i18n/TransactionValidity_ko.properties
index a12b33f6..2667471c 100644
--- a/src/main/resources/i18n/TransactionValidity_ko.properties
+++ b/src/main/resources/i18n/TransactionValidity_ko.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 거래가 ì´ë¯¸ 존재합니다
TRANSACTION_UNKNOWN = 알 수 없는 거래
TX_GROUP_ID_MISMATCH = 트랜ìžì…˜ì˜ 그룹 IDê°€ ì¼ì¹˜í•˜ì§€ 않습니다
+
+TRANSFER_PRIVS_DISABLED = 권한 ì´ì „ì´ ë¹„í™œì„±í™”ë˜ì—ˆìŠµë‹ˆë‹¤.
+
+TEMPORARY_DISABLED = ì´ë¦„ 등ë¡ì´ ì¼ì‹œì 으로 비활성화ë˜ì—ˆìŠµë‹ˆë‹¤.
diff --git a/src/main/resources/i18n/TransactionValidity_nl.properties b/src/main/resources/i18n/TransactionValidity_nl.properties
index f92adf72..4f0dd8b7 100644
--- a/src/main/resources/i18n/TransactionValidity_nl.properties
+++ b/src/main/resources/i18n/TransactionValidity_nl.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transactie bestaat reeds
TRANSACTION_UNKNOWN = transactie onbekend
TX_GROUP_ID_MISMATCH = groep-ID komt niet overeen
+
+TRANSFER_PRIVS_DISABLED = overdrachtsrechten uitgeschakeld
+
+TEMPORARY_DISABLED = Naamregistratie tijdelijk uitgeschakeld
diff --git a/src/main/resources/i18n/TransactionValidity_pl.properties b/src/main/resources/i18n/TransactionValidity_pl.properties
index bcdceb6e..50944674 100644
--- a/src/main/resources/i18n/TransactionValidity_pl.properties
+++ b/src/main/resources/i18n/TransactionValidity_pl.properties
@@ -194,3 +194,6 @@ TRANSACTION_UNKNOWN = transakcja nieznana
TX_GROUP_ID_MISMATCH = niezgodność ID grupy transakcji
+TRANSFER_PRIVS_DISABLED = uprawnienia do przenoszenia wyłączone
+
+TEMPORARY_DISABLED = Rejestracja nazwy tymczasowo wyłączona
diff --git a/src/main/resources/i18n/TransactionValidity_ro.properties b/src/main/resources/i18n/TransactionValidity_ro.properties
index 6c67f31b..4c149a62 100644
--- a/src/main/resources/i18n/TransactionValidity_ro.properties
+++ b/src/main/resources/i18n/TransactionValidity_ro.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = tranzactia exista deja
TRANSACTION_UNKNOWN = tranzactie necunoscuta
TX_GROUP_ID_MISMATCH = ID-ul de grup al tranzactiei nu se potriveste
+
+TRANSFER_PRIVS_DISABLED = privilegii de transfer dezactivate
+
+TEMPORARY_DISABLED = Înregistrarea numelui a fost temporar dezactivată
diff --git a/src/main/resources/i18n/TransactionValidity_ru.properties b/src/main/resources/i18n/TransactionValidity_ru.properties
index 2818374a..79307d7d 100644
--- a/src/main/resources/i18n/TransactionValidity_ru.properties
+++ b/src/main/resources/i18n/TransactionValidity_ru.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = Ñ‚Ñ€Ð°Ð½Ð·Ð°ÐºÑ†Ð¸Ñ ÑущеÑтвует
TRANSACTION_UNKNOWN = неизвеÑÑ‚Ð½Ð°Ñ Ñ‚Ñ€Ð°Ð½Ð·Ð°ÐºÑ†Ð¸Ñ
TX_GROUP_ID_MISMATCH = не ÑоответÑтвие идентификатора группы в Ñ…Ñш транзации
+
+TRANSFER_PRIVS_DISABLED = права на передачу отключены
+
+TEMPORARY_DISABLED = РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ временно отключена
diff --git a/src/main/resources/i18n/TransactionValidity_sv.properties b/src/main/resources/i18n/TransactionValidity_sv.properties
index a3fec831..d4688310 100644
--- a/src/main/resources/i18n/TransactionValidity_sv.properties
+++ b/src/main/resources/i18n/TransactionValidity_sv.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktionen finns redan
TRANSACTION_UNKNOWN = okänd transaktion
TX_GROUP_ID_MISMATCH = transaktionens grupp-ID matchar inte
+
+TRANSFER_PRIVS_DISABLED = överföringsprivilegier inaktiverade
+
+TEMPORARY_DISABLED = Namnregistrering tillfälligt inaktiverad
diff --git a/src/main/resources/i18n/TransactionValidity_zh_CN.properties b/src/main/resources/i18n/TransactionValidity_zh_CN.properties
index d2a2ec7c..cd16bf64 100644
--- a/src/main/resources/i18n/TransactionValidity_zh_CN.properties
+++ b/src/main/resources/i18n/TransactionValidity_zh_CN.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = æ¤äº¤æ˜“å·²å˜åœ¨
TRANSACTION_UNKNOWN = 未知的交易
TX_GROUP_ID_MISMATCH = 群组ID交易ä¸å»åˆ
+
+TRANSFER_PRIVS_DISABLED = ä¼ è¾“æƒé™å·²ç¦ç”¨
+
+TEMPORARY_DISABLED = å称注册暂时ç¦ç”¨
diff --git a/src/main/resources/i18n/TransactionValidity_zh_TW.properties b/src/main/resources/i18n/TransactionValidity_zh_TW.properties
index e88addb9..d039a8e7 100644
--- a/src/main/resources/i18n/TransactionValidity_zh_TW.properties
+++ b/src/main/resources/i18n/TransactionValidity_zh_TW.properties
@@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = æ¤äº¤æ˜“å·²å˜åœ¨
TRANSACTION_UNKNOWN = 未知的交易
TX_GROUP_ID_MISMATCH = 群組ID交易ä¸å»åˆ
+
+TRANSFER_PRIVS_DISABLED = 傳輸權é™å·²åœç”¨
+
+TEMPORARY_DISABLED = å稱註冊暫時åœç”¨
diff --git a/src/main/resources/reticulum_default_config.yml b/src/main/resources/reticulum_default_config.yml
new file mode 100644
index 00000000..18e8b729
--- /dev/null
+++ b/src/main/resources/reticulum_default_config.yml
@@ -0,0 +1,93 @@
+---
+# You should probably edit it to include any additional,
+# interfaces and settings you might need.
+
+# Only the most basic options are included in this default
+# configuration. To see a more verbose, and much longer,
+# configuration example, you can run the command:
+# rnsd --exampleconfig
+
+reticulum:
+
+ # If you enable Transport, your system will route traffic
+ # for other peers, pass announces and serve path requests.
+ # This should only be done for systems that are suited to
+ # act as transport nodes, ie. if they are stationary and
+ # always-on. This directive is optional and can be removed
+ # for brevity.
+
+ enable_transport: false
+
+ # By default, the first program to launch the Reticulum
+ # Network Stack will create a shared instance, that other
+ # programs can communicate with. Only the shared instance
+ # opens all the configured interfaces directly, and other
+ # local programs communicate with the shared instance over
+ # a local socket. This is completely transparent to the
+ # user, and should generally be turned on. This directive
+ # is optional and can be removed for brevity.
+
+ share_instance: false
+
+ # If you want to run multiple *different* shared instances
+ # on the same system, you will need to specify different
+ # shared instance ports for each. The defaults are given
+ # below, and again, these options can be left out if you
+ # don't need them.
+
+ #shared_instance_port: 37428
+ #instance_control_port: 37429
+ shared_instance_port: 37438
+ instance_control_port: 37439
+
+ # You can configure Reticulum to panic and forcibly close
+ # if an unrecoverable interface error occurs, such as the
+ # hardware device for an interface disappearing. This is
+ # an optional directive, and can be left out for brevity.
+ # This behaviour is disabled by default.
+
+ panic_on_interface_error: false
+
+
+# The interfaces section defines the physical and virtual
+# interfaces Reticulum will use to communicate on. This
+# section will contain examples for a variety of interface
+# types. You can modify these or use them as a basis for
+# your own config, or simply remove the unused ones.
+
+interfaces:
+
+ # This interface enables communication with other
+ # link-local Reticulum nodes over UDP. It does not
+ # need any functional IP infrastructure like routers
+ # or DHCP servers, but will require that at least link-
+ # local IPv6 is enabled in your operating system, which
+ # should be enabled by default in almost any OS. See
+ # the Reticulum Manual for more configuration options.
+ #"Default Interface":
+ # type: AutoInterface
+ # enabled: true
+
+ # This interface enables communication with a "backbone"
+ # server over TCP.
+ # Note: others may be added for redundancy
+ "TCP Client Interface mobilefabrik":
+ type: TCPClientInterface
+ enabled: true
+ target_host: phantom.mobilefabrik.com
+ target_port: 4242
+ #network_name: qortal
+
+ # This interface turns this Reticulum instance into a
+ # server other clients can connect to over TCP.
+ # To enable this instance to route traffic the above
+ # setting "enable_transport" needs to be set (to true).
+ # Note: this interface type is not yet supported by
+ # reticulum-network-stack.
+ #"TCP Server Interface":
+ # type: TCPServerInterface
+ # enabled: true
+ # listen_ip: 0.0.0.0
+ # listen_port: 3434
+ # #network_name: qortal
+
diff --git a/src/test/java/org/qortal/test/PenaltyFixTests.java b/src/test/java/org/qortal/test/PenaltyFixTests.java
new file mode 100644
index 00000000..6d06f5f1
--- /dev/null
+++ b/src/test/java/org/qortal/test/PenaltyFixTests.java
@@ -0,0 +1,82 @@
+package org.qortal.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.Account;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.block.Block;
+import org.qortal.controller.BlockMinter;
+import org.qortal.data.transaction.PaymentTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.test.common.transaction.TestTransaction;
+import org.qortal.utils.NTP;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class PenaltyFixTests extends Common {
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useSettings("test-settings-v2-penalty-fix.json");
+ NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
+ }
+
+ @Test
+ public void testSingleSponsor() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Test account from real penalty data (pen-revert.json)
+ Account penaltyAccount = new Account(repository, "QLcAQpko5egwNjifueCAeAsT8CAj2Sr5qJ");
+
+ // Bob sends a payment to the penalty account, so that it gets a row in the Accounts table
+ TransactionData paymentData = new PaymentTransactionData(TestTransaction.generateBase(bobAccount), penaltyAccount.getAddress(), 1);
+ TransactionUtils.signAndImportValid(repository, paymentData, bobAccount); // updates paymentData's signature
+
+ // Mint blocks up to height 4
+ Block block = null;
+ for (int i = 2; i <= 4; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(4, (int)block.getBlockData().getHeight());
+
+ // Check blocks minted penalty of penalty account
+ assertEquals(0, (int) penaltyAccount.getBlocksMintedPenalty());
+
+ // Penalty revert code runs at block 5
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(5, (int)block.getBlockData().getHeight());
+
+ // +5000000 blocks minted penalty should be applied
+ assertEquals(5000000, (int) penaltyAccount.getBlocksMintedPenalty());
+
+ // Orphan the last block, to simulate a re-org
+ BlockUtils.orphanLastBlock(repository);
+
+ assertEquals(0, (int) penaltyAccount.getBlocksMintedPenalty());
+
+ // Penalty revert code runs again
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(5, (int)block.getBlockData().getHeight());
+
+ // Penalty should still be 5000000, rather than doubled up to 10000000
+ assertEquals(5000000, (int) penaltyAccount.getBlocksMintedPenalty());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
index 5c038de2..fe0556ca 100644
--- a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
+++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
@@ -38,7 +38,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
@Before
public void beforeTest() throws DataException {
- Common.useSettings("test-settings-v2-self-sponsorship-algo.json");
+ Common.useSettings("test-settings-v2-self-sponsorship-algo-v1.json");
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
}
diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java
new file mode 100644
index 00000000..495df511
--- /dev/null
+++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java
@@ -0,0 +1,342 @@
+package org.qortal.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.Account;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.asset.Asset;
+import org.qortal.block.Block;
+import org.qortal.controller.BlockMinter;
+import org.qortal.data.account.AccountPenaltyData;
+import org.qortal.data.transaction.*;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.utils.NTP;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+import static org.qortal.test.common.AccountUtils.fee;
+
+public class SelfSponsorshipAlgoV2Tests extends Common {
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useSettings("test-settings-v2-self-sponsorship-algo-v2.json");
+ NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
+ }
+
+ @Test
+ public void tesTransferAssetsQort() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that Bob, Chloe and Dilbert are greater than level 0
+ assertTrue(new Account(repository, bobAccount.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, chloeAccount.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, dilbertAccount.getAddress()).getLevel() > 0);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that Chloe and Dilbert have more than 20 qort
+ assertTrue(new Account(repository, chloeAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); // 10 for transfer asset, 10 for fee
+ assertTrue(new Account(repository, dilbertAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); // 10 for transfer asset, 10 for fee
+
+ // Mint until block 10
+ while (block.getBlockData().getHeight() < 10)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(10, (int) block.getBlockData().getHeight());
+
+ // Chloe transfer assets to Bob and Dilbert
+ transferAssets(repository, chloeAccount, bobAccount);
+ transferAssets(repository, chloeAccount, dilbertAccount);
+
+ // Dilbert transfer assets to Bob and Chloe
+ transferAssets(repository, dilbertAccount, bobAccount);
+ transferAssets(repository, dilbertAccount, chloeAccount);
+
+ // Mint until block 29 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that Bob have no penalties and level 5
+ assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(5, (int)bobAccount.getLevel());
+
+ // Ensure that Chloe have no penalties and level 5
+ assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(5, (int)chloeAccount.getLevel());
+
+ // Ensure that Dilbert have no penalties and level6
+ assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(6, (int)dilbertAccount.getLevel());
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that Bob, Chloe and Dilbert are now have penalties
+ assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that Bob, Chloe and Dilbert are now level 0
+ assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getLevel());
+ assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getLevel());
+ assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getLevel());
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Ensure that Bob, Chloe and Dilbert are now greater than level 0
+ assertTrue(new Account(repository, bobAccount.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, chloeAccount.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, dilbertAccount.getAddress()).getLevel() > 0);
+
+ // Ensure that Bob, Chloe and Dilbert have no penalties again
+ assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testSingleTransferPrivsBeforeAlgoBlock() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that Bob have more than 20 qort
+ assertTrue(new Account(repository, bobAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20);
+
+ // Mint until block 17 (the algo runs at block 20)
+ while (block.getBlockData().getHeight() < 26)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(26, (int) block.getBlockData().getHeight());
+
+ // Bob then issues a TRANSFER_PRIVS
+ PrivateKeyAccount recipientAccount = randomTransferPrivs(repository, bobAccount);
+
+ // Ensure recipient has no level (actually, no account record) at this point (pre-confirmation)
+ assertNull(recipientAccount.getLevel());
+
+ // Mint a block, so that the TRANSFER_PRIVS confirms
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Now ensure that the TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0
+ assertTrue(recipientAccount.getLevel() > 0);
+ assertEquals(0, (int)bobAccount.getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so that we can penalize Bob after transfer privs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Update blocks minted penalty for Bob
+ Set penalties = new LinkedHashSet<>();
+ penalties.add(new AccountPenaltyData(bobAccount.getAddress(), -5000000));
+ repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
+
+ // Mint a block, so that we check if Bob got penalized before algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure Bob got penalized
+ assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure recipient account has penalty too
+ assertEquals(-5000000, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getLevel());
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Ensure recipient account has no penalty again and has a level greater than 0
+ assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty());
+ assertTrue(new Account(repository, recipientAccount.getAddress()).getLevel() > 0);
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testMultipleTransferPrivsBeforeAlgoBlock() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that Bob, Chloe and Dilbert have more than 20 qort
+ assertTrue(new Account(repository, bobAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20);
+ assertTrue(new Account(repository, chloeAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20);
+ assertTrue(new Account(repository, dilbertAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20);
+
+ // Mint until block 12 (the algo runs at block 20)
+ while (block.getBlockData().getHeight() < 22)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(22, (int) block.getBlockData().getHeight());
+
+ // Bob then issues a TRANSFER_PRIVS
+ PrivateKeyAccount recipientAccount1 = randomTransferPrivs(repository, bobAccount);
+
+ // Ensure Bob's recipient has no level (actually, no account record) at this point (pre-confirmation)
+ assertNull(recipientAccount1.getLevel());
+
+ // Mint a block, so that Bob's TRANSFER_PRIVS confirms
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Now ensure that the Bob's TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0
+ assertTrue(recipientAccount1.getLevel() > 0);
+ assertEquals(0, (int)bobAccount.getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so that Chloe can issue a TRANSFER_PRIVS
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Chloe then issues a TRANSFER_PRIVS
+ PrivateKeyAccount recipientAccount2 = randomTransferPrivs(repository, chloeAccount);
+
+ // Ensure Chloe's recipient has no level (actually, no account record) at this point (pre-confirmation)
+ assertNull(recipientAccount2.getLevel());
+
+ // Mint a block, so that Chloe's TRANSFER_PRIVS confirms
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Now ensure that the Chloe's TRANSFER_PRIVS recipient has inherited Chloe's level, and Chloe is at level 0
+ assertTrue(recipientAccount2.getLevel() > 0);
+ assertEquals(0, (int)chloeAccount.getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so that Dilbert can issue a TRANSFER_PRIVS
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Dilbert then issues a TRANSFER_PRIVS
+ PrivateKeyAccount recipientAccount3 = randomTransferPrivs(repository, dilbertAccount);
+
+ // Ensure Dilbert's recipient has no level (actually, no account record) at this point (pre-confirmation)
+ assertNull(recipientAccount3.getLevel());
+
+ // Mint a block, so that Dilbert's TRANSFER_PRIVS confirms
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Now ensure that the Dilbert's TRANSFER_PRIVS recipient has inherited Dilbert's level, and Dilbert is at level 0
+ assertTrue(recipientAccount3.getLevel() > 0);
+ assertEquals(0, (int)dilbertAccount.getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so that we can penalize Bob, Chloe and Dilbert after transfer privs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Update blocks minted penalty for Bob, Chloe and Dilbert
+ Set penalties = new LinkedHashSet<>();
+ penalties.add(new AccountPenaltyData(bobAccount.getAddress(), -5000000));
+ penalties.add(new AccountPenaltyData(chloeAccount.getAddress(), -5000000));
+ penalties.add(new AccountPenaltyData(dilbertAccount.getAddress(), -5000000));
+ repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
+
+ // Mint a block, so that we check if Bob, Chloe and Dilbert got penalized before algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure Bob, Chloe and Dilbert got penalized
+ assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure recipients accounts has penalty too
+ assertEquals(-5000000, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty());
+ assertEquals(-5000000, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getLevel());
+ assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getLevel());
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Ensure recipients accounts has no penalty again and has a level greater than 0
+ assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty());
+ assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty());
+ assertTrue(new Account(repository, recipientAccount1.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, recipientAccount2.getAddress()).getLevel() > 0);
+ assertTrue(new Account(repository, recipientAccount3.getAddress()).getLevel() > 0);
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ private static void transferAssets(Repository repository, PrivateKeyAccount senderAccount, PrivateKeyAccount recipientAccount) throws DataException {
+ for (int i = 0; i < 5; i++) {
+ // Generate new asset transfers from sender to recipient
+ BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null);
+ TransactionData transactionData;
+ transactionData = new TransferAssetTransactionData(baseTransactionData, recipientAccount.getAddress(), 1, 0);
+ TransactionUtils.signAndImportValid(repository, transactionData, senderAccount); // updates paymentData's signature
+ }
+ }
+
+ private static PrivateKeyAccount randomTransferPrivs(Repository repository, PrivateKeyAccount senderAccount) throws DataException {
+ // Generate random recipient account
+ byte[] randomPrivateKey = new byte[32];
+ new Random().nextBytes(randomPrivateKey);
+ PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey);
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null);
+ TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, recipientAccount.getAddress());
+
+ TransactionUtils.signAndImportValid(repository, transactionData, senderAccount);
+
+ return recipientAccount;
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java
new file mode 100644
index 00000000..bb9de4e3
--- /dev/null
+++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java
@@ -0,0 +1,1578 @@
+package org.qortal.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.Account;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.asset.Asset;
+import org.qortal.block.Block;
+import org.qortal.controller.BlockMinter;
+import org.qortal.data.transaction.BaseTransactionData;
+import org.qortal.data.transaction.PaymentTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.data.transaction.TransferPrivsTransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.settings.Settings;
+import org.qortal.test.common.AccountUtils;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.test.common.transaction.TestTransaction;
+import org.qortal.transaction.Transaction;
+import org.qortal.transaction.TransferPrivsTransaction;
+import org.qortal.utils.NTP;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.Assert.*;
+import static org.qortal.test.common.AccountUtils.fee;
+import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
+import static org.qortal.transaction.Transaction.ValidationResult.OK;
+
+public class SelfSponsorshipAlgoV3Tests extends Common {
+
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useSettings("test-settings-v2-self-sponsorship-algo-v3.json");
+ NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
+ }
+
+
+ @Test
+ public void testSingleSponsor() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Bob self sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(11, block.getBlockData().getOnlineAccountsCount());
+ assertEquals(10, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
+ onlineAccounts.addAll(bobSponseeSelfShares);
+
+ // Mint blocks
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Bob then consolidates funds
+ consolidateFunds(repository, bobSponsees, bobAccount);
+
+ // Mint until block 29 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that bob and his sponsees have no penalties
+ List bobAndSponsees = new ArrayList<>(bobSponsees);
+ bobAndSponsees.add(bobAccount);
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that bob and his sponsees now have penalties
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are now level 0
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel());
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Ensure that bob and his sponsees are now greater than level 0
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Ensure that bob and his sponsees have no penalties again
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testMultipleSponsors() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Bob sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Chloe sponsors 10 accounts
+ List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
+ List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
+ onlineAccounts.addAll(chloeSponseesOnlineAccounts);
+
+ // Dilbert sponsors 5 accounts
+ List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
+ List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
+ onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(26, block.getBlockData().getOnlineAccountsCount());
+ assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
+ onlineAccounts.addAll(bobSponseeSelfShares);
+
+ // Mint blocks
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Bob then consolidates funds
+ consolidateFunds(repository, bobSponsees, bobAccount);
+
+ // Mint until block 29 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that bob and his sponsees have no penalties
+ List bobAndSponsees = new ArrayList<>(bobSponsees);
+ bobAndSponsees.add(bobAccount);
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ assertEquals(6, (int)dilbertAccount.getLevel());
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that bob and his sponsees now have penalties
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that chloe and her sponsees have no penalties
+ List chloeAndSponsees = new ArrayList<>(chloeSponsees);
+ chloeAndSponsees.add(chloeAccount);
+ for (PrivateKeyAccount chloeSponsee : chloeAndSponsees)
+ assertEquals(0, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that dilbert and his sponsees have no penalties
+ List dilbertAndSponsees = new ArrayList<>(dilbertSponsees);
+ dilbertAndSponsees.add(dilbertAccount);
+ for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees)
+ assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are now level 0
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel());
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Ensure that bob and his sponsees are now greater than level 0
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Ensure that bob and his sponsees have no penalties again
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that chloe and her sponsees still have no penalties
+ for (PrivateKeyAccount chloeSponsee : chloeAndSponsees)
+ assertEquals(0, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that dilbert and his sponsees still have no penalties
+ for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees)
+ assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testMintBlockWithSignerPenalty() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ List onlineAccountsAliceSigner = new ArrayList<>();
+ List onlineAccountsBobSigner = new ArrayList<>();
+
+ // Alice self share online, and will be used to mint (some of) the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ onlineAccountsAliceSigner.add(aliceSelfShare);
+
+ // Bob self share online, and will be used to mint (some of) the blocks
+ PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
+ onlineAccountsBobSigner.add(bobSelfShare);
+
+ // Include Alice and Bob's online accounts in each other's arrays
+ onlineAccountsAliceSigner.add(bobSelfShare);
+ onlineAccountsBobSigner.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Bob sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts);
+ onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts);
+
+ // Chloe sponsors 10 accounts
+ List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
+ List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
+ onlineAccountsAliceSigner.addAll(chloeSponseesOnlineAccounts);
+ onlineAccountsBobSigner.addAll(chloeSponseesOnlineAccounts);
+
+ // Dilbert sponsors 5 accounts
+ List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
+ List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
+ onlineAccountsAliceSigner.addAll(dilbertSponseesOnlineAccounts);
+ onlineAccountsBobSigner.addAll(dilbertSponseesOnlineAccounts);
+
+ // Mint blocks (Bob is the signer)
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+
+ // Get reward share transaction count
+ assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up (Bob is the signer)
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
+ onlineAccountsAliceSigner.addAll(bobSponseeSelfShares);
+ onlineAccountsBobSigner.addAll(bobSponseeSelfShares);
+
+ // Mint blocks (Bob is the signer)
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Bob then consolidates funds
+ consolidateFunds(repository, bobSponsees, bobAccount);
+
+ // Mint until block 29 (the algo runs at block 30) (Bob is the signer)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that bob and his sponsees have no penalties
+ List bobAndSponsees = new ArrayList<>(bobSponsees);
+ bobAndSponsees.add(bobAccount);
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs (Bob is the signer)
+ // Block should be valid, because new account levels don't take effect until next block's validation
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that bob and his sponsees now have penalties
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are now level 0
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel());
+
+ // Mint a block, but Bob is now an invalid signer because he is level 0
+ block = BlockMinter.mintTestingBlockUnvalidated(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+ // Block should be null as it's unable to be minted
+ assertNull(block);
+
+ // Mint the same block with Alice as the signer, and this time it should be valid
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+ // Block should NOT be null
+ assertNotNull(block);
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testMintBlockWithFounderSignerPenalty() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ List onlineAccountsAliceSigner = new ArrayList<>();
+ List onlineAccountsBobSigner = new ArrayList<>();
+
+ // Alice self share online, and will be used to mint (some of) the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ onlineAccountsAliceSigner.add(aliceSelfShare);
+
+ // Bob self share online, and will be used to mint (some of) the blocks
+ PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
+ onlineAccountsBobSigner.add(bobSelfShare);
+
+ // Include Alice and Bob's online accounts in each other's arrays
+ onlineAccountsAliceSigner.add(bobSelfShare);
+ onlineAccountsBobSigner.add(aliceSelfShare);
+
+ PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Alice sponsors 10 accounts
+ List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
+ List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
+ onlineAccountsAliceSigner.addAll(aliceSponseesOnlineAccounts);
+ onlineAccountsBobSigner.addAll(aliceSponseesOnlineAccounts);
+
+ // Bob sponsors 9 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts);
+ onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts);
+
+ // Mint blocks (Bob is the signer)
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+
+ // Get reward share transaction count
+ assertEquals(19, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up (Alice is the signer)
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
+ onlineAccountsAliceSigner.addAll(aliceSponseeSelfShares);
+ onlineAccountsBobSigner.addAll(aliceSponseeSelfShares);
+
+ // Mint blocks (Bob is the signer)
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Alice then consolidates funds
+ consolidateFunds(repository, aliceSponsees, aliceAccount);
+
+ // Mint until block 29 (the algo runs at block 30) (Bob is the signer)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that alice and her sponsees have no penalties
+ List aliceAndSponsees = new ArrayList<>(aliceSponsees);
+ aliceAndSponsees.add(aliceAccount);
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs (Alice is the signer)
+ // Block should be valid, because new account levels don't take effect until next block's validation
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that alice and her sponsees now have penalties
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that alice and her sponsees are now level 0
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getLevel());
+
+ // Mint a block, but Alice is now an invalid signer because she has lost founder minting abilities
+ block = BlockMinter.mintTestingBlockUnvalidated(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0]));
+ // Block should be null as it's unable to be minted
+ assertNull(block);
+
+ // Mint the same block with Bob as the signer, and this time it should be valid
+ block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0]));
+ // Block should NOT be null
+ assertNotNull(block);
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testOnlineAccountsWithPenalties() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ // Bob self share online
+ PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
+ onlineAccounts.add(bobSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Bob sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Chloe sponsors 10 accounts
+ List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
+ List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
+ onlineAccounts.addAll(chloeSponseesOnlineAccounts);
+
+ // Dilbert sponsors 5 accounts
+ List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
+ List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
+ onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(27, block.getBlockData().getOnlineAccountsCount());
+ assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
+ onlineAccounts.addAll(bobSponseeSelfShares);
+
+ // Mint blocks
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount bobSponsee : bobSponsees)
+ assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Bob then consolidates funds
+ consolidateFunds(repository, bobSponsees, bobAccount);
+
+ // Mint until block 29 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that bob and his sponsees have no penalties
+ List bobAndSponsees = new ArrayList<>(bobSponsees);
+ bobAndSponsees.add(bobAccount);
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block));
+
+ // Ensure that chloe's sponsees are present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(chloeSponsees, block));
+
+ // Ensure that dilbert's sponsees are present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(dilbertSponsees, block));
+
+ // Mint a block, so the algo runs
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that bob and his sponsees now have penalties
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block));
+
+ // Mint another few blocks
+ while (block.getBlockData().getHeight() < 34)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(34, (int)block.getBlockData().getHeight());
+
+ // Ensure that bob and his sponsees are NOT present in block's online accounts (due to penalties)
+ assertFalse(areAllAccountsPresentInBlock(bobAndSponsees, block));
+
+ // Ensure that chloe's sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(chloeSponsees, block));
+
+ // Ensure that dilbert's sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(dilbertSponsees, block));
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testFounderOnlineAccountsWithPenalties() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Bob self share online, and will be used to mint the blocks
+ PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(bobSelfShare);
+
+ // Alice self share online
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Alice sponsors 10 accounts
+ List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
+ List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
+ onlineAccounts.addAll(aliceSponseesOnlineAccounts);
+ onlineAccounts.addAll(aliceSponseesOnlineAccounts);
+
+ // Bob sponsors 9 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Mint blocks (Bob is the signer)
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Get reward share transaction count
+ assertEquals(19, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() == 0);
+
+ // Mint some blocks, until accounts have leveled up (Alice is the signer)
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0);
+
+ // Generate self shares so the sponsees can start minting
+ List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
+ onlineAccounts.addAll(aliceSponseeSelfShares);
+
+ // Mint blocks (Bob is the signer)
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ for (PrivateKeyAccount aliceSponsee : aliceSponsees)
+ assertTrue(new Account(repository, aliceSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee
+
+ // Alice then consolidates funds
+ consolidateFunds(repository, aliceSponsees, aliceAccount);
+
+ // Mint until block 29 (the algo runs at block 30) (Bob is the signer)
+ while (block.getBlockData().getHeight() < 29)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(29, (int) block.getBlockData().getHeight());
+
+ // Ensure that alice and her sponsees have no penalties
+ List aliceAndSponsees = new ArrayList<>(aliceSponsees);
+ aliceAndSponsees.add(aliceAccount);
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Mint a block, so the algo runs (Alice is the signer)
+ // Block should be valid, because new account levels don't take effect until next block's validation
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Ensure that alice and her sponsees now have penalties
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(-5000000, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that alice and her sponsees are now level 0
+ for (PrivateKeyAccount aliceSponsee : aliceAndSponsees)
+ assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getLevel());
+
+ // Ensure that alice and her sponsees don't have penalties
+ List bobAndSponsees = new ArrayList<>(bobSponsees);
+ bobAndSponsees.add(bobAccount);
+ for (PrivateKeyAccount bobSponsee : bobAndSponsees)
+ assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that bob and his sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block));
+
+ // Ensure that alice and her sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(aliceAndSponsees, block));
+
+ // Mint another few blocks
+ while (block.getBlockData().getHeight() < 34)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(34, (int)block.getBlockData().getHeight());
+
+ // Ensure that alice and her sponsees are NOT present in block's online accounts (due to penalties)
+ assertFalse(areAllAccountsPresentInBlock(aliceAndSponsees, block));
+
+ // Ensure that bob and his sponsees are still present in block's online accounts
+ assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block));
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testPenaltyAccountCreateRewardShare() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+ PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
+
+ // Bob sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Chloe sponsors 10 accounts
+ List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
+ List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
+ onlineAccounts.addAll(chloeSponseesOnlineAccounts);
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(21, block.getBlockData().getOnlineAccountsCount());
+ assertEquals(20, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Generate self shares so the sponsees can start minting
+ List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
+ onlineAccounts.addAll(bobSponseeSelfShares);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Bob then consolidates funds
+ consolidateFunds(repository, bobSponsees, bobAccount);
+
+ // Mint until block 12 (rewardshare get disabled at block 15)
+ while (block.getBlockData().getHeight() < 12)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(12, (int) block.getBlockData().getHeight());
+
+ // Bob creates a valid reward share transaction
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
+
+ // Mint until block 30 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 30)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(30, (int) block.getBlockData().getHeight());
+
+ // Bob can no longer create a reward share transaction
+ assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, bobAccount));
+
+ // ... but Chloe still can
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, chloeAccount));
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Bob creates another valid reward share transaction
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ @Test
+ public void testPenaltyFounderCreateRewardShare() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size();
+
+ // Bob self share online, and will be used to mint the blocks
+ PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(bobSelfShare);
+
+ PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+
+ // Alice sponsors 10 accounts
+ List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
+ List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
+ onlineAccounts.addAll(aliceSponseesOnlineAccounts);
+
+ // Bob sponsors 10 accounts
+ List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
+ List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
+ onlineAccounts.addAll(bobSponseesOnlineAccounts);
+
+ // Mint blocks
+ Block block = null;
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ assertEquals(21, block.getBlockData().getOnlineAccountsCount());
+ assertEquals(20, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount);
+
+ // Mint some blocks, until accounts have leveled up
+ for (int i = 0; i <= 5; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Generate self shares so the sponsees can start minting
+ List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
+ onlineAccounts.addAll(aliceSponseeSelfShares);
+
+ // Mint some blocks
+ for (int i = 0; i <= 1; i++)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+
+ // Alice then consolidates funds
+ consolidateFunds(repository, aliceSponsees, aliceAccount);
+
+ // Mint until block 12 (rewardshare get disabled at block 15)
+ while (block.getBlockData().getHeight() < 12)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(12, (int) block.getBlockData().getHeight());
+
+ // Alice creates a valid reward share transaction
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount));
+
+ // Mint until block 30 (the algo runs at block 30)
+ while (block.getBlockData().getHeight() < 30)
+ block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
+ assertEquals(30, (int) block.getBlockData().getHeight());
+
+ // Ensure that alice now has a penalty
+ assertEquals(-5000000, (int) new Account(repository, aliceAccount.getAddress()).getBlocksMintedPenalty());
+
+ // Ensure that alice and her sponsees are now level 0
+ assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel());
+
+ // Alice can no longer create a reward share transaction
+ assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, aliceAccount));
+
+ // Bob can no longer create a reward share transaction (disabled at Block 15)
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Alice creates another valid reward share transaction
+ assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount));
+
+ // Run orphan check - this can't be in afterTest() because some tests access the live db
+ Common.orphanCheck();
+ }
+ }
+
+ /**
+ * This is a test to prove that Dilbert levels up from 6 to 7 in the same block that the self
+ * sponsorship algo runs. It is here to give some confidence in the following testPenaltyAccountLevelUp()
+ * test, in which we will test what happens if a penalty is applied or removed in the same block
+ * that an account would otherwise have leveled up. It also gives some confidence that the algo
+ * doesn't affect the levels of unflagged accounts.
+ *
+ * @throws DataException
+ */
+ @Test
+ public void testNonPenaltyAccountLevelUp() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ // Alice self share online, and will be used to mint the blocks
+ PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
+ List onlineAccounts = new ArrayList<>();
+ onlineAccounts.add(aliceSelfShare);
+
+ PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
+
+ // Dilbert sponsors 10 accounts
+ List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 10);
+ List