mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 13:41:23 +00:00
Compare commits
40 Commits
share-bin-
...
online-acc
Author | SHA1 | Date | |
---|---|---|---|
|
1e4281996b | ||
|
9658f0cdd4 | ||
|
b23500fdd0 | ||
|
a1365e57d8 | ||
|
d8ca3a455d | ||
|
19197812d3 | ||
|
b17e96e121 | ||
|
2d0b035f98 | ||
|
075385d3ff | ||
|
6ed8250301 | ||
|
0ddb7f8f17 | ||
|
06b5d5f1d0 | ||
|
d6d2641cad | ||
|
e71f22fd2c | ||
|
c996633732 | ||
|
55f973af3c | ||
|
fe9744eec6 | ||
|
410fa59430 | ||
|
522ae2bce7 | ||
|
a6e79947b8 | ||
|
fcd0d71cb6 | ||
|
275bee62d9 | ||
|
fbcc870d36 | ||
|
020e59743b | ||
|
0904de3f71 | ||
|
fe2c63e8e4 | ||
|
a3febdf00e | ||
|
4ca174fa0b | ||
|
294582f136 | ||
|
215800fb67 | ||
|
b05d428b2e | ||
|
d2adadb600 | ||
|
8e8c0b3fc5 | ||
|
65d63487f3 | ||
|
ff78606153 | ||
|
80188629df | ||
|
f77093731c | ||
|
ef51cf5702 | ||
|
0c3988202e | ||
|
f7dabcaeb0 |
@@ -17,10 +17,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{B786B6C1-86FA-4917-BAF9-7C9D10959D66} 1049:{60881A63-53FC-4DBE-AF3B-0568F55D2150} 2052:{108D1268-8111-49B9-B768-CC0A0A0CEDE1} 2057:{46DB692E-D942-40D5-B32E-FB94458478BF} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{E5597539-098E-4BA6-99DF-4D22018BC0D3} 1049:{2B5E55A2-142A-4BED-B3B9-5657162282B7} 2052:{6F19171F-4743-4127-B191-AAFA3FA885D2} 2057:{A1B3108D-EC5D-47A1-AEE4-DBD956E682FB} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.4.0" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.4.3" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -212,7 +212,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{D57E945C-0FFB-447C-ADF7-2253CEBF4C0C}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{F17029E8-CCC4-456D-B4AC-1854C81C46B6}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||
|
2
pom.xml
2
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<version>3.4.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
|
@@ -1220,6 +1220,7 @@ public class Block {
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("DataException during transaction validation", e);
|
||||
return ValidationResult.TRANSACTION_INVALID;
|
||||
} finally {
|
||||
// Rollback repository changes made by test-processing transactions above
|
||||
|
@@ -185,6 +185,14 @@ public class BlockChain {
|
||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||
private long onlineAccountSignaturesMaxLifetime;
|
||||
|
||||
/** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use
|
||||
* 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;
|
||||
@@ -340,6 +348,15 @@ public class BlockChain {
|
||||
return this.maxBlockSize;
|
||||
}
|
||||
|
||||
// Online accounts
|
||||
public long getOnlineAccountsModulusV2Timestamp() {
|
||||
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;
|
||||
|
@@ -90,37 +90,40 @@ public class BlockMinter extends Thread {
|
||||
|
||||
List<Block> newBlocks = new ArrayList<>();
|
||||
|
||||
// Flags for tracking change in whether minting is possible,
|
||||
// so we can notify Controller, and further update SysTray, etc.
|
||||
boolean isMintingPossible = false;
|
||||
boolean wasMintingPossible = isMintingPossible;
|
||||
while (running) {
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
|
||||
wasMintingPossible = isMintingPossible;
|
||||
// Flags for tracking change in whether minting is possible,
|
||||
// so we can notify Controller, and further update SysTray, etc.
|
||||
boolean isMintingPossible = false;
|
||||
boolean wasMintingPossible = isMintingPossible;
|
||||
while (running) {
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
try {
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
wasMintingPossible = isMintingPossible;
|
||||
|
||||
isMintingPossible = false;
|
||||
try {
|
||||
// Free up any repository locks
|
||||
repository.discardChanges();
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
continue;
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
isMintingPossible = false;
|
||||
|
||||
// No online accounts for current timestamp? (e.g. during startup)
|
||||
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||
continue;
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
continue;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
|
||||
// No online accounts for current timestamp? (e.g. during startup)
|
||||
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||
continue;
|
||||
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
@@ -198,10 +201,6 @@ public class BlockMinter extends Thread {
|
||||
// so go ahead and mint a block if possible.
|
||||
isMintingPossible = true;
|
||||
|
||||
// Reattach newBlocks to new repository handle
|
||||
for (Block newBlock : newBlocks)
|
||||
newBlock.setRepository(repository);
|
||||
|
||||
// Check blockchain hasn't changed
|
||||
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlockData = lastBlockData;
|
||||
@@ -439,13 +438,13 @@ public class BlockMinter extends Thread {
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter", e);
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1245,6 +1245,10 @@ public class Controller extends Thread {
|
||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case ONLINE_ACCOUNTS_V3:
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ARBITRARY_DATA:
|
||||
// Not currently supported
|
||||
break;
|
||||
|
@@ -9,6 +9,7 @@ import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -19,10 +20,13 @@ import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -36,7 +40,8 @@ public class OnlineAccountsManager {
|
||||
/**
|
||||
* How long online accounts signatures last before they expire.
|
||||
*/
|
||||
public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
|
||||
private static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L;
|
||||
private static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L;
|
||||
|
||||
/**
|
||||
* How many 'current' timestamp-sets of online accounts we cache.
|
||||
@@ -51,11 +56,15 @@ public class OnlineAccountsManager {
|
||||
private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; //ms
|
||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL = 60 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 15 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 5 * 1000L; // ms
|
||||
|
||||
private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; // v3.2.0
|
||||
private static final long ONLINE_ACCOUNTS_V3_PEER_VERSION = 0x0300040000L; // v3.4.0
|
||||
|
||||
// MemoryPoW
|
||||
public final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
|
||||
public int POW_DIFFICULTY = 18; // leading zero bits
|
||||
|
||||
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, new NamedThreadFactory("OnlineAccounts"));
|
||||
private volatile boolean isStopping = false;
|
||||
|
||||
@@ -78,12 +87,24 @@ public class OnlineAccountsManager {
|
||||
|
||||
private boolean hasOurOnlineAccounts = false;
|
||||
|
||||
public static long getOnlineTimestampModulus() {
|
||||
Long now = NTP.getTime();
|
||||
if (now != null && now >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) {
|
||||
return ONLINE_TIMESTAMP_MODULUS_V2;
|
||||
}
|
||||
return ONLINE_TIMESTAMP_MODULUS_V1;
|
||||
}
|
||||
public static Long getCurrentOnlineAccountTimestamp() {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return null;
|
||||
|
||||
return (now / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
|
||||
long onlineTimestampModulus = getOnlineTimestampModulus();
|
||||
return (now / onlineTimestampModulus) * onlineTimestampModulus;
|
||||
}
|
||||
|
||||
public static long toOnlineAccountTimestamp(long timestamp) {
|
||||
return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
|
||||
}
|
||||
|
||||
private OnlineAccountsManager() {
|
||||
@@ -131,6 +152,7 @@ public class OnlineAccountsManager {
|
||||
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
|
||||
final boolean mempowActive = onlineAccountsTimestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
|
||||
|
||||
Set<OnlineAccountData> replacementAccounts = new HashSet<>();
|
||||
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
|
||||
@@ -141,7 +163,9 @@ public class OnlineAccountsManager {
|
||||
: onlineAccount.sign(timestampBytes);
|
||||
byte[] publicKey = onlineAccount.getPublicKey();
|
||||
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
|
||||
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
|
||||
replacementAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
|
||||
@@ -181,6 +205,52 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if supplied onlineAccountData is superior (i.e. has a nonce value) than existing record.
|
||||
* Two entries are considered equal even if the nonce differs, to prevent multiple variations
|
||||
* co-existing. For this reason, we need to be able to check if a new OnlineAccountData entry should
|
||||
* replace the existing one, which may be missing the nonce.
|
||||
* @param onlineAccountData
|
||||
* @return true if supplied data is superior to existing entry
|
||||
*/
|
||||
private boolean isOnlineAccountsDataSuperior(OnlineAccountData onlineAccountData) {
|
||||
if (onlineAccountData.getNonce() == null || onlineAccountData.getNonce() < 0) {
|
||||
// New online account data has no usable nonce value, so it won't be better than anything we already have
|
||||
return false;
|
||||
}
|
||||
|
||||
// New online account data has a nonce value, so check if there is any existing data to compare against
|
||||
Set<OnlineAccountData> existingOnlineAccountsForTimestamp = this.currentOnlineAccounts.get(onlineAccountData.getTimestamp());
|
||||
if (existingOnlineAccountsForTimestamp == null) {
|
||||
// No existing online accounts data with this timestamp yet
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a duplicate entry exists
|
||||
OnlineAccountData existingOnlineAccountData = null;
|
||||
for (OnlineAccountData existingAccount : existingOnlineAccountsForTimestamp) {
|
||||
if (existingAccount.equals(onlineAccountData)) {
|
||||
// Found existing online account data
|
||||
existingOnlineAccountData = existingAccount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingOnlineAccountData == null) {
|
||||
// No existing online accounts data, so nothing to compare
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingOnlineAccountData.getNonce() == null || existingOnlineAccountData.getNonce() < 0) {
|
||||
// Existing data has no usable nonce value(s) so we want to replace it with the new one
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both new and old data have nonce values so the new data isn't considered superior
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Utilities
|
||||
|
||||
public static byte[] xorByteArrayInPlace(byte[] inplaceArray, byte[] otherArray) {
|
||||
@@ -203,11 +273,17 @@ public class OnlineAccountsManager {
|
||||
long onlineAccountTimestamp = onlineAccountData.getTimestamp();
|
||||
|
||||
// Check timestamp is 'recent' here
|
||||
if (Math.abs(onlineAccountTimestamp - now) > ONLINE_TIMESTAMP_MODULUS * 2) {
|
||||
if (Math.abs(onlineAccountTimestamp - now) > getOnlineTimestampModulus() * 2) {
|
||||
LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check timestamp is a multiple of online timestamp modulus
|
||||
if (onlineAccountTimestamp % getOnlineTimestampModulus() != 0) {
|
||||
LOGGER.trace(() -> String.format("Rejecting online account %s with invalid timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
|
||||
boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()
|
||||
@@ -233,6 +309,14 @@ public class OnlineAccountsManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate mempow if feature trigger is active
|
||||
if (now >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -290,6 +374,12 @@ public class OnlineAccountsManager {
|
||||
long onlineAccountTimestamp = onlineAccountData.getTimestamp();
|
||||
|
||||
Set<OnlineAccountData> onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountTimestamp, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
boolean isSuperiorEntry = isOnlineAccountsDataSuperior(onlineAccountData);
|
||||
if (isSuperiorEntry)
|
||||
// Remove existing inferior entry so it can be re-added below (it's likely the existing copy is missing a nonce value)
|
||||
onlineAccounts.remove(onlineAccountData);
|
||||
|
||||
boolean isNewEntry = onlineAccounts.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
@@ -308,7 +398,7 @@ public class OnlineAccountsManager {
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * ONLINE_TIMESTAMP_MODULUS;
|
||||
final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * getOnlineTimestampModulus();
|
||||
this.currentOnlineAccounts.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
|
||||
this.currentOnlineAccountsHashes.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
|
||||
}
|
||||
@@ -377,13 +467,26 @@ public class OnlineAccountsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'next' timestamp (prioritize this as it's the most important)
|
||||
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(now) + getOnlineTimestampModulus();
|
||||
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
|
||||
computeOurAccountsForTimestamp(onlineAccountsTimestamp);
|
||||
}
|
||||
|
||||
private boolean computeOurAccountsForTimestamp(long onlineAccountsTimestamp) {
|
||||
List<MintingAccountData> mintingAccounts;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||
|
||||
// We have no accounts to send
|
||||
if (mintingAccounts.isEmpty())
|
||||
return;
|
||||
return false;
|
||||
|
||||
// Only active reward-shares allowed
|
||||
Iterator<MintingAccountData> iterator = mintingAccounts.iterator();
|
||||
@@ -406,7 +509,7 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
|
||||
@@ -418,13 +521,46 @@ public class OnlineAccountsManager {
|
||||
byte[] privateKey = mintingAccountData.getPrivateKey();
|
||||
byte[] publicKey = Crypto.toPublicKey(privateKey);
|
||||
|
||||
// Generate bytes for mempow
|
||||
byte[] mempowBytes;
|
||||
try {
|
||||
mempowBytes = this.getMemoryPoWBytes(publicKey, onlineAccountsTimestamp);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.info("Unable to create bytes for MemoryPoW. Moving on to next account...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Send -1 if we haven't computed a nonce due to feature trigger timestamp
|
||||
nonce = -1;
|
||||
}
|
||||
|
||||
byte[] signature = useAggregateCompatibleSignature
|
||||
? Qortal25519Extras.signForAggregation(privateKey, timestampBytes)
|
||||
: Crypto.sign(privateKey, timestampBytes);
|
||||
|
||||
// Our account is online
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
|
||||
|
||||
// Make sure to verify before adding
|
||||
if (verifyMemoryPoW(ourOnlineAccountData, NTP.getTime())) {
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasOurOnlineAccounts = !ourOnlineAccounts.isEmpty();
|
||||
@@ -432,14 +568,14 @@ public class OnlineAccountsManager {
|
||||
boolean hasInfoChanged = addAccounts(ourOnlineAccounts);
|
||||
|
||||
if (!hasInfoChanged)
|
||||
return;
|
||||
return false;
|
||||
|
||||
Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts);
|
||||
Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts);
|
||||
Message messageV3 = new OnlineAccountsV2Message(ourOnlineAccounts); // TODO: V3 message
|
||||
Message messageV3 = new OnlineAccountsV3Message(ourOnlineAccounts);
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
|
||||
peer.getPeersVersion() >= OnlineAccountsV3Message.MIN_PEER_VERSION
|
||||
? messageV3
|
||||
: peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION
|
||||
? messageV2
|
||||
@@ -447,8 +583,77 @@ public class OnlineAccountsManager {
|
||||
);
|
||||
|
||||
LOGGER.debug("Broadcasted {} online account{} with timestamp {}", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MemoryPoW
|
||||
|
||||
private boolean isMemoryPoWActive(Long timestamp) {
|
||||
if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp() || Settings.getInstance().isOnlineAccountsMemPoWEnabled()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private byte[] getMemoryPoWBytes(byte[] publicKey, long onlineAccountsTimestamp) throws IOException {
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
outputStream.write(publicKey);
|
||||
outputStream.write(timestampBytes);
|
||||
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private Integer computeMemoryPoW(byte[] bytes, byte[] publicKey, long onlineAccountsTimestamp) throws TimeoutException {
|
||||
if (!isMemoryPoWActive(NTP.getTime())) {
|
||||
LOGGER.info("Mempow start timestamp not yet reached, and onlineAccountsMemPoWEnabled not enabled in settings");
|
||||
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
|
||||
Long startTime = NTP.getTime();
|
||||
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(startTime) + getOnlineTimestampModulus();
|
||||
long timeUntilNextTimestamp = nextOnlineAccountsTimestamp - startTime;
|
||||
|
||||
Integer nonce = MemoryPoW.compute2(bytes, POW_BUFFER_SIZE, POW_DIFFICULTY, timeUntilNextTimestamp);
|
||||
|
||||
double totalSeconds = (NTP.getTime() - startTime) / 1000.0f;
|
||||
int minutes = (int) ((totalSeconds % 3600) / 60);
|
||||
int seconds = (int) (totalSeconds % 60);
|
||||
double hashRate = nonce / totalSeconds;
|
||||
|
||||
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));
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, Long timestamp) {
|
||||
if (!isMemoryPoWActive(timestamp)) {
|
||||
// Not active yet, so treat it as valid
|
||||
return true;
|
||||
}
|
||||
|
||||
int nonce = onlineAccountData.getNonce();
|
||||
|
||||
byte[] mempowBytes;
|
||||
try {
|
||||
mempowBytes = this.getMemoryPoWBytes(onlineAccountData.getPublicKey(), onlineAccountData.getTimestamp());
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the nonce
|
||||
return MemoryPoW.verify2(mempowBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether online accounts manager has any online accounts with timestamp recent enough to be considered currently online.
|
||||
*/
|
||||
@@ -706,9 +911,32 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsV2Message(outgoingOnlineAccounts); // TODO: V3 message
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
peer.sendMessage(
|
||||
peer.getPeersVersion() >= OnlineAccountsV3Message.MIN_PEER_VERSION ?
|
||||
new OnlineAccountsV3Message(outgoingOnlineAccounts) :
|
||||
new OnlineAccountsV2Message(outgoingOnlineAccounts)
|
||||
);
|
||||
|
||||
LOGGER.debug("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsV3Message(Peer peer, Message message) {
|
||||
OnlineAccountsV3Message onlineAccountsMessage = (OnlineAccountsV3Message) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
|
||||
|
||||
int importCount = 0;
|
||||
|
||||
// Add any online accounts to the queue that aren't already present
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
|
||||
boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
importCount++;
|
||||
}
|
||||
|
||||
if (importCount > 0)
|
||||
LOGGER.debug("Added {} online accounts to queue", importCount);
|
||||
}
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.qortal.api.model.SimpleForeignTransaction;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
@@ -61,11 +62,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
/** How many wallet keys to generate in each batch. */
|
||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
|
||||
|
||||
/** How many wallet keys to generate when using bitcoinj as the data provider.
|
||||
* We must use a higher value here since we are unable to request multiple batches of keys.
|
||||
* Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */
|
||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50;
|
||||
|
||||
/** Byte offset into raw block headers to block timestamp. */
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
|
||||
@@ -409,9 +405,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||
Set<String> keySet = new HashSet<>();
|
||||
|
||||
// Set the number of consecutive empty batches required before giving up
|
||||
final int numberOfAdditionalBatchesToSearch = 7;
|
||||
|
||||
int unusedCounter = 0;
|
||||
int ki = 0;
|
||||
do {
|
||||
@@ -438,12 +431,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
// No transactions
|
||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||
// ... and we've hit our search limit
|
||||
break;
|
||||
}
|
||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||
unusedCounter++;
|
||||
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||
} else {
|
||||
// Some keys in this batch were used, so reset the counter
|
||||
unusedCounter = 0;
|
||||
@@ -630,7 +623,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
this.keyChain = this.wallet.getActiveKeyChain();
|
||||
|
||||
// Set up wallet's key chain
|
||||
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
|
||||
this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize());
|
||||
this.keyChain.maybeLookAhead();
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,44 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class MemoryPoW {
|
||||
|
||||
/**
|
||||
* Compute a MemoryPoW nonce
|
||||
*
|
||||
* @param data
|
||||
* @param workBufferLength
|
||||
* @param difficulty
|
||||
* @return
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
|
||||
try {
|
||||
return MemoryPoW.compute2(data, workBufferLength, difficulty, null);
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
// This won't happen, because above timeout is null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a MemoryPoW nonce, with optional timeout
|
||||
*
|
||||
* @param data
|
||||
* @param workBufferLength
|
||||
* @param difficulty
|
||||
* @param timeout maximum number of milliseconds to compute for before giving up,<br>or null if no timeout
|
||||
* @return
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
public static Integer compute2(byte[] data, int workBufferLength, long difficulty, Long timeout) throws TimeoutException {
|
||||
long startTime = NTP.getTime();
|
||||
|
||||
// Hash data with SHA256
|
||||
byte[] hash = Crypto.digest(data);
|
||||
|
||||
@@ -33,6 +67,13 @@ public class MemoryPoW {
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return -1;
|
||||
|
||||
if (timeout != null) {
|
||||
long now = NTP.getTime();
|
||||
if (now > startTime + timeout) {
|
||||
throw new TimeoutException("Timeout reached");
|
||||
}
|
||||
}
|
||||
|
||||
seed *= seedMultiplier; // per nonce
|
||||
|
||||
state[0] = longHash[0] ^ seed;
|
||||
|
@@ -16,6 +16,7 @@ public class OnlineAccountData {
|
||||
protected long timestamp;
|
||||
protected byte[] signature;
|
||||
protected byte[] publicKey;
|
||||
protected Integer nonce;
|
||||
|
||||
@XmlTransient
|
||||
private int hash;
|
||||
@@ -26,10 +27,15 @@ public class OnlineAccountData {
|
||||
protected OnlineAccountData() {
|
||||
}
|
||||
|
||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey, Integer nonce) {
|
||||
this.timestamp = timestamp;
|
||||
this.signature = signature;
|
||||
this.publicKey = publicKey;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
||||
this(timestamp, signature, publicKey, null);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
@@ -44,6 +50,10 @@ public class OnlineAccountData {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
public Integer getNonce() {
|
||||
return this.nonce;
|
||||
}
|
||||
|
||||
// For JAXB
|
||||
@XmlElement(name = "address")
|
||||
protected String getAddress() {
|
||||
|
@@ -1375,26 +1375,17 @@ public class Network {
|
||||
// We attempted to connect within the last day
|
||||
// but we last managed to connect over a week ago.
|
||||
Predicate<PeerData> isNotOldPeer = peerData -> {
|
||||
|
||||
// First check if there was a connection attempt within the last day
|
||||
if (peerData.getLastAttempted() != null
|
||||
&& peerData.getLastAttempted() > now - OLD_PEER_ATTEMPTED_PERIOD) {
|
||||
|
||||
// There was, so now check if we had a successful connection in the last 7 days
|
||||
if (peerData.getLastConnected() != null
|
||||
&& peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) {
|
||||
|
||||
// We did, so this is NOT an 'old' peer
|
||||
return true;
|
||||
}
|
||||
|
||||
// Last successful connection was more than 1 week ago - this is an 'old' peer
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Best to wait until we have a connection attempt - assume not an 'old' peer until then
|
||||
if (peerData.getLastAttempted() == null
|
||||
|| peerData.getLastAttempted() < now - OLD_PEER_ATTEMPTED_PERIOD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (peerData.getLastConnected() == null
|
||||
|| peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Disregard peers that are NOT 'old'
|
||||
|
@@ -27,6 +27,8 @@ import java.nio.channels.SocketChannel;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.LongAccumulator;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -153,6 +155,16 @@ public class Peer {
|
||||
*/
|
||||
private CommonBlockData commonBlockData;
|
||||
|
||||
// Message stats
|
||||
|
||||
private static class MessageStats {
|
||||
public final LongAdder count = new LongAdder();
|
||||
public final LongAdder totalBytes = new LongAdder();
|
||||
}
|
||||
|
||||
private final Map<MessageType, MessageStats> receivedMessageStats = new ConcurrentHashMap<>();
|
||||
private final Map<MessageType, MessageStats> sentMessageStats = new ConcurrentHashMap<>();
|
||||
|
||||
// Constructors
|
||||
|
||||
/**
|
||||
@@ -542,11 +554,18 @@ public class Peer {
|
||||
// Tidy up buffers:
|
||||
this.byteBuffer.flip();
|
||||
// Read-only, flipped buffer's position will be after end of message, so copy that
|
||||
long messageByteSize = readOnlyBuffer.position();
|
||||
this.byteBuffer.position(readOnlyBuffer.position());
|
||||
// Copy bytes after read message to front of buffer,
|
||||
// adjusting position accordingly, reset limit to capacity
|
||||
this.byteBuffer.compact();
|
||||
|
||||
// Record message stats
|
||||
MessageStats messageStats = this.receivedMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
|
||||
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
|
||||
messageStats.count.increment();
|
||||
messageStats.totalBytes.add(messageByteSize);
|
||||
|
||||
// Unsupported message type? Discard with no further processing
|
||||
if (message.getType() == MessageType.UNSUPPORTED)
|
||||
continue;
|
||||
@@ -609,6 +628,12 @@ public class Peer {
|
||||
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
|
||||
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
|
||||
|
||||
// Record message stats
|
||||
MessageStats messageStats = this.sentMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
|
||||
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
|
||||
messageStats.count.increment();
|
||||
messageStats.totalBytes.add(this.outputBuffer.limit());
|
||||
} catch (MessageException e) {
|
||||
// Something went wrong converting message to bytes, so discard but allow another round
|
||||
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
|
||||
@@ -799,8 +824,11 @@ public class Peer {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
boolean logStats = false;
|
||||
|
||||
if (!isStopping) {
|
||||
LOGGER.debug("[{}] Shutting down peer {}", this.peerConnectionId, this);
|
||||
logStats = true;
|
||||
}
|
||||
isStopping = true;
|
||||
|
||||
@@ -812,8 +840,34 @@ public class Peer {
|
||||
LOGGER.debug("[{}] IOException while trying to close peer {}", this.peerConnectionId, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (logStats && this.receivedMessageStats.size() > 0) {
|
||||
StringBuilder statsBuilder = new StringBuilder(1024);
|
||||
statsBuilder.append("peer ").append(this).append(" message stats:\n=received=");
|
||||
appendMessageStats(statsBuilder, this.receivedMessageStats);
|
||||
statsBuilder.append("\n=sent=");
|
||||
appendMessageStats(statsBuilder, this.sentMessageStats);
|
||||
|
||||
LOGGER.debug(statsBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendMessageStats(StringBuilder statsBuilder, Map<MessageType, MessageStats> messageStats) {
|
||||
if (messageStats.isEmpty()) {
|
||||
statsBuilder.append("\n none");
|
||||
return;
|
||||
}
|
||||
|
||||
messageStats.keySet().stream()
|
||||
.sorted(Comparator.comparing(MessageType::name))
|
||||
.forEach(messageType -> {
|
||||
MessageStats stats = messageStats.get(messageType);
|
||||
|
||||
statsBuilder.append("\n ").append(messageType.name())
|
||||
.append(": count=").append(stats.count.sum())
|
||||
.append(", total bytes=").append(stats.totalBytes.sum());
|
||||
});
|
||||
}
|
||||
|
||||
// Minimum version
|
||||
|
||||
|
@@ -46,7 +46,7 @@ public enum MessageType {
|
||||
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
|
||||
ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
|
||||
// ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
|
||||
ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V3(85, GetOnlineAccountsV3Message::fromByteBuffer),
|
||||
|
||||
ARBITRARY_DATA(90, ArbitraryDataMessage::fromByteBuffer),
|
||||
|
@@ -0,0 +1,121 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* For sending online accounts info to remote peer.
|
||||
*
|
||||
* Same format as V2, but with added support for a mempow nonce.
|
||||
*/
|
||||
public class OnlineAccountsV3Message extends Message {
|
||||
|
||||
public static final long MIN_PEER_VERSION = 0x300050000L; // 3.5.0
|
||||
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
|
||||
public OnlineAccountsV3Message(List<OnlineAccountData> onlineAccounts) {
|
||||
super(MessageType.ONLINE_ACCOUNTS_V3);
|
||||
|
||||
// Shortcut in case we have no online accounts
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
this.dataBytes = Ints.toByteArray(0);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
try {
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
if (onlineAccountData.getTimestamp() == timestamp) {
|
||||
bytes.write(onlineAccountData.getSignature());
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
|
||||
// Nonce is optional; use -1 as placeholder if missing
|
||||
int nonce = onlineAccountData.getNonce() != null ? onlineAccountData.getNonce() : -1;
|
||||
bytes.write(Ints.toByteArray(nonce));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private OnlineAccountsV3Message(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.ONLINE_ACCOUNTS_V3);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
while (accountCount > 0) {
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
// Nonce is optional - will be -1 if missing
|
||||
Integer nonce = bytes.getInt();
|
||||
if (nonce < 0) {
|
||||
nonce = null;
|
||||
}
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));
|
||||
}
|
||||
|
||||
if (bytes.hasRemaining()) {
|
||||
accountCount = bytes.getInt();
|
||||
} else {
|
||||
// we've finished
|
||||
accountCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new OnlineAccountsV3Message(id, onlineAccounts);
|
||||
}
|
||||
|
||||
}
|
@@ -210,9 +210,9 @@ public class Settings {
|
||||
private boolean allowConnectionsWithOlderPeerVersions = true;
|
||||
|
||||
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||
private int minPeerConnectionTime = 5 * 60; // seconds
|
||||
private int minPeerConnectionTime = 60 * 60; // seconds
|
||||
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||
private int maxPeerConnectionTime = 60 * 60; // seconds
|
||||
private int maxPeerConnectionTime = 4 * 60 * 60; // seconds
|
||||
/** Maximum time (in seconds) that a peer should remain connected when requesting QDN data */
|
||||
private int maxDataPeerConnectionTime = 2 * 60; // seconds
|
||||
|
||||
@@ -283,6 +283,21 @@ public class Settings {
|
||||
/** Additional offset added to values returned by NTP.getTime() */
|
||||
private Long testNtpOffset = null;
|
||||
|
||||
// Online accounts
|
||||
|
||||
/** Whether to opt-in to mempow computations for online accounts, ahead of general release */
|
||||
private boolean onlineAccountsMemPoWEnabled = false;
|
||||
|
||||
|
||||
/* Foreign chains */
|
||||
|
||||
/** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */
|
||||
private int gapLimit = 24;
|
||||
|
||||
/** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */
|
||||
private int bitcoinjLookaheadSize = 50;
|
||||
|
||||
|
||||
|
||||
// Data storage (QDN)
|
||||
|
||||
@@ -766,6 +781,10 @@ public class Settings {
|
||||
return this.testNtpOffset;
|
||||
}
|
||||
|
||||
public boolean isOnlineAccountsMemPoWEnabled() {
|
||||
return this.onlineAccountsMemPoWEnabled;
|
||||
}
|
||||
|
||||
public long getRepositoryBackupInterval() {
|
||||
return this.repositoryBackupInterval;
|
||||
}
|
||||
@@ -872,6 +891,15 @@ public class Settings {
|
||||
}
|
||||
|
||||
|
||||
public int getGapLimit() {
|
||||
return this.gapLimit;
|
||||
}
|
||||
|
||||
public int getBitcoinjLookaheadSize() {
|
||||
return bitcoinjLookaheadSize;
|
||||
}
|
||||
|
||||
|
||||
public boolean isQdnEnabled() {
|
||||
return this.qdnEnabled;
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import java.util.function.Supplier;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
@@ -49,7 +48,7 @@ public class PresenceTransaction extends Transaction {
|
||||
REWARD_SHARE(0) {
|
||||
@Override
|
||||
public long getLifetime() {
|
||||
return OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS;
|
||||
return OnlineAccountsManager.getOnlineTimestampModulus();
|
||||
}
|
||||
},
|
||||
TRADE_BOT(1) {
|
||||
|
@@ -23,6 +23,8 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 43200000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||
"onlineAccountsMemoryPoWTimestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
|
@@ -8,6 +8,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.test.common.AccountUtils;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -28,8 +29,6 @@ public class SchnorrTests extends Qortal25519Extras {
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
}
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
@Test
|
||||
public void testConversion() {
|
||||
// Scalar form
|
||||
@@ -130,7 +129,7 @@ public class SchnorrTests extends Qortal25519Extras {
|
||||
|
||||
@Test
|
||||
public void testSimpleAggregate() {
|
||||
List<OnlineAccountData> onlineAccounts = generateOnlineAccounts(1);
|
||||
List<OnlineAccountData> onlineAccounts = AccountUtils.generateOnlineAccounts(1);
|
||||
|
||||
byte[] aggregatePublicKey = aggregatePublicKeys(onlineAccounts.stream().map(OnlineAccountData::getPublicKey).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate public key: %s%n", HashCode.fromBytes(aggregatePublicKey));
|
||||
@@ -151,7 +150,7 @@ public class SchnorrTests extends Qortal25519Extras {
|
||||
|
||||
@Test
|
||||
public void testMultipleAggregate() {
|
||||
List<OnlineAccountData> onlineAccounts = generateOnlineAccounts(5000);
|
||||
List<OnlineAccountData> onlineAccounts = AccountUtils.generateOnlineAccounts(5000);
|
||||
|
||||
byte[] aggregatePublicKey = aggregatePublicKeys(onlineAccounts.stream().map(OnlineAccountData::getPublicKey).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate public key: %s%n", HashCode.fromBytes(aggregatePublicKey));
|
||||
@@ -166,25 +165,4 @@ public class SchnorrTests extends Qortal25519Extras {
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
assertTrue(verifyAggregated(aggregatePublicKey, aggregateSignature, timestampBytes));
|
||||
}
|
||||
|
||||
private List<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
long timestamp = System.currentTimeMillis();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
|
||||
for (int a = 0; a < numAccounts; ++a) {
|
||||
byte[] privateKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
SECURE_RANDOM.nextBytes(privateKey);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Qortal25519Extras.generatePublicKey(privateKey, 0, publicKey, 0);
|
||||
|
||||
byte[] signature = signForAggregation(privateKey, timestampBytes);
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey));
|
||||
}
|
||||
|
||||
return onlineAccounts;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,17 @@
|
||||
package org.qortal.test.common;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.qortal.crypto.Qortal25519Extras.signForAggregation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
@@ -14,6 +19,7 @@ import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
public class AccountUtils {
|
||||
@@ -21,6 +27,8 @@ public class AccountUtils {
|
||||
public static final int txGroupId = Group.NO_GROUP;
|
||||
public static final long fee = 1L * Amounts.MULTIPLIER;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
public static void pay(Repository repository, String testSenderName, String testRecipientName, long amount) throws DataException {
|
||||
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, testSenderName);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, testRecipientName);
|
||||
@@ -109,4 +117,30 @@ public class AccountUtils {
|
||||
assertEquals(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), expectedBalance, actualBalance);
|
||||
}
|
||||
|
||||
|
||||
public static List<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
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);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Qortal25519Extras.generatePublicKey(privateKey, 0, publicKey, 0);
|
||||
|
||||
byte[] signature = signForAggregation(privateKey, timestampBytes);
|
||||
|
||||
Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));
|
||||
}
|
||||
|
||||
return onlineAccounts;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,22 +1,39 @@
|
||||
package org.qortal.test.network;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import io.druid.extendedset.intset.ConciseSet;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.AccountUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Security;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class OnlineAccountsTests {
|
||||
public class OnlineAccountsTests extends Common {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
static {
|
||||
@@ -27,6 +44,12 @@ public class OnlineAccountsTests {
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException, IOException {
|
||||
Common.useSettingsAndDb("test-settings-v2-no-sig-agg.json", false);
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetOnlineAccountsV2() throws MessageException {
|
||||
@@ -111,4 +134,102 @@ public class OnlineAccountsTests {
|
||||
return onlineAccounts;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineAccountsModulusV1() throws IllegalAccessException, DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Set feature trigger timestamp to MAX long so that it is inactive
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsModulusV2Timestamp", Long.MAX_VALUE, true);
|
||||
|
||||
List<String> onlineAccountSignatures = new ArrayList<>();
|
||||
long fakeNTPOffset = 0L;
|
||||
|
||||
// Mint a block and store its timestamp
|
||||
Block block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
long lastBlockTimestamp = block.getBlockData().getTimestamp();
|
||||
|
||||
// Mint some blocks and keep track of the different online account signatures
|
||||
for (int i = 0; i < 30; i++) {
|
||||
block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
|
||||
// Increase NTP fixed offset by the block time, to simulate time passing
|
||||
long blockTimeDelta = block.getBlockData().getTimestamp() - lastBlockTimestamp;
|
||||
lastBlockTimestamp = block.getBlockData().getTimestamp();
|
||||
fakeNTPOffset += blockTimeDelta;
|
||||
NTP.setFixedOffset(fakeNTPOffset);
|
||||
|
||||
String lastOnlineAccountSignatures58 = Base58.encode(block.getBlockData().getOnlineAccountsSignatures());
|
||||
if (!onlineAccountSignatures.contains(lastOnlineAccountSignatures58)) {
|
||||
onlineAccountSignatures.add(lastOnlineAccountSignatures58);
|
||||
}
|
||||
}
|
||||
|
||||
// We expect at least 6 unique signatures over 30 blocks (generally 6-8, but could be higher due to block time differences)
|
||||
System.out.println(String.format("onlineAccountSignatures count: %d", onlineAccountSignatures.size()));
|
||||
assertTrue(onlineAccountSignatures.size() >= 6);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineAccountsModulusV2() throws IllegalAccessException, DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Set feature trigger timestamp to 0 so that it is active
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "onlineAccountsModulusV2Timestamp", 0L, true);
|
||||
|
||||
List<String> onlineAccountSignatures = new ArrayList<>();
|
||||
long fakeNTPOffset = 0L;
|
||||
|
||||
// Mint a block and store its timestamp
|
||||
Block block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
long lastBlockTimestamp = block.getBlockData().getTimestamp();
|
||||
|
||||
// Mint some blocks and keep track of the different online account signatures
|
||||
for (int i = 0; i < 30; i++) {
|
||||
block = BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
|
||||
// Increase NTP fixed offset by the block time, to simulate time passing
|
||||
long blockTimeDelta = block.getBlockData().getTimestamp() - lastBlockTimestamp;
|
||||
lastBlockTimestamp = block.getBlockData().getTimestamp();
|
||||
fakeNTPOffset += blockTimeDelta;
|
||||
NTP.setFixedOffset(fakeNTPOffset);
|
||||
|
||||
String lastOnlineAccountSignatures58 = Base58.encode(block.getBlockData().getOnlineAccountsSignatures());
|
||||
if (!onlineAccountSignatures.contains(lastOnlineAccountSignatures58)) {
|
||||
onlineAccountSignatures.add(lastOnlineAccountSignatures58);
|
||||
}
|
||||
}
|
||||
|
||||
// We expect 1-3 unique signatures over 30 blocks
|
||||
System.out.println(String.format("onlineAccountSignatures count: %d", onlineAccountSignatures.size()));
|
||||
assertTrue(onlineAccountSignatures.size() >= 1 && onlineAccountSignatures.size() <= 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "For informational use")
|
||||
public void testOnlineAccountNonceCompression() throws IOException {
|
||||
List<OnlineAccountData> onlineAccounts = AccountUtils.generateOnlineAccounts(5000);
|
||||
|
||||
// Build array of nonce values
|
||||
List<Integer> accountNonces = new ArrayList<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
accountNonces.add(onlineAccountData.getNonce());
|
||||
}
|
||||
|
||||
// Write nonces into ConciseSet
|
||||
ConciseSet nonceSet = new ConciseSet();
|
||||
nonceSet = nonceSet.convert(accountNonces);
|
||||
byte[] conciseEncodedNonces = nonceSet.toByteBuffer().array();
|
||||
|
||||
// Also write to regular byte array of ints, for comparison
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
for (Integer nonce : accountNonces) {
|
||||
bytes.write(Ints.toByteArray(nonce));
|
||||
}
|
||||
byte[] standardEncodedNonces = bytes.toByteArray();
|
||||
|
||||
System.out.println(String.format("Standard: %d", standardEncodedNonces.length));
|
||||
System.out.println(String.format("Concise: %d", conciseEncodedNonces.length));
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
87
src/test/resources/test-chain-v2-no-sig-agg.json
Normal file
87
src/test/resources/test-chain-v2-no-sig-agg.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"isTestChain": true,
|
||||
"blockTimestampMargin": 500,
|
||||
"transactionExpiryPeriod": 86400000,
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFees": [
|
||||
{ "timestamp": 1645372800000, "fee": "5" }
|
||||
],
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
{ "height": 21, "reward": 1 }
|
||||
],
|
||||
"sharesByLevel": [
|
||||
{ "levels": [ 1, 2 ], "share": 0.05 },
|
||||
{ "levels": [ 3, 4 ], "share": 0.10 },
|
||||
{ "levels": [ 5, 6 ], "share": 0.15 },
|
||||
{ "levels": [ 7, 8 ], "share": 0.20 },
|
||||
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||
],
|
||||
"qoraHoldersShare": 0.20,
|
||||
"qoraPerQortReward": 250,
|
||||
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
"assetsTimestamp": 0,
|
||||
"votingTimestamp": 0,
|
||||
"arbitraryTimestamp": 0,
|
||||
"powfixTimestamp": 0,
|
||||
"qortalTimestamp": 0,
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0,
|
||||
"atFindNextTransactionFix": 0,
|
||||
"newBlockSigHeight": 999999,
|
||||
"shareBinFix": 999999,
|
||||
"rewardShareLimitTimestamp": 9999999999999,
|
||||
"calcChainWeightTimestamp": 0,
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"aggregateSignatureTimestamp": 9999999999999
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "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 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 }
|
||||
]
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
19
src/test/resources/test-settings-v2-no-sig-agg.json
Normal file
19
src/test/resources/test-settings-v2-no-sig-agg.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"repositoryPath": "testdb",
|
||||
"bitcoinNet": "TEST3",
|
||||
"litecoinNet": "TEST3",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-no-sig-agg.json",
|
||||
"exportPath": "qortal-backup-test",
|
||||
"bootstrap": false,
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0,
|
||||
"pruneBlockLimit": 100,
|
||||
"bootstrapFilenamePrefix": "test-",
|
||||
"dataPath": "data-test",
|
||||
"tempDataPath": "data-test/_temp",
|
||||
"listsPath": "lists-test",
|
||||
"storagePolicy": "FOLLOWED_OR_VIEWED",
|
||||
"maxStorageCapacity": 104857600
|
||||
}
|
Reference in New Issue
Block a user