From 504cfc6a741051ccd4a4ad087d1356d6c85c3e5d Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 13 Sep 2019 14:21:04 +0100 Subject: [PATCH] Interim commit: online accounts and account levels Safety commit in case of data loss! Lots of changes to do with "online accounts", including: * networking + GET_ONLINE_ACCOUNTS * ONLINE_ACCOUNTS messages & handling + Changes to serialization of block data to include online accounts info * block-related + Adding online accounts info when generating blocks + Validating online accounts info in block data + Repository changes to store online accounts info * Controller + Managing in-memory cache of online accounts + Updating/broadcasting our online accounts * BlockChain config Added "account levels", so new code/changes required in the usual places, like: * transaction data object * repository * transaction transformer * transaction processing --- src/main/java/org/qora/account/Account.java | 12 + .../qora/api/resource/AddressesResource.java | 19 +- src/main/java/org/qora/block/Block.java | 118 ++++++- src/main/java/org/qora/block/BlockChain.java | 14 + .../java/org/qora/block/BlockGenerator.java | 4 + .../java/org/qora/controller/Controller.java | 226 +++++++++++++ .../org/qora/data/account/AccountData.java | 14 +- .../java/org/qora/data/block/BlockData.java | 31 +- .../org/qora/data/network/OnlineAccount.java | 47 +++ .../AccountLevelTransactionData.java | 91 +++++ .../ProxyForgingTransactionData.java | 3 + .../data/transaction/TransactionData.java | 3 +- .../message/GetOnlineAccountsMessage.java | 75 +++++ .../org/qora/network/message/Message.java | 4 +- .../message/OnlineAccountsMessage.java | 82 +++++ .../qora/repository/AccountRepository.java | 22 ++ .../hsqldb/HSQLDBAccountRepository.java | 71 +++- .../hsqldb/HSQLDBBlockRepository.java | 13 +- .../hsqldb/HSQLDBDatabaseUpdates.java | 15 + ...QLDBAccountLevelTransactionRepository.java | 56 ++++ .../transaction/AccountLevelTransaction.java | 118 +++++++ .../org/qora/transaction/Transaction.java | 3 +- .../transform/block/BlockTransformer.java | 112 ++++++- .../AccountLevelTransactionTransformer.java | 93 ++++++ src/main/resources/blockchain.json | 194 +++-------- src/test/java/org/qora/test/OnlineTests.java | 316 ++++++++++++++++++ .../org/qora/test/SerializationTests.java | 71 ++++ .../java/org/qora/test/TransactionTests.java | 4 +- .../java/org/qora/test/common/FakePeer.java | 110 ++++++ .../org/qora/test/common/PeerMessage.java | 17 + 30 files changed, 1781 insertions(+), 177 deletions(-) create mode 100644 src/main/java/org/qora/data/network/OnlineAccount.java create mode 100644 src/main/java/org/qora/data/transaction/AccountLevelTransactionData.java create mode 100644 src/main/java/org/qora/network/message/GetOnlineAccountsMessage.java create mode 100644 src/main/java/org/qora/network/message/OnlineAccountsMessage.java create mode 100644 src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAccountLevelTransactionRepository.java create mode 100644 src/main/java/org/qora/transaction/AccountLevelTransaction.java create mode 100644 src/main/java/org/qora/transform/transaction/AccountLevelTransactionTransformer.java create mode 100644 src/test/java/org/qora/test/OnlineTests.java create mode 100644 src/test/java/org/qora/test/common/FakePeer.java create mode 100644 src/test/java/org/qora/test/common/PeerMessage.java diff --git a/src/main/java/org/qora/account/Account.java b/src/main/java/org/qora/account/Account.java index ba4ea7f1..ed60774c 100644 --- a/src/main/java/org/qora/account/Account.java +++ b/src/main/java/org/qora/account/Account.java @@ -250,4 +250,16 @@ public class Account { this.repository.getAccountRepository().setForgingEnabler(accountData); } + // Account level + + public Integer getLevel() throws DataException { + return this.repository.getAccountRepository().getLevel(this.address); + } + + public void setLevel(int level) throws DataException { + AccountData accountData = this.buildAccountData(); + accountData.setLevel(level); + this.repository.getAccountRepository().setLevel(accountData); + } + } diff --git a/src/main/java/org/qora/api/resource/AddressesResource.java b/src/main/java/org/qora/api/resource/AddressesResource.java index 113ca6f6..de9a3c2c 100644 --- a/src/main/java/org/qora/api/resource/AddressesResource.java +++ b/src/main/java/org/qora/api/resource/AddressesResource.java @@ -30,9 +30,11 @@ import org.qora.api.ApiExceptionFactory; import org.qora.api.model.ProxyKeyRequest; import org.qora.api.resource.TransactionsResource; import org.qora.asset.Asset; +import org.qora.controller.Controller; import org.qora.crypto.Crypto; import org.qora.data.account.AccountData; import org.qora.data.account.ProxyForgerData; +import org.qora.data.network.OnlineAccount; import org.qora.data.transaction.ProxyForgingTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -149,7 +151,22 @@ public class AddressesResource { public boolean validate(@PathParam("address") String address) { return Crypto.isValidAddress(address); } - + + @GET + @Path("/online") + @Operation( + summary = "Return currently 'online' accounts", + responses = { + @ApiResponse( + description = "online accounts", + content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = OnlineAccount.class))) + ) + } + ) + public List getOnlineAccounts() { + return Controller.getInstance().getOnlineAccounts(); + } + @GET @Path("/generatingbalance/{address}") @Operation( diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index e1cf1fe6..192cb7b7 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -8,6 +8,7 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -20,12 +21,14 @@ import org.qora.account.PublicKeyAccount; import org.qora.asset.Asset; import org.qora.at.AT; import org.qora.block.BlockChain; +import org.qora.controller.Controller; import org.qora.crypto.Crypto; import org.qora.data.account.ProxyForgerData; import org.qora.data.at.ATData; import org.qora.data.at.ATStateData; import org.qora.data.block.BlockData; import org.qora.data.block.BlockTransactionData; +import org.qora.data.network.OnlineAccount; import org.qora.data.transaction.TransactionData; import org.qora.repository.ATRepository; import org.qora.repository.BlockRepository; @@ -37,12 +40,17 @@ import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ApprovalStatus; import org.qora.transaction.Transaction.TransactionType; import org.qora.transform.TransformationException; +import org.qora.transform.Transformer; import org.qora.transform.block.BlockTransformer; import org.qora.transform.transaction.TransactionTransformer; import org.qora.utils.Base58; import org.qora.utils.NTP; +import org.roaringbitmap.IntIterator; import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; + +import io.druid.extendedset.intset.ConciseSet; /* * Typical use-case scenarios: @@ -90,7 +98,12 @@ public class Block { TRANSACTION_PROCESSING_FAILED(53), TRANSACTION_ALREADY_PROCESSED(54), TRANSACTION_NEEDS_APPROVAL(55), - AT_STATES_MISMATCH(61); + AT_STATES_MISMATCH(61), + ONLINE_ACCOUNT_LEVEL_ZERO(70), + ONLINE_ACCOUNT_UNKNOWN(71), + ONLINE_ACCOUNT_SIGNATURES_MISSING(72), + ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73), + ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74); public final int value; @@ -134,6 +147,8 @@ public class Block { // TODO push this out to blockchain config file public static final int MAX_BLOCK_BYTES = 1048576; + public static final ConciseSet EMPTY_ONLINE_ACCOUNTS = new ConciseSet(); + // Constructors /** @@ -207,10 +222,48 @@ public class Block { byte[] reference = parentBlockData.getSignature(); BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance(); + // Fetch our list of online accounts + List onlineAccounts = Controller.getInstance().getOnlineAccounts(); + if (onlineAccounts.isEmpty()) + throw new IllegalStateException("No online accounts - not even our own?"); + + // Find newest online accounts timestamp + long onlineAccountsTimestamp = 0; + for (OnlineAccount onlineAccount : onlineAccounts) { + if (onlineAccount.getTimestamp() > onlineAccountsTimestamp) + onlineAccountsTimestamp = onlineAccount.getTimestamp(); + } + + // Map using account index (in list of proxy forger accounts) + Map indexedOnlineAccounts = new HashMap<>(); + for (OnlineAccount onlineAccount : onlineAccounts) { + // Disregard online accounts with different timestamps + if (onlineAccount.getTimestamp() != onlineAccountsTimestamp) + continue; + + int accountIndex = repository.getAccountRepository().getProxyAccountIndex(onlineAccount.getPublicKey()); + indexedOnlineAccounts.put(accountIndex, onlineAccount); + } + List accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet()); + accountIndexes.sort(null); + + // Convert to compressed integer set + ConciseSet onlineAccountsSet = new ConciseSet(); + onlineAccountsSet = onlineAccountsSet.convert(accountIndexes); + byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet); + + // Concatenate online account timestamp signatures (in correct order) + byte[] timestampSignatures = new byte[accountIndexes.size() * Transformer.SIGNATURE_LENGTH]; + for (int i = 0; i < accountIndexes.size(); ++i) { + Integer accountIndex = accountIndexes.get(i); + OnlineAccount onlineAccount = indexedOnlineAccounts.get(accountIndex); + System.arraycopy(onlineAccount.getSignature(), 0, timestampSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH); + } + byte[] generatorSignature; try { generatorSignature = generator - .sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator)); + .sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator, encodedOnlineAccounts)); } catch (TransformationException e) { throw new DataException("Unable to calculate next block generator signature", e); } @@ -229,7 +282,7 @@ public class Block { // This instance used for AT processing this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generator.getPublicKey(), generatorSignature, atCount, atFees); + generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures); // Requires this.blockData and this.transactions, sets this.ourAtStates and this.ourAtFees this.executeATs(); @@ -241,7 +294,7 @@ public class Block { // Rebuild blockData using post-AT-execute data this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generator.getPublicKey(), generatorSignature, atCount, atFees); + generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures); } /** @@ -273,7 +326,7 @@ public class Block { byte[] generatorSignature; try { generatorSignature = generator - .sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator)); + .sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator, this.blockData.getEncodedOnlineAccounts())); } catch (TransformationException e) { throw new DataException("Unable to calculate next block generator signature", e); } @@ -289,8 +342,12 @@ public class Block { int atCount = newBlock.ourAtStates.size(); BigDecimal atFees = newBlock.ourAtFees; + byte[] encodedOnlineAccounts = this.blockData.getEncodedOnlineAccounts(); + Long onlineAccountsTimestamp = this.blockData.getOnlineAccountsTimestamp(); + byte[] timestampSignatures = this.blockData.getTimestampSignatures(); + newBlock.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generator.getPublicKey(), generatorSignature, atCount, atFees); + generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures); // Resign to update transactions signature newBlock.sign(); @@ -811,6 +868,50 @@ public class Block { return ValidationResult.OK; } + public ValidationResult areOnlineAccountsValid() throws DataException { + // Expand block's online accounts indexes into actual accounts + ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts()); + + List expandedAccounts = new ArrayList<>(); + + IntIterator iterator = accountIndexes.iterator(); + while (iterator.hasNext()) { + int accountIndex = iterator.next(); + ProxyForgerData proxyAccountData = repository.getAccountRepository().getProxyAccountByIndex(accountIndex); + + // Check that claimed online account actually exists + if (proxyAccountData == null) + return ValidationResult.ONLINE_ACCOUNT_UNKNOWN; + + expandedAccounts.add(proxyAccountData); + } + + // Possibly check signatures if block is recent + long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime(); + if (this.blockData.getTimestamp() >= signatureRequirementThreshold) { + if (this.blockData.getTimestampSignatures() == null || this.blockData.getTimestampSignatures().length == 0) + return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING; + + if (this.blockData.getTimestampSignatures().length != expandedAccounts.size() * Transformer.SIGNATURE_LENGTH) + return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED; + + // Check signatures + List timestampSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getTimestampSignatures()); + byte[] message = Longs.toByteArray(this.blockData.getOnlineAccountsTimestamp()); + + for (int i = 0; i < timestampSignatures.size(); ++i) { + PublicKeyAccount account = new PublicKeyAccount(null, expandedAccounts.get(i).getProxyPublicKey()); + byte[] signature = timestampSignatures.get(i); + + if (!account.verify(signature, message)) + return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT; + } + } + + return ValidationResult.OK; + } + + /** * Returns whether Block is valid. *

@@ -863,6 +964,11 @@ public class Block { if (!isGeneratorValidToForge(parentBlock)) return ValidationResult.GENERATOR_NOT_ACCEPTED; + // Online Accounts + ValidationResult onlineAccountsResult = this.areOnlineAccountsValid(); + if (onlineAccountsResult != ValidationResult.OK) + return onlineAccountsResult; + // CIYAM ATs if (this.blockData.getATCount() != 0) { // Locally generated AT states should be valid so no need to re-execute them diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index db45fa6b..584cbdd4 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -110,6 +110,12 @@ public class BlockChain { private int maxProxyRelationships; + /** Minimum time to retain online account signatures (ms) for block validity checks. */ + private long onlineAccountSignaturesMinLifetime; + /** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */ + private long onlineAccountSignaturesMaxLifetime; + + // Constructors, etc. private BlockChain() { @@ -266,6 +272,14 @@ public class BlockChain { return this.maxProxyRelationships; } + public long getOnlineAccountSignaturesMinLifetime() { + return this.onlineAccountSignaturesMinLifetime; + } + + public long getOnlineAccountSignaturesMaxLifetime() { + return this.onlineAccountSignaturesMaxLifetime; + } + // Convenience methods for specific blockchain feature triggers public long getMessageReleaseHeight() { diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index bad4cb85..7e7540fb 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -92,6 +92,10 @@ public class BlockGenerator extends Thread { if (now == null) continue; + // No online accounts? (e.g. during startup) + if (Controller.getInstance().getOnlineAccounts().isEmpty()) + continue; + List forgingAccountsData = repository.getAccountRepository().getForgingAccounts(); // No forging accounts? if (forgingAccountsData.isEmpty()) diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index f5f8ba34..6c220005 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -18,19 +19,24 @@ import java.util.Random; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qora.account.PrivateKeyAccount; +import org.qora.account.PublicKeyAccount; import org.qora.api.ApiService; import org.qora.block.Block; import org.qora.block.BlockChain; import org.qora.block.BlockGenerator; import org.qora.controller.Synchronizer.SynchronizationResult; import org.qora.crypto.Crypto; +import org.qora.data.account.ForgingAccountData; import org.qora.data.block.BlockData; import org.qora.data.block.BlockSummaryData; +import org.qora.data.network.OnlineAccount; import org.qora.data.network.PeerData; import org.qora.data.transaction.ArbitraryTransactionData; import org.qora.data.transaction.ArbitraryTransactionData.DataType; @@ -46,6 +52,7 @@ import org.qora.network.message.BlockSummariesMessage; import org.qora.network.message.GetArbitraryDataMessage; import org.qora.network.message.GetBlockMessage; import org.qora.network.message.GetBlockSummariesMessage; +import org.qora.network.message.GetOnlineAccountsMessage; import org.qora.network.message.GetPeersMessage; import org.qora.network.message.GetSignaturesMessage; import org.qora.network.message.GetSignaturesV2Message; @@ -54,6 +61,7 @@ import org.qora.network.message.GetUnconfirmedTransactionsMessage; import org.qora.network.message.HeightMessage; import org.qora.network.message.HeightV2Message; import org.qora.network.message.Message; +import org.qora.network.message.OnlineAccountsMessage; import org.qora.network.message.SignaturesMessage; import org.qora.network.message.TransactionMessage; import org.qora.network.message.TransactionSignaturesMessage; @@ -72,6 +80,8 @@ import org.qora.utils.Base58; import org.qora.utils.NTP; import org.qora.utils.Triple; +import com.google.common.primitives.Longs; + public class Controller extends Thread { static { @@ -94,6 +104,11 @@ public class Controller extends Thread { private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000; // ms private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000; // ms + // To do with online accounts list + private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000; // ms + private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000; + private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000); + private static volatile boolean isStopping = false; private static BlockGenerator blockGenerator = null; private static volatile boolean requestSync = false; @@ -108,6 +123,8 @@ public class Controller extends Thread { private long repositoryBackupTimestamp = startTime + REPOSITORY_BACKUP_PERIOD; // ms private long ntpCheckTimestamp = startTime; // ms private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms + private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms + /** Whether BlockGenerator is allowed to generate blocks. Mostly determined by system clock accuracy. */ private volatile boolean isGenerationAllowed = false; @@ -138,6 +155,9 @@ public class Controller extends Thread { /** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly generated block. */ private final ReentrantLock blockchainLock = new ReentrantLock(); + /** Cache of 'online accounts' */ + List onlineAccounts = new ArrayList<>(); + // Constructors private Controller() { @@ -198,6 +218,7 @@ public class Controller extends Thread { return this.chainTip.get(); } + /** Cache new blockchain tip, and also wipe cache of online accounts. */ public void setChainTip(BlockData blockData) { this.chainTip.set(blockData); } @@ -374,6 +395,12 @@ public class Controller extends Thread { requestSysTrayUpdate = false; updateSysTray(); } + + // Perform tasks to do with managing online accounts list + if (now >= onlineAccountsTasksTimestamp) { + onlineAccountsTasksTimestamp = now + ONLINE_ACCOUNTS_TASKS_INTERVAL; + performOnlineAccountsTasks(); + } } } catch (InterruptedException e) { // Fall-through to exit @@ -1132,6 +1159,53 @@ public class Controller extends Thread { break; } + case GET_ONLINE_ACCOUNTS: { + 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()) { + OnlineAccount onlineAccount = iterator.next(); + + for (int i = 0; i < excludeAccounts.size(); ++i) { + OnlineAccount excludeAccount = excludeAccounts.get(i); + + if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.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)); + + break; + } + + case ONLINE_ACCOUNTS: { + OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message; + + List onlineAccounts = onlineAccountsMessage.getOnlineAccounts(); + LOGGER.trace(() -> String.format("Received %d online accounts from %s", onlineAccounts.size(), peer)); + + for (OnlineAccount onlineAccount : onlineAccounts) + this.verifyAndAddAccount(onlineAccount); + + break; + } + default: LOGGER.debug(String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1140,6 +1214,158 @@ public class Controller extends Thread { // Utilities + private void verifyAndAddAccount(OnlineAccount onlineAccount) { + // We would check timestamp is 'recent' here + + // Verify + byte[] data = Longs.toByteArray(onlineAccount.getTimestamp()); + PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey()); + if (!otherAccount.verify(onlineAccount.getSignature(), data)) { + LOGGER.trace(() -> String.format("Rejecting invalid online account %s", otherAccount.getAddress())); + return; + } + + synchronized (this.onlineAccounts) { + OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccount.getPublicKey())).findFirst().orElse(null); + + if (existingAccount != null) { + if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) { + this.onlineAccounts.remove(existingAccount); + + LOGGER.trace(() -> String.format("Updated online account %s with timestamp %d (was %d)", otherAccount.getAddress(), onlineAccount.getTimestamp(), existingAccount.getTimestamp())); + } else { + LOGGER.trace(() -> String.format("Not updating existing online account %s", otherAccount.getAddress())); + + return; + } + } else { + LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp())); + } + + this.onlineAccounts.add(onlineAccount); + } + } + + private void performOnlineAccountsTasks() { + final long now = System.currentTimeMillis(); + + // Expire old entries + final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD; + synchronized (this.onlineAccounts) { + Iterator iterator = this.onlineAccounts.iterator(); + while (iterator.hasNext()) { + OnlineAccount onlineAccount = iterator.next(); + + if (onlineAccount.getTimestamp() < cutoffThreshold) { + iterator.remove(); + + LOGGER.trace(() -> { + PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey()); + return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp()); + }); + } + } + } + + // Request data from another peer + Message message; + synchronized (this.onlineAccounts) { + message = new GetOnlineAccountsMessage(this.onlineAccounts); + } + Network.getInstance().broadcast((peer) -> message); + + // Refresh our onlineness? + sendOurOnlineAccountsInfo(); + } + + private void sendOurOnlineAccountsInfo() { + final Long now = NTP.getTime(); + if (now == null) + return; + + List forgingAccounts; + try (final Repository repository = RepositoryManager.getRepository()) { + forgingAccounts = repository.getAccountRepository().getForgingAccounts(); + + // We have no accounts, but don't reset timestamp + if (forgingAccounts.isEmpty()) + return; + + // Only proxy forging accounts allowed + Iterator iterator = forgingAccounts.iterator(); + while (iterator.hasNext()) { + ForgingAccountData forgingAccountData = iterator.next(); + + if (!repository.getAccountRepository().isProxyPublicKey(forgingAccountData.getPublicKey())) + iterator.remove(); + } + } catch (DataException e) { + LOGGER.warn("Repository issue trying to fetch forging accounts: " + e.getMessage()); + return; + } + + + // 'current' timestamp + final long onlineAccountsTimestamp = Controller.toOnlineAccountTimestamp(now); + boolean hasInfoChanged = false; + + byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp); + List ourOnlineAccounts = new ArrayList<>(); + + FORGING_ACCOUNTS: + for (ForgingAccountData forgingAccountData : forgingAccounts) { + PrivateKeyAccount forgingAccount = new PrivateKeyAccount(null, forgingAccountData.getSeed()); + + byte[] signature = forgingAccount.sign(timestampBytes); + byte[] publicKey = forgingAccount.getPublicKey(); + + // Our account is online + OnlineAccount onlineAccount = new OnlineAccount(onlineAccountsTimestamp, signature, publicKey); + synchronized (this.onlineAccounts) { + Iterator iterator = this.onlineAccounts.iterator(); + while (iterator.hasNext()) { + OnlineAccount account = iterator.next(); + + if (Arrays.equals(account.getPublicKey(), forgingAccount.getPublicKey())) { + // If onlineAccount is already present, with same timestamp, then move on to next forgingAccount + if (account.getTimestamp() == onlineAccountsTimestamp) + continue FORGING_ACCOUNTS; + + // If onlineAccount is already present, but with older timestamp, then remove it + iterator.remove(); + break; + } + } + + this.onlineAccounts.add(onlineAccount); + } + + LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", forgingAccount.getAddress(), onlineAccountsTimestamp)); + ourOnlineAccounts.add(onlineAccount); + hasInfoChanged = true; + } + + if (!hasInfoChanged) + return; + + Message message = new OnlineAccountsMessage(ourOnlineAccounts); + Network.getInstance().broadcast((peer) -> message); + + LOGGER.trace(( )-> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp)); + } + + public static long toOnlineAccountTimestamp(long timestamp) { + return (timestamp / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS; + } + + public List getOnlineAccounts() { + final long onlineTimestamp = Controller.toOnlineAccountTimestamp(NTP.getTime()); + + synchronized (this.onlineAccounts) { + return this.onlineAccounts.stream().filter(account -> account.getTimestamp() == onlineTimestamp).collect(Collectors.toList()); + } + } + public byte[] fetchArbitraryData(byte[] signature) throws InterruptedException { // Build request Message getArbitraryDataMessage = new GetArbitraryDataMessage(signature); diff --git a/src/main/java/org/qora/data/account/AccountData.java b/src/main/java/org/qora/data/account/AccountData.java index 7cb92604..2d21806f 100644 --- a/src/main/java/org/qora/data/account/AccountData.java +++ b/src/main/java/org/qora/data/account/AccountData.java @@ -16,6 +16,7 @@ public class AccountData { protected int defaultGroupId; protected int flags; protected String forgingEnabler; + protected int level; // Constructors @@ -23,17 +24,18 @@ public class AccountData { protected AccountData() { } - public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler) { + public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler, int level) { this.address = address; this.reference = reference; this.publicKey = publicKey; this.defaultGroupId = defaultGroupId; this.flags = flags; this.forgingEnabler = forgingEnabler; + this.level = level; } public AccountData(String address) { - this(address, null, null, Group.NO_GROUP, 0, null); + this(address, null, null, Group.NO_GROUP, 0, null, 0); } // Getters/Setters @@ -82,6 +84,14 @@ public class AccountData { this.forgingEnabler = forgingEnabler; } + public int getLevel() { + return this.level; + } + + public void setLevel(int level) { + this.level = level; + } + // Comparison @Override diff --git a/src/main/java/org/qora/data/block/BlockData.java b/src/main/java/org/qora/data/block/BlockData.java index 7a8f624b..79de1ad8 100644 --- a/src/main/java/org/qora/data/block/BlockData.java +++ b/src/main/java/org/qora/data/block/BlockData.java @@ -32,6 +32,9 @@ public class BlockData implements Serializable { private byte[] generatorSignature; private int atCount; private BigDecimal atFees; + private byte[] encodedOnlineAccounts; + private Long onlineAccountsTimestamp; + private byte[] timestampSignatures; // Constructors @@ -40,7 +43,8 @@ public class BlockData implements Serializable { } public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, - BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) { + BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees, + byte[] encodedOnlineAccounts, Long onlineAccountsTimestamp, byte[] timestampSignatures) { this.version = version; this.reference = reference; this.transactionCount = transactionCount; @@ -53,6 +57,9 @@ public class BlockData implements Serializable { this.generatorSignature = generatorSignature; this.atCount = atCount; this.atFees = atFees; + this.encodedOnlineAccounts = encodedOnlineAccounts; + this.onlineAccountsTimestamp = onlineAccountsTimestamp; + this.timestampSignatures = timestampSignatures; if (this.generatorSignature != null && this.transactionsSignature != null) this.signature = Bytes.concat(this.generatorSignature, this.transactionsSignature); @@ -60,6 +67,12 @@ public class BlockData implements Serializable { this.signature = null; } + public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, + BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) { + this(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees, + null, null, null); + } + // Getters/setters public byte[] getSignature() { @@ -150,6 +163,22 @@ public class BlockData implements Serializable { this.atFees = atFees; } + public byte[] getEncodedOnlineAccounts() { + return this.encodedOnlineAccounts; + } + + public Long getOnlineAccountsTimestamp() { + return onlineAccountsTimestamp; + } + + public void setOnlineAccountsTimestamp(Long onlineAccountsTimestamp) { + this.onlineAccountsTimestamp = onlineAccountsTimestamp; + } + + public byte[] getTimestampSignatures() { + return this.timestampSignatures; + } + // JAXB special @XmlElement(name = "generatorAddress") diff --git a/src/main/java/org/qora/data/network/OnlineAccount.java b/src/main/java/org/qora/data/network/OnlineAccount.java new file mode 100644 index 00000000..daf6a3b5 --- /dev/null +++ b/src/main/java/org/qora/data/network/OnlineAccount.java @@ -0,0 +1,47 @@ +package org.qora.data.network; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +import org.qora.account.PublicKeyAccount; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class OnlineAccount { + + protected long timestamp; + protected byte[] signature; + protected byte[] publicKey; + + // Constructors + + // necessary for JAXB serialization + protected OnlineAccount() { + } + + public OnlineAccount(long timestamp, byte[] signature, byte[] publicKey) { + this.timestamp = timestamp; + this.signature = signature; + this.publicKey = publicKey; + } + + public long getTimestamp() { + return this.timestamp; + } + + public byte[] getSignature() { + return this.signature; + } + + public byte[] getPublicKey() { + return this.publicKey; + } + + // For JAXB + @XmlElement(name = "address") + protected String getAddress() { + return new PublicKeyAccount(null, this.publicKey).getAddress(); + } + +} diff --git a/src/main/java/org/qora/data/transaction/AccountLevelTransactionData.java b/src/main/java/org/qora/data/transaction/AccountLevelTransactionData.java new file mode 100644 index 00000000..71f3a0c5 --- /dev/null +++ b/src/main/java/org/qora/data/transaction/AccountLevelTransactionData.java @@ -0,0 +1,91 @@ +package org.qora.data.transaction; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; +import org.qora.account.GenesisAccount; +import org.qora.block.GenesisBlock; +import org.qora.transaction.Transaction.TransactionType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +@Schema(allOf = {TransactionData.class}) +// JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below: +@XmlDiscriminatorValue("ACCOUNT_LEVEL") +public class AccountLevelTransactionData extends TransactionData { + + private String target; + private int level; + private Integer previousLevel; + + // Constructors + + // For JAXB + protected AccountLevelTransactionData() { + super(TransactionType.ACCOUNT_LEVEL); + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + /* + * If we're being constructed as part of the genesis block info inside blockchain config + * and no specific creator's public key is supplied + * then use genesis account's public key. + */ + if (parent instanceof GenesisBlock.GenesisInfo && this.creatorPublicKey == null) + this.creatorPublicKey = GenesisAccount.PUBLIC_KEY; + } + + /** From repository */ + public AccountLevelTransactionData(BaseTransactionData baseTransactionData, + String target, int level, Integer previousLevel) { + super(TransactionType.ACCOUNT_LEVEL, baseTransactionData); + + this.target = target; + this.level = level; + this.previousLevel = previousLevel; + } + + /** From network/API */ + public AccountLevelTransactionData(BaseTransactionData baseTransactionData, + String target, int level) { + this(baseTransactionData, target, level, null); + } + + // Getters / setters + + public String getTarget() { + return this.target; + } + + public int getLevel() { + return this.level; + } + + public Integer getPreviousLevel() { + return this.previousLevel; + } + + public void setPreviousLevel(Integer previousLevel) { + this.previousLevel = previousLevel; + } + + // Re-expose to JAXB + + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + public byte[] getAccountLevelCreatorPublicKey() { + return super.getCreatorPublicKey(); + } + + @XmlElement(name = "creatorPublicKey") + @Schema(name = "creatorPublicKey", description = "creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + public void setAccountLevelCreatorPublicKey(byte[] creatorPublicKey) { + super.setCreatorPublicKey(creatorPublicKey); + } + +} diff --git a/src/main/java/org/qora/data/transaction/ProxyForgingTransactionData.java b/src/main/java/org/qora/data/transaction/ProxyForgingTransactionData.java index 3498e31f..87dcce6a 100644 --- a/src/main/java/org/qora/data/transaction/ProxyForgingTransactionData.java +++ b/src/main/java/org/qora/data/transaction/ProxyForgingTransactionData.java @@ -7,6 +7,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qora.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -14,6 +15,8 @@ import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @Schema(allOf = {TransactionData.class}) +// JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below: +@XmlDiscriminatorValue("PROXY_FORGING") public class ProxyForgingTransactionData extends TransactionData { @Schema(example = "forger_public_key") diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index f9540cb2..88915c0b 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -39,7 +39,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; JoinGroupTransactionData.class, LeaveGroupTransactionData.class, GroupApprovalTransactionData.class, SetGroupTransactionData.class, UpdateAssetTransactionData.class, - AccountFlagsTransactionData.class, EnableForgingTransactionData.class, ProxyForgingTransactionData.class + AccountFlagsTransactionData.class, EnableForgingTransactionData.class, ProxyForgingTransactionData.class, + AccountLevelTransactionData.class }) //All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) diff --git a/src/main/java/org/qora/network/message/GetOnlineAccountsMessage.java b/src/main/java/org/qora/network/message/GetOnlineAccountsMessage.java new file mode 100644 index 00000000..e22f671d --- /dev/null +++ b/src/main/java/org/qora/network/message/GetOnlineAccountsMessage.java @@ -0,0 +1,75 @@ +package org.qora.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 org.qora.data.network.OnlineAccount; +import org.qora.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 = 1000; + + 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; + } + + public List getOnlineAccounts() { + return this.onlineAccounts; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + final int accountCount = bytes.getInt(); + + if (accountCount > MAX_ACCOUNT_COUNT) + return null; + + List onlineAccounts = new ArrayList<>(accountCount); + + for (int i = 0; i < accountCount; ++i) { + long timestamp = bytes.getLong(); + + byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + bytes.get(publicKey); + + onlineAccounts.add(new OnlineAccount(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) { + OnlineAccount onlineAccount = this.onlineAccounts.get(i); + bytes.write(Longs.toByteArray(onlineAccount.getTimestamp())); + + bytes.write(onlineAccount.getPublicKey()); + } + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qora/network/message/Message.java b/src/main/java/org/qora/network/message/Message.java index 4ed97a7c..fd10f8a9 100644 --- a/src/main/java/org/qora/network/message/Message.java +++ b/src/main/java/org/qora/network/message/Message.java @@ -68,7 +68,9 @@ public abstract class Message { GET_UNCONFIRMED_TRANSACTIONS(21), TRANSACTION_SIGNATURES(22), GET_ARBITRARY_DATA(23), - ARBITRARY_DATA(24); + ARBITRARY_DATA(24), + GET_ONLINE_ACCOUNTS(25), + ONLINE_ACCOUNTS(26); public final int value; public final Method fromByteBuffer; diff --git a/src/main/java/org/qora/network/message/OnlineAccountsMessage.java b/src/main/java/org/qora/network/message/OnlineAccountsMessage.java new file mode 100644 index 00000000..b7322d8f --- /dev/null +++ b/src/main/java/org/qora/network/message/OnlineAccountsMessage.java @@ -0,0 +1,82 @@ +package org.qora.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 org.qora.data.network.OnlineAccount; +import org.qora.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 = 1000; + + private List onlineAccounts; + + public OnlineAccountsMessage(List onlineAccounts) { + this(-1, onlineAccounts); + } + + private OnlineAccountsMessage(int id, List onlineAccounts) { + super(id, MessageType.ONLINE_ACCOUNTS); + + this.onlineAccounts = onlineAccounts; + } + + public List getOnlineAccounts() { + return this.onlineAccounts; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + final int accountCount = bytes.getInt(); + + if (accountCount > MAX_ACCOUNT_COUNT) + return null; + + List onlineAccounts = new ArrayList<>(accountCount); + + for (int i = 0; i < 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); + + OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey); + onlineAccounts.add(onlineAccount); + } + + 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) { + OnlineAccount onlineAccount = this.onlineAccounts.get(i); + + bytes.write(Longs.toByteArray(onlineAccount.getTimestamp())); + + bytes.write(onlineAccount.getSignature()); + + bytes.write(onlineAccount.getPublicKey()); + } + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index 6d655865..01937b5a 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -23,6 +23,9 @@ public interface AccountRepository { /** Returns account's flags or null if account not found. */ public Integer getFlags(String address) throws DataException; + /** Returns account's level or null if account not found. */ + public Integer getLevel(String address) throws DataException; + /** Returns whether account exists. */ public boolean accountExists(String address) throws DataException; @@ -57,6 +60,13 @@ public interface AccountRepository { */ public void setFlags(AccountData accountData) throws DataException; + /** + * Saves account's level, and public key if present, in repository. + *

+ * Note: ignores other fields like last reference, default groupID. + */ + public void setLevel(AccountData accountData) throws DataException; + /** * Saves account's forging enabler, and public key if present, in repository. *

@@ -97,6 +107,18 @@ public interface AccountRepository { public List findProxyAccounts(List recipients, List forgers, List involvedAddresses, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** + * Returns index in list of proxy accounts (sorted by public key). + *

+ * @return index (from 0) or null if publicKey not found in repository. + */ + public Integer getProxyAccountIndex(byte[] publicKey) throws DataException; + + /** + * Returns proxy forger data using index into list of proxy accounts. + */ + public ProxyForgerData getProxyAccountByIndex(int index) throws DataException; + public void save(ProxyForgerData proxyForgerData) throws DataException; /** Delete proxy forging relationship from repository using passed forger's public key and recipient's address. */ diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index d490d6d3..a2dc6237 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -26,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public AccountData getAccount(String address) throws DataException { - String sql = "SELECT reference, public_key, default_group_id, flags, forging_enabler FROM Accounts WHERE account = ?"; + String sql = "SELECT reference, public_key, default_group_id, flags, forging_enabler, level FROM Accounts WHERE account = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { if (resultSet == null) @@ -37,8 +37,9 @@ public class HSQLDBAccountRepository implements AccountRepository { int defaultGroupId = resultSet.getInt(3); int flags = resultSet.getInt(4); String forgingEnabler = resultSet.getString(5); + int level = resultSet.getInt(6); - return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler); + return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler, level); } catch (SQLException e) { throw new DataException("Unable to fetch account info from repository", e); } @@ -87,6 +88,20 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public Integer getLevel(String address) throws DataException { + String sql = "SELECT level FROM Accounts WHERE account = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) { + if (resultSet == null) + return null; + + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to fetch account's level from repository", e); + } + } + @Override public boolean accountExists(String address) throws DataException { try { @@ -184,6 +199,23 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public void setLevel(AccountData accountData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); + + saveHelper.bind("account", accountData.getAddress()).bind("level", accountData.getLevel()); + + byte[] publicKey = accountData.getPublicKey(); + if (publicKey != null) + saveHelper.bind("public_key", publicKey); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save account's level into repository", e); + } + } + @Override public void setForgingEnabler(AccountData accountData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); @@ -520,6 +552,41 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public Integer getProxyAccountIndex(byte[] publicKey) throws DataException { + String sql = "SELECT COUNT(*) FROM ProxyForgers WHERE proxy_public_key < ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) { + if (resultSet == null) + return null; + + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to determine account index in repository", e); + } + } + + @Override + public ProxyForgerData getProxyAccountByIndex(int index) throws DataException { + String sql = "SELECT forger, recipient, share, proxy_public_key FROM ProxyForgers " + + "ORDER BY proxy_public_key ASC " + + "OFFSET ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, index)) { + if (resultSet == null) + return null; + + byte[] forgerPublicKey = resultSet.getBytes(1); + String recipient = resultSet.getString(2); + BigDecimal share = resultSet.getBigDecimal(3); + byte[] proxyPublicKey = resultSet.getBytes(4); + + return new ProxyForgerData(forgerPublicKey, recipient, proxyPublicKey, share); + } catch (SQLException e) { + throw new DataException("Unable to fetch account info from repository", e); + } + } + @Override public void save(ProxyForgerData proxyForgerData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers"); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java index 3c5fbad0..9d4a8302 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java @@ -22,7 +22,8 @@ import static org.qora.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli public class HSQLDBBlockRepository implements BlockRepository { private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_count, AT_fees"; + + "transactions_signature, height, generation, generating_balance, generator, generator_signature, " + + "AT_count, AT_fees, online_accounts, online_accounts_timestamp, online_accounts_signatures"; protected HSQLDBRepository repository; @@ -47,9 +48,12 @@ public class HSQLDBBlockRepository implements BlockRepository { byte[] generatorSignature = resultSet.getBytes(10); int atCount = resultSet.getInt(11); BigDecimal atFees = resultSet.getBigDecimal(12); + byte[] encodedOnlineAccounts = resultSet.getBytes(13); + Long onlineAccountsTimestamp = getZonedTimestampMilli(resultSet, 14); + byte[] timestampSignatures = resultSet.getBytes(15); return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generatorPublicKey, generatorSignature, atCount, atFees); + generatorPublicKey, generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures); } catch (SQLException e) { throw new DataException("Error extracting data from result set", e); } @@ -316,7 +320,10 @@ public class HSQLDBBlockRepository implements BlockRepository { .bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight()) .bind("generation", toOffsetDateTime(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance()) .bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature()) - .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees()); + .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees()) + .bind("online_accounts", blockData.getEncodedOnlineAccounts()) + .bind("online_accounts_timestamp", toOffsetDateTime(blockData.getOnlineAccountsTimestamp())) + .bind("online_accounts_signatures", blockData.getTimestampSignatures()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index e5b6a0ae..a5ddd4b5 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -776,6 +776,21 @@ public class HSQLDBDatabaseUpdates { stmt.execute("ALTER TABLE Peers ADD COLUMN added_by VARCHAR(255)"); break; + case 54: + // Account 'level' + stmt.execute("ALTER TABLE Accounts ADD COLUMN level TINYINT NOT NULL DEFAULT 0"); + // Corresponding transaction to set level + stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QoraPublicKey NOT NULL, target QoraAddress NOT NULL, level INT NOT NULL, " + + "previous_level INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; + + case 55: + // Storage of which level 1+ accounts were 'online' for a particular block. Used to distribute block rewards. + stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts VARBINARY(1048576)"); + stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_timestamp TIMESTAMP WITH TIME ZONE"); + stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_signatures BLOB"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAccountLevelTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAccountLevelTransactionRepository.java new file mode 100644 index 00000000..29c00c23 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAccountLevelTransactionRepository.java @@ -0,0 +1,56 @@ +package org.qora.repository.hsqldb.transaction; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qora.data.transaction.AccountLevelTransactionData; +import org.qora.data.transaction.BaseTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.hsqldb.HSQLDBRepository; +import org.qora.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBAccountLevelTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBAccountLevelTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { + String sql = "SELECT target, level, previous_level FROM AccountLevelTransactions WHERE signature = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { + if (resultSet == null) + return null; + + String target = resultSet.getString(1); + int level = resultSet.getInt(2); + + Integer previousLevel = resultSet.getInt(3); + if (previousLevel == 0 && resultSet.wasNull()) + previousLevel = null; + + return new AccountLevelTransactionData(baseTransactionData, target, level, previousLevel); + } catch (SQLException e) { + throw new DataException("Unable to fetch account level transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + AccountLevelTransactionData accountLevelTransactionData = (AccountLevelTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("AccountLevelTransactions"); + + saveHelper.bind("signature", accountLevelTransactionData.getSignature()).bind("creator", accountLevelTransactionData.getCreatorPublicKey()) + .bind("target", accountLevelTransactionData.getTarget()).bind("level", accountLevelTransactionData.getLevel()) + .bind("previous_level", accountLevelTransactionData.getPreviousLevel()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save account level transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qora/transaction/AccountLevelTransaction.java b/src/main/java/org/qora/transaction/AccountLevelTransaction.java new file mode 100644 index 00000000..51534a77 --- /dev/null +++ b/src/main/java/org/qora/transaction/AccountLevelTransaction.java @@ -0,0 +1,118 @@ +package org.qora.transaction; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +import org.qora.account.Account; +import org.qora.account.GenesisAccount; +import org.qora.asset.Asset; +import org.qora.data.transaction.AccountLevelTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.repository.DataException; +import org.qora.repository.Repository; + +public class AccountLevelTransaction extends Transaction { + + // Properties + private AccountLevelTransactionData accountLevelTransactionData; + + // Constructors + + public AccountLevelTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.accountLevelTransactionData = (AccountLevelTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return Collections.emptyList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getCreator().getAddress())) + return true; + + if (address.equals(this.getTarget().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getCreator().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; + } + + // Navigation + + public Account getTarget() { + return new Account(this.repository, this.accountLevelTransactionData.getTarget()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + Account creator = getCreator(); + + // Only genesis account can modify level + if (!creator.getAddress().equals(new GenesisAccount(repository).getAddress())) + return ValidationResult.NO_FLAG_PERMISSION; + + // Check fee is zero or positive + if (accountLevelTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0) + return ValidationResult.NEGATIVE_FEE; + + // Check creator has enough funds + if (creator.getConfirmedBalance(Asset.QORA).compareTo(accountLevelTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + Account target = getTarget(); + Integer previousLevel = target.getLevel(); + + accountLevelTransactionData.setPreviousLevel(previousLevel); + + // Save this transaction with target account's previous level value + this.repository.getTransactionRepository().save(accountLevelTransactionData); + + // Set account's new level + target.setLevel(this.accountLevelTransactionData.getLevel()); + } + + @Override + public void orphan() throws DataException { + // Revert + Account target = getTarget(); + + Integer previousLevel = accountLevelTransactionData.getPreviousLevel(); + + // If previousLevel are null then account didn't exist before this transaction + if (previousLevel == null) + this.repository.getAccountRepository().delete(target.getAddress()); + else + target.setLevel(previousLevel); + + // Remove previous level from transaction itself + accountLevelTransactionData.setPreviousLevel(null); + this.repository.getTransactionRepository().save(accountLevelTransactionData); + } + +} diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index d30e53ee..269014c3 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -80,7 +80,8 @@ public abstract class Transaction { UPDATE_ASSET(35, true), ACCOUNT_FLAGS(36, false), ENABLE_FORGING(37, false), - PROXY_FORGING(38, false); + PROXY_FORGING(38, false), + ACCOUNT_LEVEL(39, false); public final int value; public final boolean needsApproval; diff --git a/src/main/java/org/qora/transform/block/BlockTransformer.java b/src/main/java/org/qora/transform/block/BlockTransformer.java index 41824329..79839ff9 100644 --- a/src/main/java/org/qora/transform/block/BlockTransformer.java +++ b/src/main/java/org/qora/transform/block/BlockTransformer.java @@ -27,6 +27,8 @@ import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; +import io.druid.extendedset.intset.ConciseSet; + public class BlockTransformer extends Transformer { private static final int VERSION_LENGTH = INT_LENGTH; @@ -47,6 +49,10 @@ public class BlockTransformer extends Transformer { protected static final int AT_FEES_LENGTH = LONG_LENGTH; protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; + protected static final int ONLINE_ACCOUNTS_SIZE_LENGTH = INT_LENGTH; + protected static final int ONLINE_ACCOUNTS_TIMESTAMP_LENGTH = LONG_LENGTH; + protected static final int ONLINE_ACCOUNTS_SIGNATURES_COUNT_LENGTH = INT_LENGTH; + protected static final int V2_AT_ENTRY_LENGTH = ADDRESS_LENGTH + MD5_LENGTH; protected static final int V4_AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + BIG_DECIMAL_LENGTH; @@ -187,13 +193,42 @@ public class BlockTransformer extends Transformer { totalFees = totalFees.add(transactionData.getFee()); } + // Online accounts info? + byte[] onlineAccounts = null; + byte[] timestampSignatures = null; + Long onlineAccountsTimestamp = null; + + if (version >= 4) { + int conciseSetLength = byteBuffer.getInt(); + + if (conciseSetLength > Block.MAX_BLOCK_BYTES) + throw new TransformationException("Byte data too long for online account info"); + + if ((conciseSetLength & 3) != 0) + throw new TransformationException("Byte data length not multiple of 4 for online account info"); + + onlineAccounts = new byte[conciseSetLength]; + byteBuffer.get(onlineAccounts); + + // Note: number of signatures, not byte length + int timestampSignaturesCount = byteBuffer.getInt(); + + if (timestampSignaturesCount > 0) { + // Online accounts timestamp is only present if there are also signatures + onlineAccountsTimestamp = byteBuffer.getLong(); + + timestampSignatures = new byte[timestampSignaturesCount * Transformer.SIGNATURE_LENGTH]; + byteBuffer.get(timestampSignatures); + } + } + if (byteBuffer.hasRemaining()) throw new TransformationException("Excess byte data found after parsing Block"); // We don't have a height! Integer height = null; BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generatorPublicKey, generatorSignature, atCount, atFees); + generatorPublicKey, generatorSignature, atCount, atFees, onlineAccounts, onlineAccountsTimestamp, timestampSignatures); return new Triple, List>(blockData, transactions, atStates); } @@ -202,9 +237,13 @@ public class BlockTransformer extends Transformer { BlockData blockData = block.getBlockData(); int blockLength = BASE_LENGTH; - if (blockData.getVersion() >= 4) + if (blockData.getVersion() >= 4) { blockLength += AT_BYTES_LENGTH + blockData.getATCount() * V4_AT_ENTRY_LENGTH; - else if (blockData.getVersion() >= 2) + blockLength += ONLINE_ACCOUNTS_SIZE_LENGTH + blockData.getEncodedOnlineAccounts().length; + blockLength += ONLINE_ACCOUNTS_SIGNATURES_COUNT_LENGTH; + if (blockData.getTimestampSignatures().length > 0) + blockLength += ONLINE_ACCOUNTS_TIMESTAMP_LENGTH + blockData.getTimestampSignatures().length; + } else if (blockData.getVersion() >= 2) blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getATCount() * V2_AT_ENTRY_LENGTH; try { @@ -271,6 +310,34 @@ public class BlockTransformer extends Transformer { bytes.write(TransactionTransformer.toBytes(transactionData)); } + // Online account info + if (blockData.getVersion() >= 4) { + byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts(); + + if (encodedOnlineAccounts != null) { + bytes.write(Ints.toByteArray(encodedOnlineAccounts.length)); + bytes.write(encodedOnlineAccounts); + } else { + bytes.write(Ints.toByteArray(0)); + } + + byte[] timestampSignatures = blockData.getTimestampSignatures(); + + if (timestampSignatures != null) { + // Note: we write the number of signatures, not the number of bytes + bytes.write(Ints.toByteArray(timestampSignatures.length / Transformer.SIGNATURE_LENGTH)); + + if (timestampSignatures.length > 0) { + // Only write online accounts timestamp if we have signatures + bytes.write(Longs.toByteArray(blockData.getOnlineAccountsTimestamp())); + + bytes.write(timestampSignatures); + } + } else { + bytes.write(Ints.toByteArray(0)); + } + } + return bytes.toByteArray(); } catch (IOException | DataException e) { throw new TransformationException("Unable to serialize block", e); @@ -285,13 +352,13 @@ public class BlockTransformer extends Transformer { byte[] generatorSignature = getGeneratorSignatureFromReference(blockData.getReference()); PublicKeyAccount generator = new PublicKeyAccount(null, blockData.getGeneratorPublicKey()); - return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator); + return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator, blockData.getEncodedOnlineAccounts()); } - public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator) + public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] encodedOnlineAccounts) throws TransformationException { try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH + encodedOnlineAccounts.length); bytes.write(generatorSignature); @@ -300,6 +367,8 @@ public class BlockTransformer extends Transformer { // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long. bytes.write(Bytes.ensureCapacity(generator.getPublicKey(), GENERATOR_LENGTH, 0)); + bytes.write(encodedOnlineAccounts); + return bytes.toByteArray(); } catch (IOException e) { throw new TransformationException(e); @@ -331,4 +400,35 @@ public class BlockTransformer extends Transformer { } } + public static byte[] encodeOnlineAccounts(ConciseSet onlineAccounts) { + return onlineAccounts.toByteBuffer().array(); + } + + public static ConciseSet decodeOnlineAccounts(byte[] encodedOnlineAccounts) { + int[] words = new int[encodedOnlineAccounts.length / 4]; + ByteBuffer.wrap(encodedOnlineAccounts).asIntBuffer().get(words); + return new ConciseSet(words, false); + } + + public static byte[] encodeTimestampSignatures(List signatures) { + byte[] encodedSignatures = new byte[signatures.size() * Transformer.SIGNATURE_LENGTH]; + + for (int i = 0; i < signatures.size(); ++i) + System.arraycopy(signatures.get(i), 0, encodedSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH); + + return encodedSignatures; + } + + public static List decodeTimestampSignatures(byte[] encodedSignatures) { + List signatures = new ArrayList<>(); + + for (int i = 0; i < encodedSignatures.length; i += Transformer.SIGNATURE_LENGTH) { + byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; + System.arraycopy(encodedSignatures, i, signature, 0, Transformer.SIGNATURE_LENGTH); + signatures.add(signature); + } + + return signatures; + } + } diff --git a/src/main/java/org/qora/transform/transaction/AccountLevelTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/AccountLevelTransactionTransformer.java new file mode 100644 index 00000000..45f591c6 --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/AccountLevelTransactionTransformer.java @@ -0,0 +1,93 @@ +package org.qora.transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.qora.block.BlockChain; +import org.qora.data.transaction.AccountLevelTransactionData; +import org.qora.data.transaction.BaseTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.transaction.Transaction.TransactionType; +import org.qora.transform.TransformationException; +import org.qora.utils.Serialization; + + +public class AccountLevelTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int TARGET_LENGTH = ADDRESS_LENGTH; + private static final int LEVEL_LENGTH = BYTE_LENGTH; + + private static final int EXTRAS_LENGTH = TARGET_LENGTH + LEVEL_LENGTH; + + protected static final TransactionLayout layout; + + static { + layout = new TransactionLayout(); + layout.add("txType: " + TransactionType.ACCOUNT_LEVEL.valueString, TransformationType.INT); + layout.add("timestamp", TransformationType.TIMESTAMP); + layout.add("transaction's groupID", TransformationType.INT); + layout.add("reference", TransformationType.SIGNATURE); + layout.add("account's public key", TransformationType.PUBLIC_KEY); + layout.add("target account's address", TransformationType.ADDRESS); + layout.add("level", TransformationType.BYTE); + layout.add("fee", TransformationType.AMOUNT); + layout.add("signature", TransformationType.SIGNATURE); + } + + public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + int txGroupId = 0; + if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp()) + txGroupId = byteBuffer.getInt(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String target = Serialization.deserializeAddress(byteBuffer); + + byte level = byteBuffer.get(); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, signature); + + return new AccountLevelTransactionData(baseTransactionData, target, level); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + return getBaseLength(transactionData) + EXTRAS_LENGTH; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + AccountLevelTransactionData accountLevelTransactionData = (AccountLevelTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + transformCommonBytes(transactionData, bytes); + + Serialization.serializeAddress(bytes, accountLevelTransactionData.getTarget()); + + bytes.write(accountLevelTransactionData.getLevel()); + + Serialization.serializeBigDecimal(bytes, accountLevelTransactionData.getFee()); + + if (accountLevelTransactionData.getSignature() != null) + bytes.write(accountLevelTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + +} diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 5d50c2f4..d9be50b8 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -1,173 +1,65 @@ { - "maxBalance": "10000000000", + "maxBalance": "10000000", "blockDifficultyInterval": 10, "minBlockTime": 60, "maxBlockTime": 300, "blockTimestampMargin": 2000, "maxBytesPerUnitFee": 1024, "unitFee": "1.0", - "useBrokenMD160ForAddresses": true, + "useBrokenMD160ForAddresses": false, "requireGroupForApproval": false, "defaultGroupId": 0, - "oneNamePerAccount": false, - "maxProxyRelationships": 8, + "oneNamePerAccount": true, + "maxProxyRelationships": 20, + "onlineAccountSignaturesMinLifetime": 2592000000, + "onlineAccountSignaturesMaxLifetime": 3196800000, "genesisInfo": { - "version": 1, - "timestamp": "1400247274336", - "generatingBalance": "10000000", + "version": 4, + "timestamp": "1568360000000", + "generatingBalance": "100000", "transactions": [ - { "type": "ISSUE_ASSET", "owner": "QLpLzqs4DW1FNJByeJ63qaqw3eAYCxfkjR", "assetName": "QORA", "description": "QORA coin", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC" }, - { "type": "GENESIS", "recipient": "QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW", "amount": "7032468.191" }, - { "type": "GENESIS", "recipient": "QVafvKkE5bZTkq8PcXvdaxwuLNN2DGCwYk", "amount": "1716146.084" }, - { "type": "GENESIS", "recipient": "QV42QQP7frYWqsVq536g7zSk97fUpf2ZSN", "amount": "5241707.06" }, - { "type": "GENESIS", "recipient": "QgkLTm5GkepJpgr53nAgUyYRsvmyHpb2zT", "amount": "854964.0816" }, - { "type": "GENESIS", "recipient": "Qc8kN338XQULMBuUa6mTqL5tipvELDhzeZ", "amount": "769467.6734" }, - { "type": "GENESIS", "recipient": "QQ81BA75jZcpjQBLZE1qcHrXV8ARC1DEec", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QeoSe4DscWX4AFkNBCdm4WS1V7QkUFSQLP", "amount": "854968.3564" }, - { "type": "GENESIS", "recipient": "Qdfu3Eh21ZVHNDY1xyNaFqTTEYscSmfSsm", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QeDSr4abXKRg9j5hTN3TK9UGuH3umThZ42", "amount": "4445813.224" }, - { "type": "GENESIS", "recipient": "QQKDuo1txYB9E2xim79YVR6SQ1ZbJtJtFX", "amount": "47023024.49" }, - { "type": "GENESIS", "recipient": "QLeaeGr4CDA95FmeMtFh8okJRMLoq8Cge5", "amount": "170992816.3" }, - { "type": "GENESIS", "recipient": "QSwN5oa8ZHWJmc6FeAJ8Xr1SHaEuSahw1J", "amount": "3419856.326" }, - { "type": "GENESIS", "recipient": "QWnoGd4a7iXqQmNEpUtCb1x7nWgcya8QbE", "amount": "17056533.43" }, - { "type": "GENESIS", "recipient": "QbJqhsJjcy3vkzsJ1kHvgn26pQF3sZEypc", "amount": "42705455.87" }, - { "type": "GENESIS", "recipient": "QiBhBcseKzaDnHKyqEJs8z1Xx2rSb9XhBr", "amount": "141069073.5" }, - { "type": "GENESIS", "recipient": "QTwYwxBhzivFEWY5yfzyz1pqhJ8XCroKwv", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QfikxUU15Dy1oxbcDNEcLeU5cHvbrceq3A", "amount": "17099281.63" }, - { "type": "GENESIS", "recipient": "QhdqBmKZeQ3Hg1XUuR5nKtAkw47tuoRi2q", "amount": "12824461.22" }, - { "type": "GENESIS", "recipient": "QaVNyTqsTHA6JWMcqntcJf1u9c3qid76xH", "amount": "128244612.2" }, - { "type": "GENESIS", "recipient": "QYaDa7bmgo5L9qkcfJKjhPPrQkvGjEoc7y", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QQPddvWaYf4pbCyVHEVoyfC72EiaAv4JhT", "amount": "25648922.45" }, - { "type": "GENESIS", "recipient": "QSQpTNtTZMwaDuNq56Jz73KHWXaey9JrT1", "amount": "26341443.35" }, - { "type": "GENESIS", "recipient": "QVjcFWE6TnGePGJEtbNc1thwD2sgHBLvUV", "amount": "42940528.25" }, - { "type": "GENESIS", "recipient": "Qga93mWNqTuJYx6o33vjUpFH7Cn4sxLyoG", "amount": "2564892.245" }, - { "type": "GENESIS", "recipient": "QXyHKyQPJnb4ejyTkvS26x9sjWnhTTJ1Uc", "amount": "10259568.98" }, - { "type": "GENESIS", "recipient": "QLurSSgJvW7WXHDFSobgfakgqXxjoZzwUH", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QadxfiARcsmwzE93wqq82yXFi3ykM2qdtS", "amount": "79118376.11" }, - { "type": "GENESIS", "recipient": "QRHhhtz3Cv9RPKB1QBBfkRmRfpXx8vkRa5", "amount": "22435418.54" }, - { "type": "GENESIS", "recipient": "Qh8UnEs55n8jcnBaBwVtrTGkFFFBDyrMqH", "amount": "128757590.7" }, - { "type": "GENESIS", "recipient": "QhF7Fu3f54CTYA7zBQ223NQEssi2yAbAcx", "amount": "258481290.8" }, - { "type": "GENESIS", "recipient": "QPk9VB6tigoifrUYQrw4arBNk7i8HEgsDD", "amount": "128244612.2" }, - { "type": "GENESIS", "recipient": "QXWJnEPsdtaLQAEutJFR4ySiMUJCWDzZJX", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QVFs42gM4Cixf4Y5vDFvKKxRAamUPMCAVq", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "Qec5ueWc4rcBrty47GZfFSqvLymxvcycFm", "amount": "129091026.7" }, - { "type": "GENESIS", "recipient": "QfYiztbDz1Nb9EMhgHidLycvuPN8HEcHEj", "amount": "128244612.2" }, - { "type": "GENESIS", "recipient": "QPdWsZtaZcAKqk2HWVhEVbws4qG5KUTXmg", "amount": "179285967.9" }, - { "type": "GENESIS", "recipient": "QVkNs5NcwQpsrCXpWzuMXkMehJr5mkvLVy", "amount": "8558190.456" }, - { "type": "GENESIS", "recipient": "Qg19DzyEfyZANx6JLy4GrSGF5LuZ2MLqyZ", "amount": "42748204.08" }, - { "type": "GENESIS", "recipient": "Qf3A8L5WJNHt1xZxmayrTp2d5owzdkcxM6", "amount": "50519827.58" }, - { "type": "GENESIS", "recipient": "QeKR4W6qkFJGF7Hmu7rSUzTSQiqJzZLXdt", "amount": "10216820.77" }, - { "type": "GENESIS", "recipient": "QWg7T5i3uBY3xeBLFTLYYruR15Ln11vwo4", "amount": "170992816.3" }, - { "type": "GENESIS", "recipient": "QUYdM5fHECPZxKQQAmoxoQa2aWg8TZYfPw", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QjhfEZCgrjUbnLRnWqWxzyYqKQpjjxkuA8", "amount": "86665653.61" }, - { "type": "GENESIS", "recipient": "QMA53u3wrzDoxC57CWUJePNdR8FoqinqUS", "amount": "85496408.16" }, - { "type": "GENESIS", "recipient": "QSuCp6mB5zNNeJKD62aq2hR9h84ks1WhHf", "amount": "161588211.4" }, - { "type": "GENESIS", "recipient": "QS2tCUk7GQefg4zGewwrumxSPmN6fgA7Xc", "amount": "170992816.3" }, - { "type": "GENESIS", "recipient": "Qcn6FZRxAgp3japtvjgUkBY6KPfbPZMZtM", "amount": "170992816.3" }, - { "type": "GENESIS", "recipient": "QZrmXZkRmjV2GwMt72Rr1ZqHJjv8raDk5J", "amount": "17099281.63" }, - { "type": "GENESIS", "recipient": "QeZzwGDfAHa132jb6r4rQHbgJstLuT8QJ3", "amount": "255875360.3" }, - { "type": "GENESIS", "recipient": "Qj3L139sMMuFvvjKQDwRnoSgKUnoMhDQs5", "amount": "76946767.34" }, - { "type": "GENESIS", "recipient": "QWJvpvbFRZHu7LRbY5MjzvrMBgzJNFYjCX", "amount": "178251461.4" }, - { "type": "GENESIS", "recipient": "QRyECqW54ywKVt4kZTEXyRY17aaFUaxzc4", "amount": "8772355.539" }, - { "type": "GENESIS", "recipient": "QgpH3K3ArkQTg15xjKqGq3BRgE3aNH9Q2P", "amount": "46766535.26" }, - { "type": "GENESIS", "recipient": "QVZ6pxi8e3K3S44zLbnrLSLwSoYT8CWbwV", "amount": "233172022.2" }, - { "type": "GENESIS", "recipient": "QNbA69dbnmwqJHLQeS9v63hSLZXXGkmtC6", "amount": "46626632.05" }, - { "type": "GENESIS", "recipient": "QgzudSKbcLUeQUhFngotVswDSkbU42dSMr", "amount": "83786479.99" }, - { "type": "GENESIS", "recipient": "QfkQ2UzKMBGPwj8Sm31SArjtXoka1ubU3i", "amount": "116345066.7" }, - { "type": "GENESIS", "recipient": "QgxHHNwawZeTmQ3i5d9enchi4T9VmzNZ5k", "amount": "155448014.8" }, - { "type": "GENESIS", "recipient": "QMNugJWNsLuV4Qmbzdf8r8RMEdXk5PNM69", "amount": "155448014.8" }, - { "type": "GENESIS", "recipient": "QVhWuJkCjStNMV4U8PtNM9Qz4PvLAEtVSj", "amount": "101041209.6" }, - { "type": "GENESIS", "recipient": "QXjNcckFG9gTr9YbiA3RrRhn3mPJ9zyR4G", "amount": "3108960.297" }, - { "type": "GENESIS", "recipient": "QThnuBadmExtxk81vhFKimSzbPaPcuPAdm", "amount": "155448014.8" }, - { "type": "GENESIS", "recipient": "QRc6sQthLHjfkmm2BUhu74g33XtkDoB7JP", "amount": "77773983.95" }, - { "type": "GENESIS", "recipient": "QcDLhirHkSbR4TLYeShLzHw61B8UGTFusk", "amount": "23317202.22" }, - { "type": "GENESIS", "recipient": "QXRnsXE6srHEf2abGh4eogs2mRsmNiuw6V", "amount": "5440680.519" }, - { "type": "GENESIS", "recipient": "QRJmEswbDw4x1kwsLyxtMS9533fv5cDvQV", "amount": "3886200.371" }, - { "type": "GENESIS", "recipient": "Qg43mCzWmFVwhVfx34g6shXnSU7U7amJNx", "amount": "6217920.593" }, - { "type": "GENESIS", "recipient": "QQ9PveFTW64yUcXEE6AxhokWCwhmn8F2TD", "amount": "8549640.816" }, - { "type": "GENESIS", "recipient": "QQaxJuTkW5XXn4DhhRekXpdXaWcsxEfCNG", "amount": "3886200.371" }, - { "type": "GENESIS", "recipient": "QifWFqW8XWL5mcNxtdr5z1LVC7XUu9tNSK", "amount": "3116732.697" }, - { "type": "GENESIS", "recipient": "QavhBKRN4vuyzHNNqcWxjcohRAJNTdTmh4", "amount": "154670774.8" }, - { "type": "GENESIS", "recipient": "QMQyR3Hybof8WpQsXPxh19AZFCj4Z4mmke", "amount": "77724007.42" }, - { "type": "GENESIS", "recipient": "QbT3GGjp1esTXtowVk2XCtBsKoRB8mkP61", "amount": "77724007.42" }, - { "type": "GENESIS", "recipient": "QT13tVMZEtbrgJEsBBcTtnyqGveC7mtqAb", "amount": "23317202.22" }, - { "type": "GENESIS", "recipient": "QegT2Ws5YjLQzEZ9YMzWsAZMBE8cAygHZN", "amount": "12606834" }, - { "type": "GENESIS", "recipient": "QXoKRBJiJGKwvdA3jkmoUhkM7y6vuMp2pn", "amount": "65117173.41" }, - { "type": "GENESIS", "recipient": "QY6SpdBzUev9ziqkmyaxESZSbdKwqGdedn", "amount": "89382608.53" }, - { "type": "GENESIS", "recipient": "QeMxyt1nEE7tbFbioc87xhiKb4szx5DsjY", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "QcTp3THGZvJ42f2mWsQrawGvgBoSHgHZyk", "amount": "39639243.78" }, - { "type": "GENESIS", "recipient": "QjSH91mTDN6TeV1naAcfwPhmRogufV4n1u", "amount": "23317202.22" }, - { "type": "GENESIS", "recipient": "QiFLELeLm2TFWsnknzje51wMdt3Srkjz8g", "amount": "1554480.148" }, - { "type": "GENESIS", "recipient": "QhxtJ3vvhsvVU9x2j5n2R3TXzutfLMUvBR", "amount": "23317202.22" }, - { "type": "GENESIS", "recipient": "QUtUSNQfqexZZkaZ2s9LcpqjnTezPTnuAx", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "Qg6sPLxNMYxjEDGLLaFkkWx6ip3py5fLEt", "amount": "777240.0742" }, - { "type": "GENESIS", "recipient": "QeLixskYbdkiAHmhBVMa2Pdi85YPFqw3Ed", "amount": "38862003.71" }, - { "type": "GENESIS", "recipient": "Qary17o9qvZ2fifiVC8tF5zoBJm79n18zA", "amount": "3893972.772" }, - { "type": "GENESIS", "recipient": "QLvCWDGwzwpR29XgiThMGDX2vxyFW5rFHB", "amount": "8790585.239" }, - { "type": "GENESIS", "recipient": "Qgc77fSoAoUSVJfq62GxxTin6dBtU7Y6Hb", "amount": "194310018.5" }, - { "type": "GENESIS", "recipient": "QPmPKjwPLCuRei6abuhMtMocxAEeSuLVcv", "amount": "23317202.22" }, - { "type": "GENESIS", "recipient": "QcGfZePUN7JHs9WEEkJhXGzALy4JybiS3N", "amount": "194224522.1" }, - { "type": "GENESIS", "recipient": "QSeXGwk7eQjR8j7bndJST19qWtM2qnqL1u", "amount": "38862003.71" }, - { "type": "GENESIS", "recipient": "QU9i68h71nKTg4gwc5yJHzNRQdQEswP7Kn", "amount": "139592317.3" }, - { "type": "GENESIS", "recipient": "QdKrZGCkwXSSeXJhVA1idDXsA4VFtrjPHN", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "QiYJ2B797xFpWYFu4XWivhGhyPXLU7S5Mr", "amount": "77724007.42" }, - { "type": "GENESIS", "recipient": "QWxqtsNXUWSjYns2wdngh4WBSWQzLoQHvx", "amount": "232613963.9" }, - { "type": "GENESIS", "recipient": "QTAGfu4FpTZ1bnvnd17YPtB3zabxfWKNeM", "amount": "101041209.6" }, - { "type": "GENESIS", "recipient": "QPtRxchgRdwdnoZRwhiAoa77AvVPNSRcQk", "amount": "114254290.9" }, - { "type": "GENESIS", "recipient": "QMcfoVc9Jat2pMFLHcuPEPnY6p6uBK6Dk7", "amount": "77724007.42" }, - { "type": "GENESIS", "recipient": "Qi84KosdwSWHZX3qz4WcMgqYGutBmj14dd", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "QjAtcHsgig2tvdGr5tR4oGmRarhuojrAK1", "amount": "2883560.675" }, - { "type": "GENESIS", "recipient": "QPJPNLP2NMHu5athB7ydezdTA6zviCV378", "amount": "6373368.608" }, - { "type": "GENESIS", "recipient": "QfVLpmLbuUnA1JEe9FmeUAzihoBvqYDp8B", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "QVVFdy6VLFqAFCb6XSBJLLZybiKgfgDDZV", "amount": "10725913.02" }, - { "type": "GENESIS", "recipient": "QVFXyeG1xpAR8Xg3u7oAmW8unueGAfeaKi", "amount": "31221733.78" }, - { "type": "GENESIS", "recipient": "QdtQtrM1h3TLtwAGCNyoTrW5HyiPRLhrPq", "amount": "138426457.2" }, - { "type": "GENESIS", "recipient": "QMukUMr84Mi2niz6rdhEJMkKJBve7uuRfe", "amount": "116586011.1" }, - { "type": "GENESIS", "recipient": "QZR8c7dmfwqGPujebFH1miQToJZ4JQfU1X", "amount": "217938116.8" }, - { "type": "GENESIS", "recipient": "QVV5Uu8eCxufTrBtquDKA96d7Kk8S4V7yX", "amount": "40091961.25" }, - { "type": "GENESIS", "recipient": "QY9YdgfTEUFvQ2UJszGS63qkwdENkW1PQ5", "amount": "154670774.8" }, - { "type": "GENESIS", "recipient": "QNgiswyhVyNJG4UMzvoSf29wDvGZqqs7WG", "amount": "11658601.11" }, - { "type": "GENESIS", "recipient": "QabjgFiY34oihNkUcy9hpFjQdCaypCShMe", "amount": "54406805.19" }, - { "type": "GENESIS", "recipient": "QionidPRekdshCTRL3c7idWWRAqGYcKaFN", "amount": "7772400.742" }, - { "type": "GENESIS", "recipient": "QcJdBJiVgiNBNg6ZwZAiEfYDMi5ZTQaYAa", "amount": "81386689.86" }, - { "type": "GENESIS", "recipient": "QNc8XMpPwM1HESwB7kqw8HoQ5sK2miZ2un", "amount": "190423818.2" }, - { "type": "GENESIS", "recipient": "QUP1SeaNw7CvCnmDp5ai3onWYwThS4GEpu", "amount": "3886200.371" }, - { "type": "GENESIS", "recipient": "QinToqEztNN1TsLdQEuzTHh7vUrEo6JTU2", "amount": "102440241.8" }, - { "type": "GENESIS", "recipient": "QcLJYLV4RD4GmPcoNnh7dQrWeYgiiPiqFQ", "amount": "32644083.11" }, - { "type": "GENESIS", "recipient": "QdYdYGYfgmMX4jQNWMZqLr81R3HdnuiKkv", "amount": "76169527.27" }, - { "type": "GENESIS", "recipient": "Qi62mUW5zfJhgRL8FRmCpjSCCnSKtf76S6", "amount": "76169527.27" }, - { "type": "GENESIS", "recipient": "QgFkxqQGkLW6CD95N2zTnT1PPqb9nxWp6b", "amount": "76169527.27" }, - { "type": "GENESIS", "recipient": "QfNUBudYsrrq27YqiHGLUg6BtG52W1W1ci", "amount": "15544801.48" }, - { "type": "GENESIS", "recipient": "QPSFoexnGoMH7EPdg72dM7SvqA7d4M2cu7", "amount": "37307523.56" }, - { "type": "GENESIS", "recipient": "QQxt5WMvoJ2TNScAzcoxHXPnLTeQ43nQ7N", "amount": "21995894.1" }, - { "type": "GENESIS", "recipient": "QicpACxck2oDYpzP8iWRQYD4oirCtvjok9", "amount": "93268808.9" }, - { "type": "GENESIS", "recipient": "QVTJkdQkTGgqEED9kAsp4BZbYNJqWfhgGw", "amount": "153909079.5" }, - { "type": "GENESIS", "recipient": "QQL5vCkhpXnP9F4wqNiBQsNaCocmRcDSUY", "amount": "15512934.64" }, - { "type": "GENESIS", "recipient": "QSvEex3p2LaZCVBaCyL8MpYsEpHLwed17r", "amount": "155448014.8" }, - { "type": "GENESIS", "recipient": "Qb3Xv96GucQpBG8n96QVFgcs2xXsEWW4CE", "amount": "38862003.71" }, - { "type": "GENESIS", "recipient": "QdRua9MqXufALpQFDeYiQDYk3EBGdwGXSx", "amount": "230303229.1" }, - { "type": "GENESIS", "recipient": "Qh16Umei91JqiHEVWV8AC6ED9aBqbDYuph", "amount": "231073474" }, - { "type": "GENESIS", "recipient": "QMu6HXfZCnwaNmyFjjhWTYAUW7k1x7PoVr", "amount": "231073474" }, - { "type": "GENESIS", "recipient": "QgcphUTiVHHfHg8e1LVgg5jujVES7ZDUTr", "amount": "115031531" }, - { "type": "GENESIS", "recipient": "QbQk9s4j4EAxAguBhmqA8mdtTct3qGnsrx", "amount": "138348733.2" }, - { "type": "GENESIS", "recipient": "QT79PhvBwE6vFzfZ4oh5wdKVsEazZuVJFy", "amount": "6360421.343" } + { "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 10000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" }, + { "type": "GENESIS", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "amount": "1000000" }, + { "type": "ACCOUNT_FLAGS", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "ACCOUNT_FLAGS", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "ACCOUNT_FLAGS", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "ACCOUNT_FLAGS", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "ACCOUNT_LEVEL", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "level": 8 }, + { "type": "ACCOUNT_LEVEL", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "level": 3 }, + { "type": "ACCOUNT_LEVEL", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "level": 10 }, + { "type": "ACCOUNT_LEVEL", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "level": 10 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "owner": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT60", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "JBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte", "owner": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "groupName": "Tiga", "description": "Tiga's group", "isOpen": true, "approvalThreshold": "PCT20", "minimumBlockDelay": 120, "maximumBlockDelay": 2880 }, + { "type": "PROXY_FORGING", "forgerPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "proxyPublicKey": "8X3w1521UNnnonieugAxhfbfvqoRpwPXJrwGQZb5JjQ3", "share": 100 } ] }, "rewardsByHeight": [ - { "height": 1, "reward": 0 } + { "height": 1, "reward": 0 }, + { "height": 100, "reward": 100 }, + { "height": 200, "reward": 20 }, + { "height": 1000, "reward": 1 }, + { "height": 2000, "reward": 0 } ], "forgingTiers": [ + { "minBlocks": 50, "maxSubAccounts": 5 }, + { "minBlocks": 50, "maxSubAccounts": 1 }, { "minBlocks": 0, "maxSubAccounts": 0 } ], "featureTriggers": { - "messageHeight": 99000, - "atHeight": 99000, + "messageHeight": 0, + "atHeight": 0, + "newBlockDistanceHeight": 0, + "newBlockTimingHeight": 0, + "newBlockTimestampHeight": 0, "assetsTimestamp": 0, - "votingTimestamp": "1403715600000", - "arbitraryTimestamp": "1405702800000", - "powfixTimestamp": "1456426800000", - "v2Timestamp": "1559347200000", - "newAssetPricingTimestamp": "2000000000000", - "groupApprovalTimestamp": "2000000000000" + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "v2Timestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0 } } diff --git a/src/test/java/org/qora/test/OnlineTests.java b/src/test/java/org/qora/test/OnlineTests.java new file mode 100644 index 00000000..ef3bcfb7 --- /dev/null +++ b/src/test/java/org/qora/test/OnlineTests.java @@ -0,0 +1,316 @@ +package org.qora.test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.Before; +import org.junit.Test; +import org.qora.account.PrivateKeyAccount; +import org.qora.account.PublicKeyAccount; +import org.qora.data.network.OnlineAccount; +import org.qora.network.message.GetOnlineAccountsMessage; +import org.qora.network.message.Message; +import org.qora.network.message.OnlineAccountsMessage; +import org.qora.repository.DataException; +import org.qora.test.common.Common; +import org.qora.test.common.FakePeer; +import org.qora.utils.ByteArray; + +import com.google.common.primitives.Longs; + +public class OnlineTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + private static final int MAX_PEERS = 100; + private static final int MAX_RUNNING_PEERS = 20; + private static final boolean LOG_CONNECTION_CHANGES = false; + private static final boolean LOG_ACCOUNT_CHANGES = true; + private static final boolean GET_ONLINE_UNICAST_NOT_BROADCAST = false; + private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000; + + private static List allKnownAccounts; + private static final Random random = new Random(); + + static class OnlinePeer extends FakePeer { + private static final long LAST_SEEN_EXPIRY_PERIOD = 6 * 60 * 1000; + private static final long ONLINE_REFRESH_INTERVAL = 4 * 60 * 1000; + private static final int MAX_CONNECTED_PEERS = 5; + + private final PrivateKeyAccount account; + + private List onlineAccounts; + private long nextOnlineRefresh = 0; + + public OnlinePeer(int id, PrivateKeyAccount account) { + super(id); + + this.account = account; + + this.onlineAccounts = Collections.synchronizedList(new ArrayList<>()); + } + + @Override + protected void processMessage(FakePeer peer, Message message) throws InterruptedException { + switch (message.getType()) { + case GET_ONLINE_ACCOUNTS: { + 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()) { + OnlineAccount onlineAccount = iterator.next(); + + for (int i = 0; i < excludeAccounts.size(); ++i) { + OnlineAccount excludeAccount = excludeAccounts.get(i); + + if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.getPublicKey())) { + iterator.remove(); + continue SEND_ITERATOR; + } + } + } + + Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend); + this.send(peer, onlineAccountsMessage); + + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] sent %d of our %d online accounts to %d", this.getId(), accountsToSend.size(), onlineAccounts.size(), peer.getId())); + + break; + } + + case ONLINE_ACCOUNTS: { + OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message; + + List onlineAccounts = onlineAccountsMessage.getOnlineAccounts(); + + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] received %d online accounts from %d", this.getId(), onlineAccounts.size(), peer.getId())); + + for (OnlineAccount onlineAccount : onlineAccounts) + verifyAndAddAccount(onlineAccount); + + break; + } + + default: + break; + } + } + + private void verifyAndAddAccount(OnlineAccount onlineAccount) { + // we would check timestamp is 'recent' here + + // Verify + byte[] data = Longs.toByteArray(onlineAccount.getTimestamp()); + PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey()); + if (!otherAccount.verify(onlineAccount.getSignature(), data)) { + System.out.println(String.format("[%d] rejecting invalid online account %s", this.getId(), otherAccount.getAddress())); + return; + } + + ByteArray publicKeyBA = new ByteArray(onlineAccount.getPublicKey()); + + synchronized (this.onlineAccounts) { + OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> new ByteArray(account.getPublicKey()).equals(publicKeyBA)).findFirst().orElse(null); + + if (existingAccount != null) { + if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) { + this.onlineAccounts.remove(existingAccount); + + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] updated online account %s with timestamp %d (was %d)", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp(), existingAccount.getTimestamp())); + } else { + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] ignoring existing online account %s", this.getId(), otherAccount.getAddress())); + + return; + } + } else { + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] added online account %s with timestamp %d", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp())); + } + + this.onlineAccounts.add(onlineAccount); + } + } + + @Override + protected void performIdleTasks() { + final long now = System.currentTimeMillis(); + + // Expire old entries + final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD; + synchronized (this.onlineAccounts) { + Iterator iterator = this.onlineAccounts.iterator(); + while (iterator.hasNext()) { + OnlineAccount onlineAccount = iterator.next(); + + if (onlineAccount.getTimestamp() < cutoffThreshold) { + iterator.remove(); + + if (LOG_ACCOUNT_CHANGES) { + PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey()); + System.out.println(String.format("[%d] removed expired online account %s with timestamp %d", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp())); + } + } + } + } + + // Request data from another peer + Message message; + synchronized (this.onlineAccounts) { + message = new GetOnlineAccountsMessage(this.onlineAccounts); + } + + if (GET_ONLINE_UNICAST_NOT_BROADCAST) { + FakePeer peer = this.pickRandomPeer(); + if (peer != null) + this.send(peer, message); + } else { + this.broadcast(message); + } + + // Refresh our onlineness? + if (now >= this.nextOnlineRefresh) { + this.nextOnlineRefresh = now + ONLINE_REFRESH_INTERVAL; + refreshOnlineness(); + } + + // Log our online list + synchronized (this.onlineAccounts) { + System.out.println(String.format("[%d] Connections: %d, online accounts: %d", this.getId(), this.peers.size(), this.onlineAccounts.size())); + } + } + + private void refreshOnlineness() { + // Broadcast signed timestamp + final long timestamp = (System.currentTimeMillis() / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS; + + byte[] data = Longs.toByteArray(timestamp); + byte[] signature = this.account.sign(data); + byte[] publicKey = this.account.getPublicKey(); + + // Our account is online + OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey); + synchronized (this.onlineAccounts) { + this.onlineAccounts.removeIf(account -> account.getPublicKey() == this.account.getPublicKey()); + this.onlineAccounts.add(onlineAccount); + } + + Message message = new OnlineAccountsMessage(Arrays.asList(onlineAccount)); + this.broadcast(message); + + if (LOG_ACCOUNT_CHANGES) + System.out.println(String.format("[%d] broadcasted online account %s with timestamp %d", this.getId(), this.account.getAddress(), timestamp)); + } + + @Override + public void connect(FakePeer otherPeer) { + int totalPeers; + synchronized (this.peers) { + totalPeers = this.peers.size(); + } + + if (totalPeers >= MAX_CONNECTED_PEERS) + return; + + super.connect(otherPeer); + + if (LOG_CONNECTION_CHANGES) + System.out.println(String.format("[%d] Connected to peer %d, total peers: %d", this.getId(), otherPeer.getId(), totalPeers + 1)); + } + + public void randomDisconnect() { + FakePeer peer; + int totalPeers; + + synchronized (this.peers) { + peer = this.pickRandomPeer(); + if (peer == null) + return; + + totalPeers = this.peers.size(); + } + + this.disconnect(peer); + + if (LOG_CONNECTION_CHANGES) + System.out.println(String.format("[%d] Disconnected peer %d, total peers: %d", this.getId(), peer.getId(), totalPeers - 1)); + } + } + + @Test + public void testOnlineness() throws InterruptedException { + allKnownAccounts = new ArrayList<>(); + + List allPeers = new ArrayList<>(); + + for (int i = 0; i < MAX_PEERS; ++i) { + byte[] seed = new byte[32]; + random.nextBytes(seed); + PrivateKeyAccount account = new PrivateKeyAccount(null, seed); + + allKnownAccounts.add(account); + + OnlinePeer peer = new OnlinePeer(i, account); + allPeers.add(peer); + } + + // Start up some peers + List runningPeers = new ArrayList<>(); + ExecutorService peerExecutor = Executors.newCachedThreadPool(); + + for (int c = 0; c < MAX_RUNNING_PEERS; ++c) { + OnlinePeer newPeer; + do { + int i = random.nextInt(allPeers.size()); + newPeer = allPeers.get(i); + } while (runningPeers.contains(newPeer)); + + runningPeers.add(newPeer); + peerExecutor.execute(newPeer); + } + + // Randomly connect/disconnect peers + while (true) { + int i = random.nextInt(runningPeers.size()); + OnlinePeer peer = runningPeers.get(i); + + if ((random.nextInt() & 0xf) != 0) { + // Connect + OnlinePeer otherPeer; + do { + int j = random.nextInt(runningPeers.size()); + otherPeer = runningPeers.get(j); + } while (otherPeer == peer); + + peer.connect(otherPeer); + } else { + peer.randomDisconnect(); + } + + Thread.sleep(100); + } + } + +} diff --git a/src/test/java/org/qora/test/SerializationTests.java b/src/test/java/org/qora/test/SerializationTests.java index b012dfe4..6fc76d86 100644 --- a/src/test/java/org/qora/test/SerializationTests.java +++ b/src/test/java/org/qora/test/SerializationTests.java @@ -15,8 +15,14 @@ import org.qora.utils.Base58; import com.google.common.hash.HashCode; +import io.druid.extendedset.intset.ConciseSet; + import static org.junit.Assert.*; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + import org.junit.Before; public class SerializationTests extends Common { @@ -70,4 +76,69 @@ public class SerializationTests extends Common { } } + @Test + public void testAccountBitMap() { + Random random = new Random(); + + final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1; + System.out.println(String.format("Number of known accounts: %d", numberOfKnownAccounts)); + + // 5% to 15% + final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 10) + numberOfKnownAccounts / 5); + System.out.println(String.format("Number of accounts to encode: %d", numberOfAccountsToEncode)); + + final int bitsLength = numberOfKnownAccounts; + System.out.println(String.format("Bits to fit all accounts: %d", bitsLength)); + + // Enough bytes to fit at least bitsLength bits + final int byteLength = ((bitsLength - 1) >> 3) + 1; + System.out.println(String.format("Uncompressed bytes to fit all accounts: %d", byteLength)); + + List accountIndexes = new LinkedList<>(); + for (int i = 0; i < numberOfAccountsToEncode; ++i) { + final int accountIndex = random.nextInt(numberOfKnownAccounts); + accountIndexes.add(accountIndex); + // System.out.println(String.format("Account [%d]: %d / 0x%08x", i, accountIndex, accountIndex)); + } + + ConciseSet compressedSet = new ConciseSet(); + + for (Integer accountIndex : accountIndexes) + compressedSet.add(accountIndex); + + int compressedSize = compressedSet.toByteBuffer().remaining(); + + System.out.println(String.format("Out of %d known accounts, encoding %d accounts needs %d uncompressed bytes but only %d compressed bytes", + numberOfKnownAccounts, numberOfAccountsToEncode, byteLength, compressedSize)); + } + + @Test + public void benchmarkBitSetCompression() { + Random random = new Random(); + + System.out.println(String.format("Known Online UncompressedBitSet UncompressedIntList Compressed")); + + for (int run = 0; run < 1000; ++run) { + final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1; + + // 5% to 25% + final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 20) + numberOfKnownAccounts / 5); + + // Enough uncompressed bytes to fit one bit per known account + final int uncompressedBitSetSize = ((numberOfKnownAccounts - 1) >> 3) + 1; + + // Size of a simple list of ints + final int uncompressedIntListSize = numberOfAccountsToEncode * 4; + + ConciseSet compressedSet = new ConciseSet(); + + for (int i = 0; i < numberOfAccountsToEncode; ++i) + compressedSet.add(random.nextInt(numberOfKnownAccounts)); + + int compressedSize = compressedSet.toByteBuffer().remaining(); + + System.out.println(String.format("%d %d %d %d %d", numberOfKnownAccounts, numberOfAccountsToEncode, uncompressedBitSetSize, uncompressedIntListSize, compressedSize)); + } + } + } \ No newline at end of file diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java index cd7341f0..f80ecf44 100644 --- a/src/test/java/org/qora/test/TransactionTests.java +++ b/src/test/java/org/qora/test/TransactionTests.java @@ -105,7 +105,7 @@ public class TransactionTests extends Common { // Create test generator account generator = new PrivateKeyAccount(repository, generatorSeed); - accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null)); + accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null, 0)); accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, initialGeneratorBalance)); // Create test sender account @@ -113,7 +113,7 @@ public class TransactionTests extends Common { // Mock account reference = senderSeed; - accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null)); + accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null, 0)); // Mock balance accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance)); diff --git a/src/test/java/org/qora/test/common/FakePeer.java b/src/test/java/org/qora/test/common/FakePeer.java new file mode 100644 index 00000000..042ed194 --- /dev/null +++ b/src/test/java/org/qora/test/common/FakePeer.java @@ -0,0 +1,110 @@ +package org.qora.test.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.qora.network.message.Message; + +public abstract class FakePeer implements Runnable { + private static final long DEFAULT_BROADCAST_INTERVAL = 60 * 1000; + + protected final int id; + protected final long startedWhen; + + protected final LinkedBlockingQueue pendingMessages; + protected final List peers; + + private long nextBroadcast; + + public FakePeer(int id) { + this.id = id; + this.startedWhen = System.currentTimeMillis(); + + this.pendingMessages = new LinkedBlockingQueue<>(); + this.peers = Collections.synchronizedList(new ArrayList<>()); + + this.nextBroadcast = this.startedWhen; + } + + protected static long getBroadcastInterval() { + return DEFAULT_BROADCAST_INTERVAL; + } + + public int getId() { + return this.id; + } + + public void run() { + try { + while (true) { + PeerMessage peerMessage = this.pendingMessages.poll(1, TimeUnit.SECONDS); + + if (peerMessage != null) + processMessage(peerMessage.peer, peerMessage.message); + else + idleTasksCheck(); + } + } catch (InterruptedException e) { + // fall-through to exit + } + } + + protected abstract void processMessage(FakePeer peer, Message message) throws InterruptedException; + + protected void idleTasksCheck() { + final long now = System.currentTimeMillis(); + + if (now < this.nextBroadcast) + return; + + this.nextBroadcast = now + getBroadcastInterval(); + + this.performIdleTasks(); + } + + protected abstract void performIdleTasks(); + + public void connect(FakePeer peer) { + synchronized (this.peers) { + if (this.peers.contains(peer)) + return; + + this.peers.add(peer); + } + } + + protected void send(FakePeer otherPeer, Message message) { + otherPeer.receive(this, message); + } + + protected void broadcast(Message message) { + synchronized (this.peers) { + for (int i = 0; i < this.peers.size(); ++i) + this.send(this.peers.get(i), message); + } + } + + public void receive(FakePeer sendingPeer, Message message) { + this.pendingMessages.add(new PeerMessage(sendingPeer, message)); + } + + public void disconnect(FakePeer peer) { + this.peers.remove(peer); + } + + public FakePeer pickRandomPeer() { + synchronized (this.peers) { + if (this.peers.isEmpty()) + return null; + + Random random = new Random(); + int i = random.nextInt(this.peers.size()); + return this.peers.get(i); + } + } + +} diff --git a/src/test/java/org/qora/test/common/PeerMessage.java b/src/test/java/org/qora/test/common/PeerMessage.java new file mode 100644 index 00000000..be9527de --- /dev/null +++ b/src/test/java/org/qora/test/common/PeerMessage.java @@ -0,0 +1,17 @@ +package org.qora.test.common; + +import org.qora.network.message.Message; +import org.qora.test.common.FakePeer; + +public class PeerMessage { + public final FakePeer peer; + public final Message message; + public final long sentWhen; + public Long processedWhen = null; + + public PeerMessage(FakePeer peer, Message message) { + this.peer = peer; + this.message = message; + this.sentWhen = System.currentTimeMillis(); + } +}