From f993f938f4379499630f6c3f310fa9c15ab69e8d Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 1 Apr 2022 12:02:29 +0100 Subject: [PATCH] Added mempow support in OnlineAccountsManager. - Removed OnlineAccountsMessage and GetOnlineAccountsMessage (no longer any need for backwards support). - Added OnlineAccountsV3Message and GetOnlineAccountsV3Message. - Mempow computations can be opted in early via the "onlineAccountsMemPoWEnabled" setting (although they won't be validated). - Feature trigger timestamp set to unknown future date. - Still needs calibration on a testnet. - Still need to uncomment/finalize code to calculate "next" signature ahead of time. --- .../java/org/qortal/block/BlockChain.java | 8 + .../org/qortal/controller/Controller.java | 16 +- .../controller/OnlineAccountsManager.java | 342 +++++++++++++----- .../data/network/OnlineAccountData.java | 21 +- .../message/GetOnlineAccountsMessage.java | 73 ---- .../message/GetOnlineAccountsV3Message.java | 113 ++++++ .../org/qortal/network/message/Message.java | 4 +- .../message/OnlineAccountsMessage.java | 80 ---- .../message/OnlineAccountsV3Message.java | 139 +++++++ .../java/org/qortal/settings/Settings.java | 9 + .../org/qortal/transform/Transformer.java | 2 + src/main/resources/blockchain.json | 1 + .../test/network/OnlineAccountsTests.java | 24 -- .../test-chain-v2-founder-rewards.json | 1 + .../test-chain-v2-leftover-reward.json | 1 + src/test/resources/test-chain-v2-minting.json | 1 + .../test-chain-v2-qora-holder-extremes.json | 1 + .../resources/test-chain-v2-qora-holder.json | 1 + .../test-chain-v2-reward-levels.json | 1 + .../test-chain-v2-reward-scaling.json | 1 + src/test/resources/test-chain-v2.json | 1 + 21 files changed, 564 insertions(+), 276 deletions(-) delete mode 100644 src/main/java/org/qortal/network/message/GetOnlineAccountsMessage.java create mode 100644 src/main/java/org/qortal/network/message/GetOnlineAccountsV3Message.java delete mode 100644 src/main/java/org/qortal/network/message/OnlineAccountsMessage.java create mode 100644 src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index d9d2e34a..b19b5e57 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -162,6 +162,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; + /** Settings relating to CIYAM AT feature. */ public static class CiyamAtSettings { /** Fee per step/op-code executed. */ @@ -325,6 +329,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; diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index e2ae6ca4..144439b2 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1140,14 +1140,6 @@ public class Controller extends Thread { TransactionImporter.getInstance().onNetworkTransactionSignaturesMessage(peer, message); break; - case GET_ONLINE_ACCOUNTS: - OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsMessage(peer, message); - break; - - case ONLINE_ACCOUNTS: - OnlineAccountsManager.getInstance().onNetworkOnlineAccountsMessage(peer, message); - break; - case GET_ONLINE_ACCOUNTS_V2: OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV2Message(peer, message); break; @@ -1156,6 +1148,14 @@ public class Controller extends Thread { OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message); break; + case GET_ONLINE_ACCOUNTS_V3: + OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message); + break; + + case ONLINE_ACCOUNTS_V3: + OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message); + break; + case GET_ARBITRARY_DATA: // Not currently supported break; diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java index a5f86b48..afc8e97e 100644 --- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java +++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java @@ -7,8 +7,10 @@ import org.qortal.account.Account; import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.block.BlockChain; +import org.qortal.crypto.MemoryPoW; import org.qortal.data.account.MintingAccountData; import org.qortal.data.account.RewardShareData; +import org.qortal.data.block.BlockData; import org.qortal.data.network.OnlineAccountData; import org.qortal.network.Network; import org.qortal.network.Peer; @@ -16,10 +18,14 @@ 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 java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.*; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; public class OnlineAccountsManager extends Thread { @@ -47,6 +53,11 @@ public class OnlineAccountsManager extends Thread { private static OnlineAccountsManager instance; private volatile boolean isStopping = false; + // MemoryPoW + public final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes + public int POW_DIFFICULTY = 18; // leading zero bits + public static final int MAX_NONCE_COUNT = 1; // Maximum number of nonces to verify + // To do with online accounts list private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms @@ -54,7 +65,7 @@ public class OnlineAccountsManager extends Thread { public static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L; /** How many (latest) blocks' worth of online accounts we cache */ private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2; - private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; + private static final long ONLINE_ACCOUNTS_V3_PEER_VERSION = 0x0300030000L; private long onlineAccountsTasksTimestamp = Controller.startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms @@ -193,8 +204,16 @@ public class OnlineAccountsManager extends Thread { return; } + // Validate mempow if feature trigger is active + if (now >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) { + if (!this.verifyMemoryPoW(onlineAccountData)) { + LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress())); + return; + } + } + synchronized (this.onlineAccounts) { - OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null); + OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null); // CME?? if (existingAccountData != null) { if (existingAccountData.getTimestamp() < onlineAccountData.getTimestamp()) { @@ -225,21 +244,21 @@ public class OnlineAccountsManager extends Thread { return; final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now); - byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp); + + List mintingAccounts = new ArrayList<>(); synchronized (this.onlineAccounts) { this.onlineAccounts.clear(); - - for (PrivateKeyAccount onlineAccount : onlineAccounts) { - // Check mintingAccount is actually reward-share? - - byte[] signature = onlineAccount.sign(timestampBytes); - byte[] publicKey = onlineAccount.getPublicKey(); - - OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey); - this.onlineAccounts.add(ourOnlineAccountData); - } } + + for (PrivateKeyAccount onlineAccount : onlineAccounts) { + // Check mintingAccount is actually reward-share? + + MintingAccountData mintingAccountData = new MintingAccountData(onlineAccount.getPrivateKey(), onlineAccount.getPublicKey()); + mintingAccounts.add(mintingAccountData); + } + + computeOurAccountsForTimestamp(mintingAccounts, onlineAccountsTimestamp); } private void performOnlineAccountsTasks() { @@ -273,11 +292,11 @@ public class OnlineAccountsManager extends Thread { safeOnlineAccounts = new ArrayList<>(this.onlineAccounts); } - Message messageV1 = new GetOnlineAccountsMessage(safeOnlineAccounts); Message messageV2 = new GetOnlineAccountsV2Message(safeOnlineAccounts); + Message messageV3 = new GetOnlineAccountsV3Message(safeOnlineAccounts); Network.getInstance().broadcast(peer -> - peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1 + peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION ? messageV3 : messageV2 ); } } @@ -288,6 +307,11 @@ public class OnlineAccountsManager extends Thread { return; } + // If we're not up-to-date, then there's no point in computing anything yet + if (!Controller.getInstance().isUpToDate()) { + return; + } + List mintingAccounts; try (final Repository repository = RepositoryManager.getRepository()) { mintingAccounts = repository.getAccountRepository().getMintingAccounts(); @@ -328,55 +352,183 @@ public class OnlineAccountsManager extends Thread { // 'current' timestamp final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now); - boolean hasInfoChanged = false; + computeOurAccountsForTimestamp(mintingAccounts, onlineAccountsTimestamp); - byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp); - List ourOnlineAccounts = new ArrayList<>(); + // 'next' timestamp // TODO +// final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(now) + getOnlineTimestampModulus(); +// computeOurAccountsForTimestamp(mintingAccounts, nextOnlineAccountsTimestamp); + } - MINTING_ACCOUNTS: - for (MintingAccountData mintingAccountData : mintingAccounts) { - PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey()); + /** + * Compute a mempow nonce and signature for a given set of accounts and timestamp + * @param mintingAccounts - the online accounts + * @param onlineAccountsTimestamp - the online accounts timestamp + */ + private void computeOurAccountsForTimestamp(List mintingAccounts, long onlineAccountsTimestamp) { + try (final Repository repository = RepositoryManager.getRepository()) { - byte[] signature = mintingAccount.sign(timestampBytes); - byte[] publicKey = mintingAccount.getPublicKey(); + boolean hasInfoChanged = false; - // Our account is online - OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey); - synchronized (this.onlineAccounts) { - Iterator iterator = this.onlineAccounts.iterator(); + final long currentOnlineAccountsTimestamp = toOnlineAccountTimestamp(NTP.getTime()); + + List ourOnlineAccounts = new ArrayList<>(); + + MINTING_ACCOUNTS: + for (MintingAccountData mintingAccountData : mintingAccounts) { + PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey()); + byte[] publicKey = mintingAccount.getPublicKey(); + + // Our account is online + List safeOnlineAccounts; + synchronized (this.onlineAccounts) { + safeOnlineAccounts = new ArrayList<>(this.onlineAccounts); + } + + Iterator iterator = safeOnlineAccounts.iterator(); while (iterator.hasNext()) { OnlineAccountData existingOnlineAccountData = iterator.next(); - if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) { + if (Arrays.equals(existingOnlineAccountData.getPublicKey(), publicKey)) { // If our online account is already present, with same timestamp, then move on to next mintingAccount if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp) continue MINTING_ACCOUNTS; // If our online account is already present, but with older timestamp, then remove it - iterator.remove(); - break; + if (existingOnlineAccountData.getTimestamp() < currentOnlineAccountsTimestamp) { + this.onlineAccounts.remove(existingOnlineAccountData); // Safe because we are iterating through a copy + } } } + // We need to add a new account + + byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp); + int chainHeight = repository.getBlockRepository().getBlockchainHeight(); + int referenceHeight = Math.max(1, chainHeight - 10); + BlockData recentBlockData = repository.getBlockRepository().fromHeight(referenceHeight); + if (recentBlockData == null || recentBlockData.getSignature() == null) { + LOGGER.info("Unable to compute online accounts without having a recent block"); + return; + } + byte[] reducedRecentBlockSignature = Arrays.copyOfRange(recentBlockData.getSignature(), 0, 8); + + byte[] mempowBytes; + try { + mempowBytes = this.getMemoryPoWBytes(publicKey, onlineAccountsTimestamp, reducedRecentBlockSignature); + } + catch (IOException e) { + LOGGER.info("Unable to create bytes for MemoryPoW. Moving on to next account..."); + continue MINTING_ACCOUNTS; + } + + Integer nonce = this.computeMemoryPoW(mempowBytes, publicKey); + if (nonce == null) { + // Send zero if we haven't computed a nonce due to feature trigger timestamp + nonce = 0; + } + + byte[] signature = mintingAccount.sign(timestampBytes); // TODO: include nonce and block signature? + + OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, Arrays.asList(nonce), reducedRecentBlockSignature); + this.onlineAccounts.add(ourOnlineAccountData); + + LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp)); + ourOnlineAccounts.add(ourOnlineAccountData); + hasInfoChanged = true; } - LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp)); - ourOnlineAccounts.add(ourOnlineAccountData); - hasInfoChanged = true; + if (!hasInfoChanged) + return; + + Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts); + Message messageV3 = new OnlineAccountsV3Message(ourOnlineAccounts); + + Network.getInstance().broadcast(peer -> + peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION ? messageV3 : messageV2 + ); + + LOGGER.trace(() -> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp)); + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while computing online accounts"), e); + } + } + + private byte[] getMemoryPoWBytes(byte[] publicKey, long onlineAccountsTimestamp, byte[] reducedRecentBlockSignature) throws IOException { + byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(publicKey); + outputStream.write(timestampBytes); + outputStream.write(reducedRecentBlockSignature); + + return outputStream.toByteArray(); + } + + private Integer computeMemoryPoW(byte[] bytes, byte[] publicKey) { + Long startTime = NTP.getTime(); + if (startTime < BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp() || Settings.getInstance().isOnlineAccountsMemPoWEnabled()) { + LOGGER.info("Mempow start timestamp not yet reached, and onlineAccountsMemPoWEnabled not enabled in settings"); + return null; } - if (!hasInfoChanged) - return; + LOGGER.info(String.format("Computing nonce for account %.8s...", Base58.encode(publicKey))); - Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts); - Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts); + // Calculate the time until the next online timestamp and use it as a timeout when computing the nonce + final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(startTime) + getOnlineTimestampModulus(); + long timeUntilNextTimestamp = nextOnlineAccountsTimestamp - startTime; - Network.getInstance().broadcast(peer -> - peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1 - ); + Integer nonce; + try { + nonce = MemoryPoW.compute2(bytes, POW_BUFFER_SIZE, POW_DIFFICULTY, timeUntilNextTimestamp); + } catch (TimeoutException e) { + LOGGER.info("Timed out computing nonce for account %.8s", Base58.encode(publicKey)); + return null; + } - LOGGER.trace(() -> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp)); + 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 account %.8s: %d. Buffer size: %d. Difficulty: %d. " + + "Time taken: %02d:%02d. Hashrate: %f", Base58.encode(publicKey), nonce, + POW_BUFFER_SIZE, POW_DIFFICULTY, minutes, seconds, hashRate)); + + return nonce; + } + + public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData) { + List nonces = onlineAccountData.getNonces(); + if (nonces == null || nonces.isEmpty()) { + // Missing required nonce value(s) + return false; + } + + if (nonces.size() > MAX_NONCE_COUNT) { + // More than the allowed nonce count + return false; + } + + byte[] reducedBlockSignature = onlineAccountData.getReducedBlockSignature(); + if (reducedBlockSignature == null) { + // Missing required block signature + return false; + } + + byte[] mempowBytes; + try { + mempowBytes = this.getMemoryPoWBytes(onlineAccountData.getPublicKey(), onlineAccountData.getTimestamp(), reducedBlockSignature); + } catch (IOException e) { + return false; + } + + // For now, we will only require a single nonce + int nonce = nonces.get(0); + + // Verify the nonce + return MemoryPoW.verify2(mempowBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce); } public static long toOnlineAccountTimestamp(long timestamp) { @@ -422,53 +574,6 @@ public class OnlineAccountsManager extends Thread { // Network handlers - public void onNetworkGetOnlineAccountsMessage(Peer peer, Message message) { - GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message; - - List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts(); - - // Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts - List accountsToSend; - synchronized (this.onlineAccounts) { - accountsToSend = new ArrayList<>(this.onlineAccounts); - } - - Iterator iterator = accountsToSend.iterator(); - - SEND_ITERATOR: - while (iterator.hasNext()) { - OnlineAccountData onlineAccountData = iterator.next(); - - for (int i = 0; i < excludeAccounts.size(); ++i) { - OnlineAccountData excludeAccountData = excludeAccounts.get(i); - - if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) { - iterator.remove(); - continue SEND_ITERATOR; - } - } - } - - Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend); - peer.sendMessage(onlineAccountsMessage); - - LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer)); - } - - public void onNetworkOnlineAccountsMessage(Peer peer, Message message) { - OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message; - - List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts(); - LOGGER.trace(() -> String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer)); - - try (final Repository repository = RepositoryManager.getRepository()) { - for (OnlineAccountData onlineAccountData : peersOnlineAccounts) - this.verifyAndAddAccount(repository, onlineAccountData); - } catch (DataException e) { - LOGGER.error(String.format("Repository issue while verifying online accounts from peer %s", peer), e); - } - } - public void onNetworkGetOnlineAccountsV2Message(Peer peer, Message message) { GetOnlineAccountsV2Message getOnlineAccountsMessage = (GetOnlineAccountsV2Message) message; @@ -529,4 +634,65 @@ public class OnlineAccountsManager extends Thread { LOGGER.debug(String.format("Added %d online accounts to queue", importCount)); } + + public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) { + GetOnlineAccountsV3Message getOnlineAccountsMessage = (GetOnlineAccountsV3Message) message; + + List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts(); + + // Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts + List accountsToSend; + synchronized (this.onlineAccounts) { + accountsToSend = new ArrayList<>(this.onlineAccounts); + } + + Iterator iterator = accountsToSend.iterator(); + + SEND_ITERATOR: + while (iterator.hasNext()) { + OnlineAccountData onlineAccountData = iterator.next(); + + for (int i = 0; i < excludeAccounts.size(); ++i) { + OnlineAccountData excludeAccountData = excludeAccounts.get(i); + + if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) { + iterator.remove(); + continue SEND_ITERATOR; + } + } + } + + Message onlineAccountsMessage = new OnlineAccountsV3Message(accountsToSend); + peer.sendMessage(onlineAccountsMessage); + + LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer)); + } + + public void onNetworkOnlineAccountsV3Message(Peer peer, Message message) { + OnlineAccountsV3Message onlineAccountsMessage = (OnlineAccountsV3Message) message; + + List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts(); + LOGGER.debug(String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer)); + + int importCount = 0; + + // Add any online accounts to the queue that aren't already present + for (OnlineAccountData onlineAccountData : peersOnlineAccounts) { + + // Do we already know about this online account data? + if (onlineAccounts.contains(onlineAccountData)) { + continue; + } + + // Is it already in the import queue? + if (onlineAccountsImportQueue.contains(onlineAccountData)) { + continue; + } + + onlineAccountsImportQueue.add(onlineAccountData); + importCount++; + } + + LOGGER.debug(String.format("Added %d online accounts to queue", importCount)); + } } diff --git a/src/main/java/org/qortal/data/network/OnlineAccountData.java b/src/main/java/org/qortal/data/network/OnlineAccountData.java index 15792307..8a93eaef 100644 --- a/src/main/java/org/qortal/data/network/OnlineAccountData.java +++ b/src/main/java/org/qortal/data/network/OnlineAccountData.java @@ -1,6 +1,7 @@ package org.qortal.data.network; import java.util.Arrays; +import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -15,6 +16,8 @@ public class OnlineAccountData { protected long timestamp; protected byte[] signature; protected byte[] publicKey; + protected List nonces; + protected byte[] reducedBlockSignature; // Constructors @@ -22,10 +25,16 @@ public class OnlineAccountData { protected OnlineAccountData() { } - public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) { + public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey, List nonces, byte[] reducedBlockSignature) { this.timestamp = timestamp; this.signature = signature; this.publicKey = publicKey; + this.nonces = nonces; + this.reducedBlockSignature = reducedBlockSignature; + } + + public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) { + this(timestamp, signature, publicKey, null, null); } public long getTimestamp() { @@ -40,6 +49,14 @@ public class OnlineAccountData { return this.publicKey; } + public List getNonces() { + return this.nonces; + } + + public byte[] getReducedBlockSignature() { + return this.reducedBlockSignature; + } + // For JAXB @XmlElement(name = "address") protected String getAddress() { @@ -69,6 +86,8 @@ public class OnlineAccountData { if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey)) return false; + // Best not to consider additional properties for the purposes of uniqueness + return true; } diff --git a/src/main/java/org/qortal/network/message/GetOnlineAccountsMessage.java b/src/main/java/org/qortal/network/message/GetOnlineAccountsMessage.java deleted file mode 100644 index 23c21bc5..00000000 --- a/src/main/java/org/qortal/network/message/GetOnlineAccountsMessage.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.qortal.data.network.OnlineAccountData; -import org.qortal.transform.Transformer; - -import com.google.common.primitives.Ints; -import com.google.common.primitives.Longs; - -public class GetOnlineAccountsMessage extends Message { - private static final int MAX_ACCOUNT_COUNT = 5000; - - private List onlineAccounts; - - public GetOnlineAccountsMessage(List onlineAccounts) { - this(-1, onlineAccounts); - } - - private GetOnlineAccountsMessage(int id, List onlineAccounts) { - super(id, MessageType.GET_ONLINE_ACCOUNTS); - - this.onlineAccounts = onlineAccounts.stream().limit(MAX_ACCOUNT_COUNT).collect(Collectors.toList()); - } - - public List getOnlineAccounts() { - return this.onlineAccounts; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - final int accountCount = bytes.getInt(); - - List onlineAccounts = new ArrayList<>(accountCount); - - for (int i = 0; i < Math.min(MAX_ACCOUNT_COUNT, accountCount); ++i) { - long timestamp = bytes.getLong(); - - byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; - bytes.get(publicKey); - - onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey)); - } - - return new GetOnlineAccountsMessage(id, onlineAccounts); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.onlineAccounts.size())); - - for (int i = 0; i < this.onlineAccounts.size(); ++i) { - OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); - bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp())); - - bytes.write(onlineAccountData.getPublicKey()); - } - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/GetOnlineAccountsV3Message.java b/src/main/java/org/qortal/network/message/GetOnlineAccountsV3Message.java new file mode 100644 index 00000000..f736cf8e --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetOnlineAccountsV3Message.java @@ -0,0 +1,113 @@ +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.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * For requesting online accounts info from remote peer, given our list of online accounts. + * + * Identical to V2, but added for consistency, since V2 will ultimately be phased out + */ +public class GetOnlineAccountsV3Message extends Message { + private List onlineAccounts; + private byte[] cachedData; + + public GetOnlineAccountsV3Message(List onlineAccounts) { + this(-1, onlineAccounts); + } + + private GetOnlineAccountsV3Message(int id, List onlineAccounts) { + super(id, MessageType.GET_ONLINE_ACCOUNTS_V2); + + this.onlineAccounts = onlineAccounts; + } + + public List getOnlineAccounts() { + return this.onlineAccounts; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + int accountCount = bytes.getInt(); + + List onlineAccounts = new ArrayList<>(accountCount); + + while (accountCount > 0) { + long timestamp = bytes.getLong(); + + for (int i = 0; i < accountCount; ++i) { + byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + bytes.get(publicKey); + + onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey)); + } + + if (bytes.hasRemaining()) { + accountCount = bytes.getInt(); + } else { + // we've finished + accountCount = 0; + } + } + + return new GetOnlineAccountsV3Message(id, onlineAccounts); + } + + @Override + protected synchronized byte[] toData() { + if (this.cachedData != null) + return this.cachedData; + + // Shortcut in case we have no online accounts + if (this.onlineAccounts.isEmpty()) { + this.cachedData = Ints.toByteArray(0); + return this.cachedData; + } + + // How many of each timestamp + Map countByTimestamp = new HashMap<>(); + + for (int i = 0; i < this.onlineAccounts.size(); ++i) { + OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); + 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) + + this.onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH; + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize); + + for (long timestamp : countByTimestamp.keySet()) { + bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp))); + + bytes.write(Longs.toByteArray(timestamp)); + + for (int i = 0; i < this.onlineAccounts.size(); ++i) { + OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); + + if (onlineAccountData.getTimestamp() == timestamp) + bytes.write(onlineAccountData.getPublicKey()); + } + } + + this.cachedData = bytes.toByteArray(); + return this.cachedData; + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index b06a5133..65996232 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -76,10 +76,10 @@ public abstract class Message { BLOCK_SUMMARIES(70), GET_BLOCK_SUMMARIES(71), - ONLINE_ACCOUNTS(80), - GET_ONLINE_ACCOUNTS(81), ONLINE_ACCOUNTS_V2(82), GET_ONLINE_ACCOUNTS_V2(83), + ONLINE_ACCOUNTS_V3(84), + GET_ONLINE_ACCOUNTS_V3(85), ARBITRARY_DATA(90), GET_ARBITRARY_DATA(91), diff --git a/src/main/java/org/qortal/network/message/OnlineAccountsMessage.java b/src/main/java/org/qortal/network/message/OnlineAccountsMessage.java deleted file mode 100644 index 02c46717..00000000 --- a/src/main/java/org/qortal/network/message/OnlineAccountsMessage.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.qortal.data.network.OnlineAccountData; -import org.qortal.transform.Transformer; - -import com.google.common.primitives.Ints; -import com.google.common.primitives.Longs; - -public class OnlineAccountsMessage extends Message { - private static final int MAX_ACCOUNT_COUNT = 5000; - - private List onlineAccounts; - - public OnlineAccountsMessage(List onlineAccounts) { - this(-1, onlineAccounts); - } - - private OnlineAccountsMessage(int id, List onlineAccounts) { - super(id, MessageType.ONLINE_ACCOUNTS); - - this.onlineAccounts = onlineAccounts.stream().limit(MAX_ACCOUNT_COUNT).collect(Collectors.toList()); - } - - public List getOnlineAccounts() { - return this.onlineAccounts; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - final int accountCount = bytes.getInt(); - - List onlineAccounts = new ArrayList<>(accountCount); - - for (int i = 0; i < Math.min(MAX_ACCOUNT_COUNT, accountCount); ++i) { - long timestamp = bytes.getLong(); - - byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; - bytes.get(signature); - - byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; - bytes.get(publicKey); - - OnlineAccountData onlineAccountData = new OnlineAccountData(timestamp, signature, publicKey); - onlineAccounts.add(onlineAccountData); - } - - return new OnlineAccountsMessage(id, onlineAccounts); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.onlineAccounts.size())); - - for (int i = 0; i < this.onlineAccounts.size(); ++i) { - OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); - - bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp())); - - bytes.write(onlineAccountData.getSignature()); - - bytes.write(onlineAccountData.getPublicKey()); - } - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java b/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java new file mode 100644 index 00000000..a7d39506 --- /dev/null +++ b/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java @@ -0,0 +1,139 @@ +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.io.UnsupportedEncodingException; +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 mempow nonce values and a recent block signature + */ +public class OnlineAccountsV3Message extends Message { + private List onlineAccounts; + private byte[] cachedData; + + public OnlineAccountsV3Message(List onlineAccounts) { + this(-1, onlineAccounts); + } + + private OnlineAccountsV3Message(int id, List onlineAccounts) { + super(id, MessageType.ONLINE_ACCOUNTS_V2); + + this.onlineAccounts = onlineAccounts; + } + + public List getOnlineAccounts() { + return this.onlineAccounts; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + int accountCount = bytes.getInt(); + + List 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); + + byte[] reducedBlockSignature = new byte[Transformer.REDUCED_SIGNATURE_LENGTH]; + bytes.get(reducedBlockSignature); + + int nonceCount = bytes.getInt(); + List nonces = new ArrayList<>(); + for (int n = 0; n < nonceCount; ++n) { + Integer nonce = bytes.getInt(); + nonces.add(nonce); + } + + onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonces, reducedBlockSignature)); + } + + if (bytes.hasRemaining()) { + accountCount = bytes.getInt(); + } else { + // we've finished + accountCount = 0; + } + } + + return new OnlineAccountsV3Message(id, onlineAccounts); + } + + @Override + protected synchronized byte[] toData() { + if (this.cachedData != null) + return this.cachedData; + + // Shortcut in case we have no online accounts + if (this.onlineAccounts.isEmpty()) { + this.cachedData = Ints.toByteArray(0); + return this.cachedData; + } + + // How many of each timestamp + Map countByTimestamp = new HashMap<>(); + + for (int i = 0; i < this.onlineAccounts.size(); ++i) { + OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); + 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) + + this.onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH); + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize); + + for (long timestamp : countByTimestamp.keySet()) { + bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp))); + + bytes.write(Longs.toByteArray(timestamp)); + + for (int i = 0; i < this.onlineAccounts.size(); ++i) { + OnlineAccountData onlineAccountData = this.onlineAccounts.get(i); + + if (onlineAccountData.getTimestamp() == timestamp) { + bytes.write(onlineAccountData.getSignature()); + + bytes.write(onlineAccountData.getPublicKey()); + + bytes.write(onlineAccountData.getReducedBlockSignature()); + + int nonceCount = onlineAccountData.getNonces() != null ? onlineAccountData.getNonces().size() : 0; + bytes.write(Ints.toByteArray(nonceCount)); + + for (int n = 0; n < nonceCount; ++n) { + int nonce = onlineAccountData.getNonces().get(i); + bytes.write(Ints.toByteArray(nonce)); + } + } + } + } + + this.cachedData = bytes.toByteArray(); + return this.cachedData; + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 24fbfff6..d9791475 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -273,6 +273,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; + // Data storage (QDN) @@ -740,6 +745,10 @@ public class Settings { return this.testNtpOffset; } + public boolean isOnlineAccountsMemPoWEnabled() { + return this.onlineAccountsMemPoWEnabled; + } + public long getRepositoryBackupInterval() { return this.repositoryBackupInterval; } diff --git a/src/main/java/org/qortal/transform/Transformer.java b/src/main/java/org/qortal/transform/Transformer.java index e78d3284..3f2a44d3 100644 --- a/src/main/java/org/qortal/transform/Transformer.java +++ b/src/main/java/org/qortal/transform/Transformer.java @@ -18,6 +18,8 @@ public abstract class Transformer { public static final int SIGNATURE_LENGTH = 64; public static final int TIMESTAMP_LENGTH = LONG_LENGTH; + public static final int REDUCED_SIGNATURE_LENGTH = 8; + public static final int MD5_LENGTH = 16; public static final int SHA256_LENGTH = 32; public static final int AES256_LENGTH = 32; diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index a55e9cb6..b5999352 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -18,6 +18,7 @@ "onlineAccountSignaturesMinLifetime": 43200000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 5.00 }, { "height": 259201, "reward": 4.75 }, diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java index f2e0130e..e4ffc4b6 100644 --- a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java +++ b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java @@ -63,18 +63,6 @@ public class OnlineAccountsTests extends Common { assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size()); assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut)); - - Message oldMessageOut = new GetOnlineAccountsMessage(onlineAccountsOut); - byte[] oldMessageBytes = oldMessageOut.toBytes(); - - long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count(); - - System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d", - onlineAccountsOut.size(), - numTimestamps, - numTimestamps != 1 ? "s" : "", - oldMessageBytes.length, - messageBytes.length)); } @Test @@ -92,18 +80,6 @@ public class OnlineAccountsTests extends Common { assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size()); assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut)); - - Message oldMessageOut = new OnlineAccountsMessage(onlineAccountsOut); - byte[] oldMessageBytes = oldMessageOut.toBytes(); - - long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count(); - - System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d", - onlineAccountsOut.size(), - numTimestamps, - numTimestamps != 1 ? "s" : "", - oldMessageBytes.length, - messageBytes.length)); } private List generateOnlineAccounts(boolean withSignatures) { diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 2932324f..d58d12a4 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 9805f98b..7a12831e 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 6650592b..fac40d49 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index c2f89112..8545dae2 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index ddf6de27..6635d8da 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index db4c2124..887c9c21 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 165b5d86..fed9ad4d 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 }, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 57fe5936..7dd05e8d 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -13,6 +13,7 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, + "onlineAccountsMemoryPoWTimestamp": 9999999999999, "rewardsByHeight": [ { "height": 1, "reward": 100 }, { "height": 11, "reward": 10 },