diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
new file mode 100644
index 00000000..c2c3d355
Binary files /dev/null and b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar differ
diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
new file mode 100644
index 00000000..0dc1aedc
--- /dev/null
+++ b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
@@ -0,0 +1,9 @@
+
+
+ 4.0.0
+ org.ciyam
+ AT
+ 1.4.0
+ POM was created from install:install-file
+
diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml
index 8f8b1f6e..063c735d 100644
--- a/lib/org/ciyam/AT/maven-metadata-local.xml
+++ b/lib/org/ciyam/AT/maven-metadata-local.xml
@@ -3,14 +3,15 @@
org.ciyam
AT
- 1.3.8
+ 1.4.0
1.3.4
1.3.5
1.3.6
1.3.7
1.3.8
+ 1.4.0
- 20200925114415
+ 20221105114346
diff --git a/pom.xml b/pom.xml
index eb306420..52c574b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 3.6.4
+ 3.7.0
jar
true
@@ -11,7 +11,7 @@
0.15.10
1.69
${maven.build.timestamp}
- 1.3.8
+ 1.4.0
3.6
1.8
2.6
diff --git a/src/main/java/org/qortal/arbitrary/misc/Service.java b/src/main/java/org/qortal/arbitrary/misc/Service.java
index 5d94d806..5dd8d94e 100644
--- a/src/main/java/org/qortal/arbitrary/misc/Service.java
+++ b/src/main/java/org/qortal/arbitrary/misc/Service.java
@@ -1,16 +1,18 @@
package org.qortal.arbitrary.misc;
+import org.apache.commons.io.FilenameUtils;
import org.json.JSONObject;
import org.qortal.arbitrary.ArbitraryDataRenderer;
import org.qortal.transaction.Transaction;
import org.qortal.utils.FilesystemUtils;
+import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -38,6 +40,7 @@ public enum Service {
GIT_REPOSITORY(300, false, null, null),
IMAGE(400, true, 10*1024*1024L, null),
THUMBNAIL(410, true, 500*1024L, null),
+ QCHAT_IMAGE(420, true, 500*1024L, null),
VIDEO(500, false, null, null),
AUDIO(600, false, null, null),
BLOG(700, false, null, null),
@@ -48,7 +51,30 @@ public enum Service {
PLAYLIST(910, true, null, null),
APP(1000, false, null, null),
METADATA(1100, false, null, null),
- QORTAL_METADATA(1111, true, 10*1024L, Arrays.asList("title", "description", "tags"));
+ GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
+ @Override
+ public ValidationResult validate(Path path) {
+ // Custom validation function to require .gif files only, and at least 1
+ int gifCount = 0;
+ File[] files = path.toFile().listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ return ValidationResult.DIRECTORIES_NOT_ALLOWED;
+ }
+ String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
+ if (!Objects.equals(extension, "gif")) {
+ return ValidationResult.INVALID_FILE_EXTENSION;
+ }
+ gifCount++;
+ }
+ }
+ if (gifCount == 0) {
+ return ValidationResult.MISSING_DATA;
+ }
+ return ValidationResult.OK;
+ }
+ };
public final int value;
private final boolean requiresValidation;
@@ -114,7 +140,10 @@ public enum Service {
OK(1),
MISSING_KEYS(2),
EXCEEDS_SIZE_LIMIT(3),
- MISSING_INDEX_FILE(4);
+ MISSING_INDEX_FILE(4),
+ DIRECTORIES_NOT_ALLOWED(5),
+ INVALID_FILE_EXTENSION(6),
+ MISSING_DATA(7);
public final int value;
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index 07c7db6f..5e838458 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -366,14 +366,9 @@ public class Block {
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
- // Fetch our list of online accounts
+ // Fetch our list of online accounts, removing any that are missing a nonce
List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
-
- // If mempow is active, remove any legacy accounts that are missing a nonce
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
- }
-
+ onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
@@ -412,29 +407,27 @@ public class Block {
// Aggregated, single signature
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
- // Add nonces to the end of the online accounts signatures if mempow is active
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- try {
- // Create ordered list of nonce values
- List nonces = new ArrayList<>();
- for (int i = 0; i < onlineAccountsCount; ++i) {
- Integer accountIndex = accountIndexes.get(i);
- OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
- nonces.add(onlineAccountData.getNonce());
- }
-
- // Encode the nonces to a byte array
- byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
-
- // Append the encoded nonces to the encoded online account signatures
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- outputStream.write(onlineAccountsSignatures);
- outputStream.write(encodedNonces);
- onlineAccountsSignatures = outputStream.toByteArray();
- }
- catch (TransformationException | IOException e) {
- return null;
+ // Add nonces to the end of the online accounts signatures
+ try {
+ // Create ordered list of nonce values
+ List nonces = new ArrayList<>();
+ for (int i = 0; i < onlineAccountsCount; ++i) {
+ Integer accountIndex = accountIndexes.get(i);
+ OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
+ nonces.add(onlineAccountData.getNonce());
}
+
+ // Encode the nonces to a byte array
+ byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
+
+ // Append the encoded nonces to the encoded online account signatures
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ outputStream.write(onlineAccountsSignatures);
+ outputStream.write(encodedNonces);
+ onlineAccountsSignatures = outputStream.toByteArray();
+ }
+ catch (TransformationException | IOException e) {
+ return null;
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
@@ -1047,14 +1040,9 @@ public class Block {
final int signaturesLength = Transformer.SIGNATURE_LENGTH;
final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH;
- if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- // We expect nonces to be appended to the online accounts signatures
- if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
- return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
- } else {
- if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength)
- return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
- }
+ // We expect nonces to be appended to the online accounts signatures
+ if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
+ return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
@@ -1063,32 +1051,33 @@ public class Block {
byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures();
// Split online account signatures into signature(s) + nonces, then validate the nonces
- if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
- byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
- encodedOnlineAccountSignatures = extractedSignatures;
+ byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
+ byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
+ encodedOnlineAccountSignatures = extractedSignatures;
- List nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
+ List nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
- // Build block's view of online accounts (without signatures, as we don't need them here)
- Set onlineAccounts = new HashSet<>();
- for (int i = 0; i < onlineRewardShares.size(); ++i) {
- Integer nonce = nonces.get(i);
- byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
+ // Build block's view of online accounts (without signatures, as we don't need them here)
+ Set onlineAccounts = new HashSet<>();
+ for (int i = 0; i < onlineRewardShares.size(); ++i) {
+ Integer nonce = nonces.get(i);
+ byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
- OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
- onlineAccounts.add(onlineAccountData);
- }
-
- // Remove those already validated & cached by online accounts manager - no need to re-validate them
- OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
-
- // Validate the rest
- for (OnlineAccountData onlineAccount : onlineAccounts)
- if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, this.blockData.getTimestamp()))
- return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
+ OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
+ onlineAccounts.add(onlineAccountData);
}
+ // Remove those already validated & cached by online accounts manager - no need to re-validate them
+ OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
+
+ // Validate the rest
+ for (OnlineAccountData onlineAccount : onlineAccounts)
+ if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, null))
+ return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
+
+ // Cache the valid online accounts as they will likely be needed for the next block
+ OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
+
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index d483f8d7..70751056 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -74,6 +74,7 @@ public class BlockChain {
transactionV5Timestamp,
transactionV6Timestamp,
disableReferenceTimestamp,
+ increaseOnlineAccountsDifficultyTimestamp,
chatReferenceTimestamp;
}
@@ -196,10 +197,6 @@ public class BlockChain {
* featureTriggers because unit tests need to set this value via Reflection. */
private long onlineAccountsModulusV2Timestamp;
- /** Feature trigger timestamp for online accounts mempow verification. Can't use featureTriggers
- * because unit tests need to set this value via Reflection. */
- private long onlineAccountsMemoryPoWTimestamp;
-
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -360,10 +357,6 @@ public class BlockChain {
return this.onlineAccountsModulusV2Timestamp;
}
- public long getOnlineAccountsMemoryPoWTimestamp() {
- return this.onlineAccountsMemoryPoWTimestamp;
- }
-
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -487,6 +480,10 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
}
+ public long getIncreaseOnlineAccountsDifficultyTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
+ }
+
public long getChatReferenceTimestamp() {
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
}
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 6fe6a159..bcd010e8 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -838,6 +838,12 @@ public class Controller extends Thread {
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
if (!Settings.getInstance().isLite()) {
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
+
+ final Integer blocksRemaining = Synchronizer.getInstance().getBlocksRemaining();
+ if (blocksRemaining != null && blocksRemaining > 0) {
+ String blocksRemainingText = Translator.INSTANCE.translate("SysTray", "BLOCKS_REMAINING");
+ tooltip = tooltip.concat(String.format(" - %d %s", blocksRemaining, blocksRemainingText));
+ }
}
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
SysTray.getInstance().setToolTipText(tooltip);
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index 45b47f5d..fd2c38df 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -64,9 +64,19 @@ public class OnlineAccountsManager {
private static final long ONLINE_ACCOUNTS_COMPUTE_INITIAL_SLEEP_INTERVAL = 30 * 1000L; // ms
- // MemoryPoW
- public final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
- public int POW_DIFFICULTY = 18; // leading zero bits
+ // MemoryPoW - mainnet
+ public static final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
+ public static final int POW_DIFFICULTY_V1 = 18; // leading zero bits
+ public static final int POW_DIFFICULTY_V2 = 19; // leading zero bits
+
+ // MemoryPoW - testnet
+ public static final int POW_BUFFER_SIZE_TESTNET = 1 * 1024 * 1024; // bytes
+ public static final int POW_DIFFICULTY_TESTNET = 5; // leading zero bits
+
+ // IMPORTANT: if we ever need to dynamically modify the buffer size using a feature trigger, the
+ // pre-allocated buffer below will NOT work, and we should instead use a dynamically allocated
+ // one for the transition period.
+ private static long[] POW_VERIFY_WORK_BUFFER = new long[getPoWBufferSize() / 8];
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, new NamedThreadFactory("OnlineAccounts"));
private volatile boolean isStopping = false;
@@ -112,6 +122,23 @@ public class OnlineAccountsManager {
return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
}
+ private static int getPoWBufferSize() {
+ if (Settings.getInstance().isTestNet())
+ return POW_BUFFER_SIZE_TESTNET;
+
+ return POW_BUFFER_SIZE;
+ }
+
+ private static int getPoWDifficulty(long timestamp) {
+ if (Settings.getInstance().isTestNet())
+ return POW_DIFFICULTY_TESTNET;
+
+ if (timestamp >= BlockChain.getInstance().getIncreaseOnlineAccountsDifficultyTimestamp())
+ return POW_DIFFICULTY_V2;
+
+ return POW_DIFFICULTY_V1;
+ }
+
private OnlineAccountsManager() {
}
@@ -156,7 +183,6 @@ public class OnlineAccountsManager {
return;
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
- final boolean mempowActive = onlineAccountsTimestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
Set replacementAccounts = new HashSet<>();
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
@@ -165,7 +191,7 @@ public class OnlineAccountsManager {
byte[] signature = Qortal25519Extras.signForAggregation(onlineAccount.getPrivateKey(), timestampBytes);
byte[] publicKey = onlineAccount.getPublicKey();
- Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
+ Integer nonce = new Random().nextInt(500000);
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
replacementAccounts.add(ourOnlineAccountData);
@@ -321,13 +347,10 @@ public class OnlineAccountsManager {
return false;
}
- // Validate mempow if feature trigger is active (or if online account's timestamp is past the trigger timestamp)
- long memoryPoWStartTimestamp = BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
- if (now >= memoryPoWStartTimestamp || onlineAccountTimestamp >= memoryPoWStartTimestamp) {
- if (!getInstance().verifyMemoryPoW(onlineAccountData, now)) {
- LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress()));
- return false;
- }
+ // Validate mempow
+ if (!getInstance().verifyMemoryPoW(onlineAccountData, POW_VERIFY_WORK_BUFFER)) {
+ LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress()));
+ return false;
}
return true;
@@ -471,12 +494,10 @@ public class OnlineAccountsManager {
// 'next' timestamp (prioritize this as it's the most important, if mempow active)
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(now) + getOnlineTimestampModulus();
- if (isMemoryPoWActive(now)) {
- boolean success = computeOurAccountsForTimestamp(nextOnlineAccountsTimestamp);
- if (!success) {
- // We didn't compute the required nonce value(s), and so can't proceed until they have been retried
- return;
- }
+ boolean success = computeOurAccountsForTimestamp(nextOnlineAccountsTimestamp);
+ if (!success) {
+ // We didn't compute the required nonce value(s), and so can't proceed until they have been retried
+ return;
}
// 'current' timestamp
@@ -553,21 +574,15 @@ public class OnlineAccountsManager {
// Compute nonce
Integer nonce;
- if (isMemoryPoWActive(NTP.getTime())) {
- try {
- nonce = this.computeMemoryPoW(mempowBytes, publicKey, onlineAccountsTimestamp);
- if (nonce == null) {
- // A nonce is required
- return false;
- }
- } catch (TimeoutException e) {
- LOGGER.info(String.format("Timed out computing nonce for account %.8s", Base58.encode(publicKey)));
+ try {
+ nonce = this.computeMemoryPoW(mempowBytes, publicKey, onlineAccountsTimestamp);
+ if (nonce == null) {
+ // A nonce is required
return false;
}
- }
- else {
- // Send -1 if we haven't computed a nonce due to feature trigger timestamp
- nonce = -1;
+ } catch (TimeoutException e) {
+ LOGGER.info(String.format("Timed out computing nonce for account %.8s", Base58.encode(publicKey)));
+ return false;
}
byte[] signature = Qortal25519Extras.signForAggregation(privateKey, timestampBytes);
@@ -576,7 +591,7 @@ public class OnlineAccountsManager {
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
// Make sure to verify before adding
- if (verifyMemoryPoW(ourOnlineAccountData, NTP.getTime())) {
+ if (verifyMemoryPoW(ourOnlineAccountData, null)) {
ourOnlineAccounts.add(ourOnlineAccountData);
}
}
@@ -599,12 +614,6 @@ public class OnlineAccountsManager {
// MemoryPoW
- private boolean isMemoryPoWActive(Long timestamp) {
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- return true;
- }
- return false;
- }
private byte[] getMemoryPoWBytes(byte[] publicKey, long onlineAccountsTimestamp) throws IOException {
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
@@ -616,11 +625,6 @@ public class OnlineAccountsManager {
}
private Integer computeMemoryPoW(byte[] bytes, byte[] publicKey, long onlineAccountsTimestamp) throws TimeoutException {
- if (!isMemoryPoWActive(NTP.getTime())) {
- LOGGER.info("Mempow start timestamp not yet reached");
- return null;
- }
-
LOGGER.info(String.format("Computing nonce for account %.8s and timestamp %d...", Base58.encode(publicKey), onlineAccountsTimestamp));
// Calculate the time until the next online timestamp and use it as a timeout when computing the nonce
@@ -628,7 +632,8 @@ public class OnlineAccountsManager {
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(startTime) + getOnlineTimestampModulus();
long timeUntilNextTimestamp = nextOnlineAccountsTimestamp - startTime;
- Integer nonce = MemoryPoW.compute2(bytes, POW_BUFFER_SIZE, POW_DIFFICULTY, timeUntilNextTimestamp);
+ int difficulty = getPoWDifficulty(onlineAccountsTimestamp);
+ Integer nonce = MemoryPoW.compute2(bytes, getPoWBufferSize(), difficulty, timeUntilNextTimestamp);
double totalSeconds = (NTP.getTime() - startTime) / 1000.0f;
int minutes = (int) ((totalSeconds % 3600) / 60);
@@ -637,18 +642,12 @@ public class OnlineAccountsManager {
LOGGER.info(String.format("Computed nonce for timestamp %d and account %.8s: %d. Buffer size: %d. Difficulty: %d. " +
"Time taken: %02d:%02d. Hashrate: %f", onlineAccountsTimestamp, Base58.encode(publicKey),
- nonce, POW_BUFFER_SIZE, POW_DIFFICULTY, minutes, seconds, hashRate));
+ nonce, getPoWBufferSize(), difficulty, minutes, seconds, hashRate));
return nonce;
}
- public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, Long timestamp) {
- long memoryPoWStartTimestamp = BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
- if (timestamp < memoryPoWStartTimestamp && onlineAccountData.getTimestamp() < memoryPoWStartTimestamp) {
- // Not active yet, so treat it as valid
- return true;
- }
-
+ public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, long[] workBuffer) {
// Require a valid nonce value
if (onlineAccountData.getNonce() == null || onlineAccountData.getNonce() < 0) {
return false;
@@ -664,7 +663,7 @@ public class OnlineAccountsManager {
}
// Verify the nonce
- return MemoryPoW.verify2(mempowBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
+ return MemoryPoW.verify2(mempowBytes, workBuffer, getPoWBufferSize(), getPoWDifficulty(onlineAccountData.getTimestamp()), nonce);
}
@@ -748,11 +747,12 @@ public class OnlineAccountsManager {
* Typically called by {@link Block#areOnlineAccountsValid()}
*/
public void addBlocksOnlineAccounts(Set blocksOnlineAccounts, Long timestamp) {
- // We want to add to 'current' in preference if possible
- if (this.currentOnlineAccounts.containsKey(timestamp)) {
- addAccounts(blocksOnlineAccounts);
+ // If these are current accounts, then there is no need to cache them, and should instead rely
+ // on the more complete entries we already have in self.currentOnlineAccounts.
+ // Note: since sig-agg, we no longer have individual signatures included in blocks, so we
+ // mustn't add anything to currentOnlineAccounts from here.
+ if (this.currentOnlineAccounts.containsKey(timestamp))
return;
- }
// Add to block cache instead
this.latestBlocksOnlineAccounts.computeIfAbsent(timestamp, k -> ConcurrentHashMap.newKeySet())
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 6f2a0fe1..cd9483e9 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -76,6 +76,8 @@ public class Synchronizer extends Thread {
private volatile boolean isSynchronizing = false;
/** Temporary estimate of synchronization progress for SysTray use. */
private volatile int syncPercent = 0;
+ /** Temporary estimate of blocks remaining for SysTray use. */
+ private volatile int blocksRemaining = 0;
private static volatile boolean requestSync = false;
private boolean syncRequestPending = false;
@@ -181,6 +183,18 @@ public class Synchronizer extends Thread {
}
}
+ public Integer getBlocksRemaining() {
+ synchronized (this.syncLock) {
+ // Report as 0 blocks remaining if the latest block is within the last 60 mins
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
+ if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
+ return 0;
+ }
+
+ return this.isSynchronizing ? this.blocksRemaining : null;
+ }
+ }
+
public void requestSync() {
requestSync = true;
}
@@ -1457,6 +1471,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
+ synchronized (this.syncLock) {
+ if (peer.getChainTipData() != null) {
+ this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
+ }
+ }
+
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
@@ -1552,6 +1572,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
+ synchronized (this.syncLock) {
+ if (peer.getChainTipData() != null) {
+ this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
+ }
+ }
+
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
index a31a1a28..a4ae921e 100644
--- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+ // Do this in a new thread so caller doesn't have to wait for computeNonce()
+ // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
+ new Thread(() -> {
+ try (final Repository threadsRepository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
- messageTransaction.computeNonce();
- messageTransaction.sign(sender);
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
- // reset repository state to prevent deadlock
- repository.discardChanges();
- ValidationResult result = messageTransaction.importAsUnconfirmed();
+ // reset repository state to prevent deadlock
+ threadsRepository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
- if (result != ValidationResult.OK) {
- LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
- return ResponseResult.NETWORK_ISSUE;
- }
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ }
+ } catch (DataException e) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
+ }
+ }, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
diff --git a/src/main/java/org/qortal/crypto/MemoryPoW.java b/src/main/java/org/qortal/crypto/MemoryPoW.java
index f27c8f7a..634b8f9b 100644
--- a/src/main/java/org/qortal/crypto/MemoryPoW.java
+++ b/src/main/java/org/qortal/crypto/MemoryPoW.java
@@ -99,6 +99,10 @@ public class MemoryPoW {
}
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
+ return verify2(data, null, workBufferLength, difficulty, nonce);
+ }
+
+ public static boolean verify2(byte[] data, long[] workBuffer, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
@@ -111,7 +115,10 @@ public class MemoryPoW {
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
- long[] workBuffer = new long[longBufferLength];
+
+ if (workBuffer == null)
+ workBuffer = new long[longBufferLength];
+
long[] state = new long[4];
long seed = 8682522807148012L;
diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java
index 060901f2..ec1139f4 100644
--- a/src/main/java/org/qortal/data/transaction/TransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/TransactionData.java
@@ -128,6 +128,10 @@ public abstract class TransactionData {
return this.txGroupId;
}
+ public void setTxGroupId(int txGroupId) {
+ this.txGroupId = txGroupId;
+ }
+
public byte[] getReference() {
return this.reference;
}
diff --git a/src/main/java/org/qortal/group/Group.java b/src/main/java/org/qortal/group/Group.java
index 1dbb18b0..465743a9 100644
--- a/src/main/java/org/qortal/group/Group.java
+++ b/src/main/java/org/qortal/group/Group.java
@@ -80,6 +80,9 @@ public class Group {
// Useful constants
public static final int NO_GROUP = 0;
+ // Null owner address corresponds with public key "11111111111111111111111111111111"
+ public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
+
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 32;
public static final int MAX_DESCRIPTION_SIZE = 128;
diff --git a/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java b/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java
index 15dc51bf..f38638c5 100644
--- a/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java
+++ b/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java
@@ -2,6 +2,7 @@ package org.qortal.transaction;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
@@ -64,15 +65,24 @@ public class AddGroupAdminTransaction extends Transaction {
Account owner = getOwner();
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
+ boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
- // Check transaction's public key matches group's current owner
- if (!owner.getAddress().equals(groupOwner))
+ // Require approval if transaction relates to a group owned by the null account
+ if (groupOwnedByNullAccount && !this.needsGroupApproval())
+ return ValidationResult.GROUP_APPROVAL_REQUIRED;
+
+ // Check transaction's public key matches group's current owner (except for groups owned by the null account)
+ if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
return ValidationResult.INVALID_GROUP_OWNER;
// Check address is a group member
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
return ValidationResult.NOT_GROUP_MEMBER;
+ // Check transaction creator is a group member
+ if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
+ return ValidationResult.NOT_GROUP_MEMBER;
+
// Check group member is not already an admin
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
return ValidationResult.ALREADY_GROUP_ADMIN;
diff --git a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java
index 3e5f1e6d..043b5423 100644
--- a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java
+++ b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java
@@ -2,6 +2,7 @@ package org.qortal.transaction;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
@@ -65,11 +66,21 @@ public class RemoveGroupAdminTransaction extends Transaction {
return ValidationResult.GROUP_DOES_NOT_EXIST;
Account owner = getOwner();
+ String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
+ boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
- // Check transaction's public key matches group's current owner
- if (!owner.getAddress().equals(groupData.getOwner()))
+ // Require approval if transaction relates to a group owned by the null account
+ if (groupOwnedByNullAccount && !this.needsGroupApproval())
+ return ValidationResult.GROUP_APPROVAL_REQUIRED;
+
+ // Check transaction's public key matches group's current owner (except for groups owned by the null account)
+ if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
return ValidationResult.INVALID_GROUP_OWNER;
+ // Check transaction creator is a group member
+ if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
+ return ValidationResult.NOT_GROUP_MEMBER;
+
Account admin = getAdmin();
// Check member is an admin
diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java
index b56d48cf..203cc342 100644
--- a/src/main/java/org/qortal/transaction/Transaction.java
+++ b/src/main/java/org/qortal/transaction/Transaction.java
@@ -1,13 +1,7 @@
package org.qortal.transaction;
import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
@@ -69,8 +63,8 @@ public abstract class Transaction {
AT(21, false),
CREATE_GROUP(22, true),
UPDATE_GROUP(23, true),
- ADD_GROUP_ADMIN(24, false),
- REMOVE_GROUP_ADMIN(25, false),
+ ADD_GROUP_ADMIN(24, true),
+ REMOVE_GROUP_ADMIN(25, true),
GROUP_BAN(26, false),
CANCEL_GROUP_BAN(27, false),
GROUP_KICK(28, false),
@@ -250,6 +244,7 @@ public abstract class Transaction {
INVALID_TIMESTAMP_SIGNATURE(95),
ADDRESS_BLOCKED(96),
NAME_BLOCKED(97),
+ GROUP_APPROVAL_REQUIRED(98),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000);
@@ -760,9 +755,13 @@ public abstract class Transaction {
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
return true; // stops tx being included in block but it will eventually expire
+ String groupOwner = this.repository.getGroupRepository().getOwner(txGroupId);
+ boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
+
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
+ // This is disabled for null-owned groups, since these require approval from other admins
PublicKeyAccount creator = this.getCreator();
- if (groupRepository.adminExists(txGroupId, creator.getAddress()))
+ if (!groupOwnedByNullAccount && groupRepository.adminExists(txGroupId, creator.getAddress()))
return false;
return true;
diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java
index 9e02a6f5..c97aa090 100644
--- a/src/main/java/org/qortal/transform/block/BlockTransformer.java
+++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java
@@ -235,7 +235,7 @@ public class BlockTransformer extends Transformer {
// Online accounts timestamp is only present if there are also signatures
onlineAccountsTimestamp = byteBuffer.getLong();
- final int signaturesByteLength = getOnlineAccountSignaturesLength(onlineAccountsSignaturesCount, onlineAccountsCount, timestamp);
+ final int signaturesByteLength = (onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH) + (onlineAccountsCount * INT_LENGTH);
if (signaturesByteLength > BlockChain.getInstance().getMaxBlockSize())
throw new TransformationException("Byte data too long for online accounts signatures");
@@ -511,16 +511,6 @@ public class BlockTransformer extends Transformer {
return nonces;
}
- public static int getOnlineAccountSignaturesLength(int onlineAccountsSignaturesCount, int onlineAccountCount, long blockTimestamp) {
- if (blockTimestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- // Once mempow is active, we expect the online account signatures to be appended with the nonce values
- return (onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH) + (onlineAccountCount * INT_LENGTH);
- }
- else {
- // Before mempow, only the online account signatures were included (which will likely be a single signature)
- return onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH;
- }
- }
public static byte[] extract(byte[] input, int pos, int length) {
byte[] output = new byte[length];
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index 2255c0a8..24cea83c 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -24,7 +24,6 @@
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
- "onlineAccountsMemoryPoWTimestamp": 1666454400000,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },
@@ -81,6 +80,7 @@
"transactionV5Timestamp": 1642176000000,
"transactionV6Timestamp": 9999999999999,
"disableReferenceTimestamp": 1655222400000,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 9999999999999
},
"genesisInfo": {
diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties
index b949ca8c..4dc7edd2 100644
--- a/src/main/resources/i18n/SysTray_de.properties
+++ b/src/main/resources/i18n/SysTray_de.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatisches Update
BLOCK_HEIGHT = height
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Build-Version
CHECK_TIME_ACCURACY = Prüfe Zeitgenauigkeit
diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties
index 204f0df2..39940be0 100644
--- a/src/main/resources/i18n/SysTray_en.properties
+++ b/src/main/resources/i18n/SysTray_en.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Auto Update
BLOCK_HEIGHT = height
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Build version
CHECK_TIME_ACCURACY = Check time accuracy
diff --git a/src/main/resources/i18n/SysTray_es.properties b/src/main/resources/i18n/SysTray_es.properties
index d4b931d4..36cbb22c 100644
--- a/src/main/resources/i18n/SysTray_es.properties
+++ b/src/main/resources/i18n/SysTray_es.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Actualización automática
BLOCK_HEIGHT = altura
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versión de compilación
CHECK_TIME_ACCURACY = Comprobar la precisión del tiempo
diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties
index bc787715..4038d615 100644
--- a/src/main/resources/i18n/SysTray_fi.properties
+++ b/src/main/resources/i18n/SysTray_fi.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automaattinen päivitys
BLOCK_HEIGHT = korkeus
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versio
CHECK_TIME_ACCURACY = Tarkista ajan tarkkuus
diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties
index 6e60713c..2e376842 100644
--- a/src/main/resources/i18n/SysTray_fr.properties
+++ b/src/main/resources/i18n/SysTray_fr.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Mise à jour automatique
BLOCK_HEIGHT = hauteur
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Numéro de version
CHECK_TIME_ACCURACY = Vérifier l'heure
diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties
index 9bc51ff5..74ab21ac 100644
--- a/src/main/resources/i18n/SysTray_hu.properties
+++ b/src/main/resources/i18n/SysTray_hu.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatikus Frissítés
BLOCK_HEIGHT = blokkmagasság
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Verzió
CHECK_TIME_ACCURACY = Óra pontosságának ellenőrzése
diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties
index bf61cc46..d966d825 100644
--- a/src/main/resources/i18n/SysTray_it.properties
+++ b/src/main/resources/i18n/SysTray_it.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Aggiornamento automatico
BLOCK_HEIGHT = altezza
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versione
CHECK_TIME_ACCURACY = Controlla la precisione dell'ora
diff --git a/src/main/resources/i18n/SysTray_ko.properties b/src/main/resources/i18n/SysTray_ko.properties
index 9773a54f..dc6cb69b 100644
--- a/src/main/resources/i18n/SysTray_ko.properties
+++ b/src/main/resources/i18n/SysTray_ko.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 자동 업데이트
BLOCK_HEIGHT = 높이
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 빌드 버전
CHECK_TIME_ACCURACY = 시간 정확도 점검
diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties
index 8a4f112b..c2acb7ce 100644
--- a/src/main/resources/i18n/SysTray_nl.properties
+++ b/src/main/resources/i18n/SysTray_nl.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatische Update
BLOCK_HEIGHT = Block hoogte
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versie nummer
CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd
diff --git a/src/main/resources/i18n/SysTray_ro.properties b/src/main/resources/i18n/SysTray_ro.properties
index 0e1aa6c6..4130bbcb 100644
--- a/src/main/resources/i18n/SysTray_ro.properties
+++ b/src/main/resources/i18n/SysTray_ro.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Actualizare automata
BLOCK_HEIGHT = dimensiune
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = versiunea compilatiei
CHECK_TIME_ACCURACY = verificare exactitate ora
diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties
index fc3d8648..ff346304 100644
--- a/src/main/resources/i18n/SysTray_ru.properties
+++ b/src/main/resources/i18n/SysTray_ru.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Автоматическое обновление
BLOCK_HEIGHT = Высота блока
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Версия сборки
CHECK_TIME_ACCURACY = Проверка точного времени
diff --git a/src/main/resources/i18n/SysTray_sv.properties b/src/main/resources/i18n/SysTray_sv.properties
index 0e74337b..96f291b5 100644
--- a/src/main/resources/i18n/SysTray_sv.properties
+++ b/src/main/resources/i18n/SysTray_sv.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatisk uppdatering
BLOCK_HEIGHT = höjd
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Byggversion
CHECK_TIME_ACCURACY = Kontrollera tidens noggrannhet
diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties
index c103d24b..d6848a7c 100644
--- a/src/main/resources/i18n/SysTray_zh_CN.properties
+++ b/src/main/resources/i18n/SysTray_zh_CN.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 自动更新
BLOCK_HEIGHT = 区块高度
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 版本
CHECK_TIME_ACCURACY = 检查时间准确性
diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties
index 5e6ccc3e..eabdbb63 100644
--- a/src/main/resources/i18n/SysTray_zh_TW.properties
+++ b/src/main/resources/i18n/SysTray_zh_TW.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 自動更新
BLOCK_HEIGHT = 區塊高度
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 版本
CHECK_TIME_ACCURACY = 檢查時間準確性
diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
index 4db8bdc7..e6a51776 100644
--- a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
+++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
@@ -102,77 +102,77 @@ public class ArbitraryServiceTests extends Common {
}
@Test
- public void testValidQortalMetadata() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Write to temp path
- Path path = Files.createTempFile("testValidQortalMetadata", null);
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateGifRepository");
path.toFile().deleteOnExit();
- Files.write(path, dataString.getBytes(), StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
+
+ // There is an index file in the root
assertEquals(ValidationResult.OK, service.validate(path));
}
@Test
- public void testQortalMetadataMissingKeys() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateMultiLayerGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Write to temp path
- Path path = Files.createTempFile("testQortalMetadataMissingKeys", null);
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateMultiLayerGifRepository");
path.toFile().deleteOnExit();
- Files.write(path, dataString.getBytes(), StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
- Service service = Service.QORTAL_METADATA;
+ Path subdirectory = Paths.get(path.toString(), "subdirectory");
+ Files.createDirectories(subdirectory);
+ Files.write(Paths.get(subdirectory.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(subdirectory.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
+
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- assertEquals(ValidationResult.MISSING_KEYS, service.validate(path));
+
+ // There is an index file in the root
+ assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
}
@Test
- public void testQortalMetadataTooLarge() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateEmptyGifRepository() throws IOException {
+ Path path = Files.createTempDirectory("testValidateEmptyGifRepository");
- // Generate some large data to go along with it
- int largeDataSize = 11*1024; // Larger than allowed 10kiB
- byte[] largeData = new byte[largeDataSize];
- new Random().nextBytes(largeData);
-
- // Write to temp path
- Path path = Files.createTempDirectory("testQortalMetadataTooLarge");
- path.toFile().deleteOnExit();
- Files.write(Paths.get(path.toString(), "data"), dataString.getBytes(), StandardOpenOption.CREATE);
- Files.write(Paths.get(path.toString(), "large_data"), largeData, StandardOpenOption.CREATE);
-
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- assertEquals(ValidationResult.EXCEEDS_SIZE_LIMIT, service.validate(path));
+
+ // There is an index file in the root
+ assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
}
@Test
- public void testMultipleFileMetadata() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateInvalidGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Generate some large data to go along with it
- int otherDataSize = 1024; // Smaller than 10kiB limit
- byte[] otherData = new byte[otherDataSize];
- new Random().nextBytes(otherData);
-
- // Write to temp path
- Path path = Files.createTempDirectory("testMultipleFileMetadata");
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateInvalidGifRepository");
path.toFile().deleteOnExit();
- Files.write(Paths.get(path.toString(), "data"), dataString.getBytes(), StandardOpenOption.CREATE);
- Files.write(Paths.get(path.toString(), "other_data"), otherData, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image3.jpg"), data, StandardOpenOption.CREATE); // Invalid extension
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- // There are multiple files, so we don't know which one to parse as JSON
- assertEquals(ValidationResult.MISSING_KEYS, service.validate(path));
+ // There is an index file in the root
+ assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
}
}
diff --git a/src/test/java/org/qortal/test/common/AccountUtils.java b/src/test/java/org/qortal/test/common/AccountUtils.java
index 0d0b6d6a..0d8baae2 100644
--- a/src/test/java/org/qortal/test/common/AccountUtils.java
+++ b/src/test/java/org/qortal/test/common/AccountUtils.java
@@ -124,8 +124,6 @@ public class AccountUtils {
long timestamp = System.currentTimeMillis();
byte[] timestampBytes = Longs.toByteArray(timestamp);
- final boolean mempowActive = timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
-
for (int a = 0; a < numAccounts; ++a) {
byte[] privateKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
SECURE_RANDOM.nextBytes(privateKey);
@@ -135,7 +133,7 @@ public class AccountUtils {
byte[] signature = signForAggregation(privateKey, timestampBytes);
- Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
+ Integer nonce = new Random().nextInt(500000);
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));
}
diff --git a/src/test/java/org/qortal/test/group/DevGroupAdminTests.java b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java
new file mode 100644
index 00000000..131359c6
--- /dev/null
+++ b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java
@@ -0,0 +1,388 @@
+package org.qortal.test.group;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.data.transaction.*;
+import org.qortal.group.Group;
+import org.qortal.group.Group.ApprovalThreshold;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.GroupUtils;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.test.common.transaction.TestTransaction;
+import org.qortal.transaction.Transaction;
+import org.qortal.transaction.Transaction.ValidationResult;
+import org.qortal.utils.Base58;
+
+import static org.junit.Assert.*;
+
+/**
+ * Dev group admin tests
+ *
+ * The dev group (ID 1) is owned by the null account with public key 11111111111111111111111111111111
+ * To regain access to otherwise blocked owner-based rules, it has different validation logic
+ * which applies to groups with this same null owner.
+ *
+ * The main difference is that approval is required for certain transaction types relating to
+ * null-owned groups. This allows existing admins to approve updates to the group (using group's
+ * approval threshold) instead of these actions being performed by the owner.
+ *
+ * Since these apply to all null-owned groups, this allows anyone to update their group to
+ * the null owner if they want to take advantage of this decentralized approval system.
+ *
+ * Currently, the affected transaction types are:
+ * - AddGroupAdminTransaction
+ * - RemoveGroupAdminTransaction
+ *
+ * This same approach could ultimately be applied to other group transactions too.
+ */
+public class DevGroupAdminTests extends Common {
+
+ private static final int DEV_GROUP_ID = 1;
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings();
+ }
+
+ @After
+ public void afterTest() throws DataException {
+ Common.orphanCheck();
+ }
+
+ @Test
+ public void testGroupKickMember() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ // Dev group
+ int groupId = DEV_GROUP_ID;
+
+ // Confirm Bob is not a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Attempt to kick Bob
+ ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Alice to invite Bob, as it's a closed group
+ groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
+
+ // Bob to join
+ joinGroup(repository, bob, groupId);
+
+ // Confirm Bob now a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+
+ // Attempt to kick Bob
+ result = groupKick(repository, alice, groupId, bob.getAddress());
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Confirm Bob no longer a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Confirm Bob now a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+ }
+ }
+
+ @Test
+ public void testGroupKickAdmin() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ // Dev group
+ int groupId = DEV_GROUP_ID;
+
+ // Confirm Bob is not a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Alice to invite Bob, as it's a closed group
+ groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
+
+ // Bob to join
+ joinGroup(repository, bob, groupId);
+
+ // Confirm Bob now a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+
+ // Promote Bob to admin
+ TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
+
+ // Confirm transaction needs approval, and hasn't been approved
+ Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
+ assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
+
+ // Have Alice approve Bob's approval-needed transaction
+ GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
+
+ // Mint a block so that the transaction becomes approved
+ BlockUtils.mintBlock(repository);
+
+ // Confirm transaction is approved
+ approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
+ assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
+
+ // Confirm Bob is now admin
+ assertTrue(isAdmin(repository, bob.getAddress(), groupId));
+
+ // Attempt to kick Bob
+ ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
+ // Shouldn't be allowed
+ assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
+
+ // Confirm Bob is still a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+
+ // Confirm Bob still an admin
+ assertTrue(isAdmin(repository, bob.getAddress(), groupId));
+
+ // Orphan last block
+ BlockUtils.orphanLastBlock(repository);
+
+ // Confirm Bob no longer an admin (ADD_GROUP_ADMIN no longer approved)
+ assertFalse(isAdmin(repository, bob.getAddress(), groupId));
+
+ // Have Alice try to kick herself!
+ result = groupKick(repository, alice, groupId, alice.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Have Bob try to kick Alice
+ result = groupKick(repository, bob, groupId, alice.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+ }
+ }
+
+ @Test
+ public void testGroupBanMember() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ // Dev group
+ int groupId = DEV_GROUP_ID;
+
+ // Confirm Bob is not a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Attempt to cancel non-existent Bob ban
+ ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Attempt to ban Bob
+ result = groupBan(repository, alice, groupId, bob.getAddress());
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Bob attempts to rejoin
+ result = joinGroup(repository, bob, groupId);
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Orphan last block (Bob ban)
+ BlockUtils.orphanLastBlock(repository);
+ // Delete unconfirmed group-ban transaction
+ TransactionUtils.deleteUnconfirmedTransactions(repository);
+
+ // Confirm Bob is not a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Alice to invite Bob, as it's a closed group
+ groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
+
+ // Bob to join
+ result = joinGroup(repository, bob, groupId);
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Confirm Bob now a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+
+ // Attempt to ban Bob
+ result = groupBan(repository, alice, groupId, bob.getAddress());
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Confirm Bob no longer a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Bob attempts to rejoin
+ result = joinGroup(repository, bob, groupId);
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Cancel Bob's ban
+ result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Bob attempts to rejoin
+ result = joinGroup(repository, bob, groupId);
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Orphan last block (Bob join)
+ BlockUtils.orphanLastBlock(repository);
+ // Delete unconfirmed join-group transaction
+ TransactionUtils.deleteUnconfirmedTransactions(repository);
+
+ // Orphan last block (Cancel Bob ban)
+ BlockUtils.orphanLastBlock(repository);
+ // Delete unconfirmed cancel-ban transaction
+ TransactionUtils.deleteUnconfirmedTransactions(repository);
+
+ // Bob attempts to rejoin
+ result = joinGroup(repository, bob, groupId);
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Orphan last block (Bob ban)
+ BlockUtils.orphanLastBlock(repository);
+ // Delete unconfirmed group-ban transaction
+ TransactionUtils.deleteUnconfirmedTransactions(repository);
+
+ // Confirm Bob now a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+ }
+ }
+
+ @Test
+ public void testGroupBanAdmin() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
+ PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
+
+ // Dev group
+ int groupId = DEV_GROUP_ID;
+
+ // Confirm Bob is not a member
+ assertFalse(isMember(repository, bob.getAddress(), groupId));
+
+ // Alice to invite Bob, as it's a closed group
+ groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
+
+ // Bob to join
+ ValidationResult result = joinGroup(repository, bob, groupId);
+ // Should be OK
+ assertEquals(ValidationResult.OK, result);
+
+ // Promote Bob to admin
+ TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
+
+ // Confirm transaction needs approval, and hasn't been approved
+ Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
+ assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
+
+ // Have Alice approve Bob's approval-needed transaction
+ GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
+
+ // Mint a block so that the transaction becomes approved
+ BlockUtils.mintBlock(repository);
+
+ // Confirm transaction is approved
+ approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
+ assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
+
+ // Confirm Bob is now admin
+ assertTrue(isAdmin(repository, bob.getAddress(), groupId));
+
+ // Attempt to ban Bob
+ result = groupBan(repository, alice, groupId, bob.getAddress());
+ // .. but we can't, because Bob is an admin and the group has no owner
+ assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
+
+ // Confirm Bob still a member
+ assertTrue(isMember(repository, bob.getAddress(), groupId));
+
+ // ... and still an admin
+ assertTrue(isAdmin(repository, bob.getAddress(), groupId));
+
+ // Have Alice try to ban herself!
+ result = groupBan(repository, alice, groupId, alice.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+
+ // Have Bob try to ban Alice
+ result = groupBan(repository, bob, groupId, alice.getAddress());
+ // Should NOT be OK
+ assertNotSame(ValidationResult.OK, result);
+ }
+ }
+
+
+ private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
+ JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
+ ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner);
+
+ if (result == ValidationResult.OK)
+ BlockUtils.mintBlock(repository);
+
+ return result;
+ }
+
+ private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
+ GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
+ TransactionUtils.signAndMint(repository, transactionData, admin);
+ }
+
+ private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
+ GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing");
+ ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
+
+ if (result == ValidationResult.OK)
+ BlockUtils.mintBlock(repository);
+
+ return result;
+ }
+
+ private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
+ GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
+ ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
+
+ if (result == ValidationResult.OK)
+ BlockUtils.mintBlock(repository);
+
+ return result;
+ }
+
+ private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
+ CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member);
+ ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
+
+ if (result == ValidationResult.OK)
+ BlockUtils.mintBlock(repository);
+
+ return result;
+ }
+
+ private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException {
+ AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member);
+ transactionData.setTxGroupId(groupId);
+ TransactionUtils.signAndMint(repository, transactionData, owner);
+ return transactionData;
+ }
+
+ private boolean isMember(Repository repository, String address, int groupId) throws DataException {
+ return repository.getGroupRepository().memberExists(groupId, address);
+ }
+
+ private boolean isAdmin(Repository repository, String address, int groupId) throws DataException {
+ return repository.getGroupRepository().adminExists(groupId, address);
+ }
+
+}
diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json
index 028519f8..940d8ec6 100644
--- a/src/test/resources/test-chain-v2-block-timestamps.json
+++ b/src/test/resources/test-chain-v2-block-timestamps.json
@@ -70,6 +70,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 9999999999999,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json
index 541ce779..66e31047 100644
--- a/src/test/resources/test-chain-v2-disable-reference.json
+++ b/src/test/resources/test-chain-v2-disable-reference.json
@@ -73,6 +73,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 0,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json
index 7392d5ae..020e7be5 100644
--- a/src/test/resources/test-chain-v2-founder-rewards.json
+++ b/src/test/resources/test-chain-v2-founder-rewards.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json
index ed46cd56..8b5e94cb 100644
--- a/src/test/resources/test-chain-v2-leftover-reward.json
+++ b/src/test/resources/test-chain-v2-leftover-reward.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json
index a705016c..60174b5d 100644
--- a/src/test/resources/test-chain-v2-minting.json
+++ b/src/test/resources/test-chain-v2-minting.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json
index 880af61b..ea8eb113 100644
--- a/src/test/resources/test-chain-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json
index 27451201..70203336 100644
--- a/src/test/resources/test-chain-v2-qora-holder-reduction.json
+++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json
@@ -75,6 +75,7 @@
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
"aggregateSignatureTimestamp": 0,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json
index fbc58d80..8651adbc 100644
--- a/src/test/resources/test-chain-v2-qora-holder.json
+++ b/src/test/resources/test-chain-v2-qora-holder.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json
index 0f7adf6f..0e6bd584 100644
--- a/src/test/resources/test-chain-v2-reward-levels.json
+++ b/src/test/resources/test-chain-v2-reward-levels.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json
index 802ad8fe..8a99fb84 100644
--- a/src/test/resources/test-chain-v2-reward-scaling.json
+++ b/src/test/resources/test-chain-v2-reward-scaling.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json
index 2becc875..57207482 100644
--- a/src/test/resources/test-chain-v2-reward-shares.json
+++ b/src/test/resources/test-chain-v2-reward-shares.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json
index c3b740ff..ea91ea1e 100644
--- a/src/test/resources/test-chain-v2.json
+++ b/src/test/resources/test-chain-v2.json
@@ -74,6 +74,7 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"chatReferenceTimestamp": 0
},
"genesisInfo": {
@@ -91,6 +92,8 @@
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
+ { "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
+
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
diff --git a/tools/approve-dev-transaction.sh b/tools/approve-dev-transaction.sh
new file mode 100755
index 00000000..6b611b59
--- /dev/null
+++ b/tools/approve-dev-transaction.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+
+port=12391
+if [ $# -gt 0 -a "$1" = "-t" ]; then
+ port=62391
+fi
+
+printf "Searching for auto-update transactions to approve...\n";
+
+tx=$( curl --silent --url "http://localhost:${port}/transactions/search?txGroupId=1&txType=ADD_GROUP_ADMIN&txType=REMOVE_GROUP_ADMIN&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
+if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
+ true
+else
+ echo "Can't find any pending transactions"
+ exit
+fi
+
+sig=$( perl -n -e 'print $1 if m/"signature":"(\w+)"/' <<< "${tx}" )
+if [ -z "${sig}" ]; then
+ printf "Can't find transaction signature in JSON:\n%s\n" "${tx}"
+ exit
+fi
+
+printf "Found transaction %s\n" $sig;
+
+printf "\nPaste your dev account private key:\n";
+IFS=
+read -s privkey
+printf "\n"
+
+# Convert to public key
+pubkey=$( curl --silent --url "http://localhost:${port}/utils/publickey" --data @- <<< "${privkey}" );
+if egrep -v --silent '^\w{44,46}$' <<< "${pubkey}"; then
+ printf "Invalid response from API - was your private key correct?\n%s\n" "${pubkey}"
+ exit
+fi
+printf "Your public key: %s\n" ${pubkey}
+
+# Convert to address
+address=$( curl --silent --url "http://localhost:${port}/addresses/convert/${pubkey}" );
+printf "Your address: %s\n" ${address}
+
+# Grab last reference
+lastref=$( curl --silent --url "http://localhost:${port}/addresses/lastreference/{$address}" );
+printf "Your last reference: %s\n" ${lastref}
+
+# Build GROUP_APPROVAL transaction
+timestamp=$( date +%s )000
+tx_json=$( cat < 0; seconds--)); do
+ if [ "${seconds}" = "1" ]; then
+ plural=""
+ fi
+ printf "\rBroadcasting in %d second%s...(CTRL-C) to abort " $seconds $plural
+ sleep 1
+done
+
+printf "\rBroadcasting signed GROUP_APPROVAL transaction... \n"
+result=$( curl --silent --url "http://localhost:${port}/transactions/process" --data @- <<< "${signed_tx}" )
+printf "API response:\n%s\n" "${result}"
diff --git a/tools/publish-auto-update-v5.pl b/tools/publish-auto-update-v5.pl
index aad49d4e..f97fe115 100755
--- a/tools/publish-auto-update-v5.pl
+++ b/tools/publish-auto-update-v5.pl
@@ -4,6 +4,7 @@ use strict;
use warnings;
use POSIX;
use Getopt::Std;
+use File::Slurp;
sub usage() {
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
@@ -34,6 +35,8 @@ while () {
}
close(POM);
+my $apikey = read_file('apikey.txt');
+
# Do we need to determine commit hash?
unless ($commit_hash) {
# determine git branch
@@ -124,7 +127,7 @@ my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
-my $computed_tx = `curl --silent -X POST --url http://localhost:${port}/arbitrary/compute -d "${raw_tx}"`;
+my $computed_tx = `curl --silent -X POST --url http://localhost:${port}/arbitrary/compute -H "X-API-KEY: ${apikey}" -d "${raw_tx}"`;
die("Can't compute nonce for transaction:\n$computed_tx\n") unless $computed_tx =~ m/^\w{300,320}$/; # Roughly 300 to 320 base58 chars
printf "\nRaw computed transaction (base58):\n%s\n", $computed_tx;
diff --git a/tools/tx.pl b/tools/tx.pl
index db6958e2..fe3cd872 100755
--- a/tools/tx.pl
+++ b/tools/tx.pl
@@ -71,9 +71,14 @@ our %TRANSACTION_TYPES = (
},
add_group_admin => {
url => 'groups/addadmin',
- required => [qw(groupId member)],
+ required => [qw(groupId txGroupId member)],
key_name => 'ownerPublicKey',
},
+ remove_group_admin => {
+ url => 'groups/removeadmin',
+ required => [qw(groupId txGroupId admin)],
+ key_name => 'ownerPublicKey',
+ },
group_approval => {
url => 'groups/approval',
required => [qw(pendingSignature approval)],