forked from Qortal/qortal
Merge branch 'master' into chat-reference
# Conflicts: # src/main/java/org/qortal/block/BlockChain.java # src/main/resources/blockchain.json # src/test/resources/test-chain-v2-block-timestamps.json # src/test/resources/test-chain-v2-disable-reference.json # src/test/resources/test-chain-v2-founder-rewards.json # src/test/resources/test-chain-v2-leftover-reward.json # src/test/resources/test-chain-v2-minting.json # src/test/resources/test-chain-v2-qora-holder-extremes.json # src/test/resources/test-chain-v2-qora-holder-reduction.json # src/test/resources/test-chain-v2-qora-holder.json # src/test/resources/test-chain-v2-reward-levels.json # src/test/resources/test-chain-v2-reward-scaling.json # src/test/resources/test-chain-v2-reward-shares.json # src/test/resources/test-chain-v2.json
This commit is contained in:
commit
2b6ae57a27
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@ -3,14 +3,15 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.8</release>
|
||||
<release>1.4.0</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
<version>1.4.0</version>
|
||||
</versions>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
<lastUpdated>20221105114346</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
4
pom.xml
4
pom.xml
@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.6.4</version>
|
||||
<version>3.7.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
@ -11,7 +11,7 @@
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||
<ciyam-at.version>1.4.0</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<commons-io.version>2.6</commons-io.version>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<OnlineAccountData> 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<Integer> 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<Integer> 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<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
|
||||
List<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
|
||||
|
||||
// Build block's view of online accounts (without signatures, as we don't need them here)
|
||||
Set<OnlineAccountData> 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<OnlineAccountData> 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<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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<OnlineAccountData> 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<OnlineAccountData> 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())
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,8 @@ AUTO_UPDATE = 자동 업데이트
|
||||
|
||||
BLOCK_HEIGHT = 높이
|
||||
|
||||
BLOCKS_REMAINING = blocks remaining
|
||||
|
||||
BUILD_VERSION = 빌드 버전
|
||||
|
||||
CHECK_TIME_ACCURACY = 시간 정확도 점검
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,8 @@ AUTO_UPDATE = Автоматическое обновление
|
||||
|
||||
BLOCK_HEIGHT = Высота блока
|
||||
|
||||
BLOCKS_REMAINING = blocks remaining
|
||||
|
||||
BUILD_VERSION = Версия сборки
|
||||
|
||||
CHECK_TIME_ACCURACY = Проверка точного времени
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,8 @@ AUTO_UPDATE = 自动更新
|
||||
|
||||
BLOCK_HEIGHT = 区块高度
|
||||
|
||||
BLOCKS_REMAINING = blocks remaining
|
||||
|
||||
BUILD_VERSION = 版本
|
||||
|
||||
CHECK_TIME_ACCURACY = 检查时间准确性
|
||||
|
@ -7,6 +7,8 @@ AUTO_UPDATE = 自動更新
|
||||
|
||||
BLOCK_HEIGHT = 區塊高度
|
||||
|
||||
BLOCKS_REMAINING = blocks remaining
|
||||
|
||||
BUILD_VERSION = 版本
|
||||
|
||||
CHECK_TIME_ACCURACY = 檢查時間準確性
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -70,6 +70,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 9999999999999,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -73,6 +73,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 0,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -75,6 +75,7 @@
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"aggregateSignatureTimestamp": 0,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -74,6 +74,7 @@
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
|
@ -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 },
|
||||
|
97
tools/approve-dev-transaction.sh
Executable file
97
tools/approve-dev-transaction.sh
Executable file
@ -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 <<TX_END
|
||||
{
|
||||
"timestamp": ${timestamp},
|
||||
"reference": "${lastref}",
|
||||
"fee": 0.001,
|
||||
"txGroupId": 0,
|
||||
"adminPublicKey": "${pubkey}",
|
||||
"pendingSignature": "${sig}",
|
||||
"approval": true
|
||||
}
|
||||
TX_END
|
||||
)
|
||||
|
||||
raw_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:${port}/groups/approval" --data @- <<< "${tx_json}" )
|
||||
if egrep -v --silent '^\w{100,}' <<< "${raw_tx}"; then
|
||||
printf "Building GROUP_APPROVAL transaction failed:\n%s\n" "${raw_tx}"
|
||||
exit
|
||||
fi
|
||||
printf "\nRaw approval tx:\n%s\n" ${raw_tx}
|
||||
|
||||
# sign
|
||||
sign_json=$( cat <<SIGN_END
|
||||
{
|
||||
"privateKey": "${privkey}",
|
||||
"transactionBytes": "${raw_tx}"
|
||||
}
|
||||
SIGN_END
|
||||
)
|
||||
signed_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:${port}/transactions/sign" --data @- <<< "${sign_json}" )
|
||||
printf "\nSigned tx:\n%s\n" ${signed_tx}
|
||||
if egrep -v --silent '^\w{100,}' <<< "${signed_tx}"; then
|
||||
printf "Signing GROUP_APPROVAL transaction failed:\n%s\n" "${signed_tx}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# ready to publish?
|
||||
plural="s"
|
||||
printf "\n"
|
||||
for ((seconds = 5; seconds > 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}"
|
@ -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 (<POM>) {
|
||||
}
|
||||
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;
|
||||
|
||||
|
@ -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)],
|
||||
|
Loading…
Reference in New Issue
Block a user