Compare commits

...

13 Commits

Author SHA1 Message Date
CalDescent
1e4281996b Merge branch 'master' into online-accounts-mempow-v2
# Conflicts:
#	src/main/resources/blockchain.json
2022-08-21 13:43:02 +01:00
CalDescent
0ddb7f8f17 Merge branch 'master' into online-accounts-mempow-v2
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
2022-07-31 16:00:32 +01:00
CalDescent
fbcc870d36 Added informational test to compare ConsiceSet size against an int array for online account nonce arrays. 2022-07-15 10:23:14 +01:00
CalDescent
020e59743b Fixed failing test(s) due to merge. 2022-07-10 19:49:24 +01:00
CalDescent
0904de3f71 Merge branch 'master' into online-accounts-mempow-v2
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
2022-07-10 16:50:28 +01:00
CalDescent
fe2c63e8e4 Generate random nonces for test accounts.
These don't have to be valid for unit tests, because they are treated as "cached already valid accounts" in the block validation.
2022-07-02 17:30:31 +01:00
CalDescent
a3febdf00e Pass timestamp to OnlineAccountsManager.isMemoryPoWActive() so that block timestamp can be used. 2022-07-02 17:26:53 +01:00
CalDescent
4ca174fa0b Fixed bug in isMemoryPoWActive() which affected the ability to enable mempow via settings. 2022-07-02 13:45:39 +01:00
CalDescent
294582f136 Added mempow support in OnlineAccountsManager.
- This adds mempow requirements to online account importing (after activation timestamp), however doesn't yet add any requirements to block validation.
- It also causes the 'next' online accounts timestamp to be computed in addition to the 'current', so that the computed nonce value is ready when the next online accounts timestamp window begins.
2022-07-02 12:33:02 +01:00
CalDescent
215800fb67 Added optional "timeout" parameter to MemoryPoW.compute2().
This can be used to give up after the specified number of milliseconds.
2022-07-01 22:36:51 +01:00
CalDescent
b05d428b2e Added onlineAccountsMemPoWEnabled setting (for beta testing) 2022-07-01 22:31:51 +01:00
CalDescent
d2adadb600 Added onlineAccountsMemoryPoWTimestamp to blockchain.json 2022-07-01 22:31:30 +01:00
CalDescent
8e8c0b3fc5 Added OnlineAccountsV3Message, along with optional nonce Integer in OnlineAccountData.
This could potentially be released ahead of the other mempow code, splitting the rollout into multiple smaller phases.
2022-07-01 22:29:05 +01:00
12 changed files with 491 additions and 42 deletions

View File

@ -189,6 +189,10 @@ 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;
@ -349,6 +353,10 @@ 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;

View File

@ -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;

View File

@ -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;
@ -52,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;
@ -95,6 +103,10 @@ public class OnlineAccountsManager {
return (now / onlineTimestampModulus) * onlineTimestampModulus;
}
public static long toOnlineAccountTimestamp(long timestamp) {
return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
}
private OnlineAccountsManager() {
}
@ -140,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) {
@ -150,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);
}
@ -190,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) {
@ -248,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;
}
@ -305,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)
@ -392,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();
@ -421,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();
@ -433,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();
@ -447,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
@ -462,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.
*/
@ -721,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);
}
}

View File

@ -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;

View File

@ -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() {

View File

@ -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),

View File

@ -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);
}
}

View File

@ -283,6 +283,11 @@ 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 */
@ -776,6 +781,10 @@ public class Settings {
return this.testNtpOffset;
}
public boolean isOnlineAccountsMemPoWEnabled() {
return this.onlineAccountsMemPoWEnabled;
}
public long getRepositoryBackupInterval() {
return this.repositoryBackupInterval;
}

View File

@ -24,6 +24,7 @@
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
"onlineAccountsMemoryPoWTimestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,26 +1,29 @@
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.account.PrivateKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.controller.BlockMinter;
import org.qortal.controller.OnlineAccountsManager;
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;
@ -202,4 +205,31 @@ public class OnlineAccountsTests extends Common {
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));
}
}