From a921db2cc6413a0b738abf2e7a24ccea24a05cf0 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 18:12:50 +0000 Subject: [PATCH 01/20] Added "lite" setting to designate the core as a lite node. --- src/main/java/org/qortal/settings/Settings.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 24fbfff6..57169273 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -143,6 +143,8 @@ public class Settings { * This has a significant effect on execution time. */ private int onlineSignaturesTrimBatchSize = 100; // blocks + /** Lite nodes don't sync blocks, and instead request "derived data" from peers */ + private boolean lite = false; /** Whether we should prune old data to reduce database size * This prevents the node from being able to serve older blocks */ @@ -796,6 +798,10 @@ public class Settings { return this.onlineSignaturesTrimBatchSize; } + public boolean isLite() { + return this.lite; + } + public boolean isTopOnly() { return this.topOnly; } From 0e3a9ee2b25366b141fc232497819d67691fc1f8 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 18:13:45 +0000 Subject: [PATCH 02/20] Return the node "type" (full / topOnly / lite) in GET /admin/info endpoint. This can used by the UI to hide features that aren't supported on lite nodes. --- src/main/java/org/qortal/api/model/NodeInfo.java | 1 + .../java/org/qortal/api/resource/AdminResource.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/org/qortal/api/model/NodeInfo.java b/src/main/java/org/qortal/api/model/NodeInfo.java index 16a4df75..6732357a 100644 --- a/src/main/java/org/qortal/api/model/NodeInfo.java +++ b/src/main/java/org/qortal/api/model/NodeInfo.java @@ -12,6 +12,7 @@ public class NodeInfo { public long buildTimestamp; public String nodeId; public boolean isTestNet; + public String type; public NodeInfo() { } diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 277b5f00..0e16297d 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -119,10 +119,23 @@ public class AdminResource { nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp(); nodeInfo.nodeId = Network.getInstance().getOurNodeId(); nodeInfo.isTestNet = Settings.getInstance().isTestNet(); + nodeInfo.type = getNodeType(); return nodeInfo; } + private String getNodeType() { + if (Settings.getInstance().isTopOnly()) { + return "topOnly"; + } + else if (Settings.getInstance().isLite()) { + return "lite"; + } + else { + return "full"; + } + } + @GET @Path("/status") @Operation( From cfe92525ed00250f1de065ae5a978fcabf66634b Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 18:28:51 +0000 Subject: [PATCH 03/20] Disable various core functions when running as a lite node. Lite nodes can't sync or mint blocks, and they also have a very limited ability to verify unconfirmed transactions due to a lack of contextual information (i.e. the blockchain). For now, most validation is skipped and they simply act as relays to help get transactions around the network. Full and topOnly nodes will disregard any invalid transactions upon receipt as usual, and since the lite nodes aren't signing any blocks, there is little risk to the reduced validation, other than the experience of the lite node itself. This can be tightened up considerably as the lite nodes become more powerful, but the current approach works as a PoC. --- .../org/qortal/controller/BlockMinter.java | 5 ++ .../org/qortal/controller/Controller.java | 68 +++++++++++++------ .../org/qortal/controller/Synchronizer.java | 5 ++ .../controller/TransactionImporter.java | 9 +++ .../controller/repository/AtStatesPruner.java | 5 ++ .../repository/AtStatesTrimmer.java | 5 ++ .../controller/repository/BlockArchiver.java | 2 +- .../controller/repository/BlockPruner.java | 5 ++ .../OnlineAccountsSignaturesTrimmer.java | 5 ++ src/main/java/org/qortal/network/Network.java | 12 ++-- .../qortal/repository/RepositoryManager.java | 10 +++ .../org/qortal/transaction/Transaction.java | 26 ++++--- src/main/resources/i18n/SysTray_de.properties | 2 + src/main/resources/i18n/SysTray_en.properties | 2 + src/main/resources/i18n/SysTray_fi.properties | 2 + src/main/resources/i18n/SysTray_fr.properties | 2 + src/main/resources/i18n/SysTray_hu.properties | 2 + src/main/resources/i18n/SysTray_it.properties | 2 + src/main/resources/i18n/SysTray_nl.properties | 2 + src/main/resources/i18n/SysTray_ru.properties | 2 + .../resources/i18n/SysTray_zh_CN.properties | 2 + .../resources/i18n/SysTray_zh_TW.properties | 2 + .../qortal/test/apps/CheckTranslations.java | 2 +- 23 files changed, 142 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index de73adbe..e45e8f9a 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -61,6 +61,11 @@ public class BlockMinter extends Thread { public void run() { Thread.currentThread().setName("BlockMinter"); + if (Settings.getInstance().isLite()) { + // Lite nodes do not mint + return; + } + try (final Repository repository = RepositoryManager.getRepository()) { if (Settings.getInstance().getWipeUnconfirmedOnStart()) { // Wipe existing unconfirmed transactions diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index fcf6270f..e2425ba6 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -362,23 +362,27 @@ public class Controller extends Thread { return; // Not System.exit() so that GUI can display error } - // Rebuild Names table and check database integrity (if enabled) - NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); - namesDatabaseIntegrityCheck.rebuildAllNames(); - if (Settings.getInstance().isNamesIntegrityCheckEnabled()) { - namesDatabaseIntegrityCheck.runIntegrityCheck(); - } + // If we have a non-lite node, we need to perform some startup actions + if (!Settings.getInstance().isLite()) { - LOGGER.info("Validating blockchain"); - try { - BlockChain.validate(); + // Rebuild Names table and check database integrity (if enabled) + NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildAllNames(); + if (Settings.getInstance().isNamesIntegrityCheckEnabled()) { + namesDatabaseIntegrityCheck.runIntegrityCheck(); + } - Controller.getInstance().refillLatestBlocksCache(); - LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight())); - } catch (DataException e) { - LOGGER.error("Couldn't validate blockchain", e); - Gui.getInstance().fatalError("Blockchain validation issue", e); - return; // Not System.exit() so that GUI can display error + LOGGER.info("Validating blockchain"); + try { + BlockChain.validate(); + + Controller.getInstance().refillLatestBlocksCache(); + LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight())); + } catch (DataException e) { + LOGGER.error("Couldn't validate blockchain", e); + Gui.getInstance().fatalError("Blockchain validation issue", e); + return; // Not System.exit() so that GUI can display error + } } // Import current trade bot states and minting accounts if they exist @@ -754,7 +758,11 @@ public class Controller extends Thread { final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L); synchronized (Synchronizer.getInstance().syncLock) { - if (this.isMintingPossible) { + if (Settings.getInstance().isLite()) { + actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE"); + SysTray.getInstance().setTrayIcon(4); + } + else if (this.isMintingPossible) { actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED"); SysTray.getInstance().setTrayIcon(2); } @@ -776,7 +784,11 @@ public class Controller extends Thread { } } - String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion); + String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText); + if (!Settings.getInstance().isLite()) { + tooltip.concat(String.format(" - %s %d", heightText, height)); + } + tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion)); SysTray.getInstance().setToolTipText(tooltip); this.callbackExecutor.execute(() -> { @@ -933,6 +945,11 @@ public class Controller extends Thread { // Callbacks for/from network public void doNetworkBroadcast() { + if (Settings.getInstance().isLite()) { + // Lite nodes have nothing to broadcast + return; + } + Network network = Network.getInstance(); // Send (if outbound) / Request peer lists @@ -1450,11 +1467,13 @@ public class Controller extends Thread { private void onNetworkHeightV2Message(Peer peer, Message message) { HeightV2Message heightV2Message = (HeightV2Message) message; - // If peer is inbound and we've not updated their height - // then this is probably their initial HEIGHT_V2 message - // so they need a corresponding HEIGHT_V2 message from us - if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null)) - peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip())); + if (!Settings.getInstance().isLite()) { + // If peer is inbound and we've not updated their height + // then this is probably their initial HEIGHT_V2 message + // so they need a corresponding HEIGHT_V2 message from us + if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null)) + peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip())); + } // Update peer chain tip data PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey()); @@ -1515,6 +1534,11 @@ public class Controller extends Thread { * @return boolean - whether our node's blockchain is up to date or not */ public boolean isUpToDate(Long minLatestBlockTimestamp) { + if (Settings.getInstance().isLite()) { + // Lite nodes are always "up to date" + return true; + } + // Do we even have a vaguely recent block? if (minLatestBlockTimestamp == null) return false; diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index d574ef87..e1dc2b89 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -134,6 +134,11 @@ public class Synchronizer extends Thread { public void run() { Thread.currentThread().setName("Synchronizer"); + if (Settings.getInstance().isLite()) { + // Lite nodes don't need to sync + return; + } + try { while (running && !Controller.isStopping()) { Thread.sleep(1000); diff --git a/src/main/java/org/qortal/controller/TransactionImporter.java b/src/main/java/org/qortal/controller/TransactionImporter.java index 3514ea47..c23316cd 100644 --- a/src/main/java/org/qortal/controller/TransactionImporter.java +++ b/src/main/java/org/qortal/controller/TransactionImporter.java @@ -11,6 +11,7 @@ import org.qortal.network.message.TransactionSignaturesMessage; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; import org.qortal.transaction.Transaction; import org.qortal.utils.Base58; import org.qortal.utils.NTP; @@ -105,6 +106,8 @@ public class TransactionImporter extends Thread { List sigValidTransactions = new ArrayList<>(); + boolean isLiteNode = Settings.getInstance().isLite(); + // Signature validation round - does not require blockchain lock for (Map.Entry transactionEntry : incomingTransactionsCopy.entrySet()) { // Quick exit? @@ -118,6 +121,12 @@ public class TransactionImporter extends Thread { // Only validate signature if we haven't already done so Boolean isSigValid = transactionEntry.getValue(); if (!Boolean.TRUE.equals(isSigValid)) { + if (isLiteNode) { + // Lite nodes can't validate transactions, so can only assume that everything is valid + sigValidTransactions.add(transaction); + continue; + } + if (!transaction.isSignatureValid()) { String signature58 = Base58.encode(transactionData.getSignature()); diff --git a/src/main/java/org/qortal/controller/repository/AtStatesPruner.java b/src/main/java/org/qortal/controller/repository/AtStatesPruner.java index 54fba699..bd12f784 100644 --- a/src/main/java/org/qortal/controller/repository/AtStatesPruner.java +++ b/src/main/java/org/qortal/controller/repository/AtStatesPruner.java @@ -19,6 +19,11 @@ public class AtStatesPruner implements Runnable { public void run() { Thread.currentThread().setName("AT States pruner"); + if (Settings.getInstance().isLite()) { + // Nothing to prune in lite mode + return; + } + boolean archiveMode = false; if (!Settings.getInstance().isTopOnly()) { // Top-only mode isn't enabled, but we might want to prune for the purposes of archiving diff --git a/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java b/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java index d3bdc345..69fa347c 100644 --- a/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java +++ b/src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java @@ -19,6 +19,11 @@ public class AtStatesTrimmer implements Runnable { public void run() { Thread.currentThread().setName("AT States trimmer"); + if (Settings.getInstance().isLite()) { + // Nothing to trim in lite mode + return; + } + try (final Repository repository = RepositoryManager.getRepository()) { int trimStartHeight = repository.getATRepository().getAtTrimHeight(); diff --git a/src/main/java/org/qortal/controller/repository/BlockArchiver.java b/src/main/java/org/qortal/controller/repository/BlockArchiver.java index ef26610c..8757bf32 100644 --- a/src/main/java/org/qortal/controller/repository/BlockArchiver.java +++ b/src/main/java/org/qortal/controller/repository/BlockArchiver.java @@ -21,7 +21,7 @@ public class BlockArchiver implements Runnable { public void run() { Thread.currentThread().setName("Block archiver"); - if (!Settings.getInstance().isArchiveEnabled()) { + if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) { return; } diff --git a/src/main/java/org/qortal/controller/repository/BlockPruner.java b/src/main/java/org/qortal/controller/repository/BlockPruner.java index 03fb38b9..23e3a45a 100644 --- a/src/main/java/org/qortal/controller/repository/BlockPruner.java +++ b/src/main/java/org/qortal/controller/repository/BlockPruner.java @@ -19,6 +19,11 @@ public class BlockPruner implements Runnable { public void run() { Thread.currentThread().setName("Block pruner"); + if (Settings.getInstance().isLite()) { + // Nothing to prune in lite mode + return; + } + boolean archiveMode = false; if (!Settings.getInstance().isTopOnly()) { // Top-only mode isn't enabled, but we might want to prune for the purposes of archiving diff --git a/src/main/java/org/qortal/controller/repository/OnlineAccountsSignaturesTrimmer.java b/src/main/java/org/qortal/controller/repository/OnlineAccountsSignaturesTrimmer.java index dfd9d45e..d74df4b5 100644 --- a/src/main/java/org/qortal/controller/repository/OnlineAccountsSignaturesTrimmer.java +++ b/src/main/java/org/qortal/controller/repository/OnlineAccountsSignaturesTrimmer.java @@ -21,6 +21,11 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable { public void run() { Thread.currentThread().setName("Online Accounts trimmer"); + if (Settings.getInstance().isLite()) { + // Nothing to trim in lite mode + return; + } + try (final Repository repository = RepositoryManager.getRepository()) { // Don't even start trimming until initial rush has ended Thread.sleep(INITIAL_SLEEP_PERIOD); diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index d4435ddb..a58db403 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -1062,11 +1062,13 @@ public class Network { // (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message). if (peer.isOutbound()) { - // Send our height - Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip()); - if (!peer.sendMessage(heightMessage)) { - peer.disconnect("failed to send height/info"); - return; + if (!Settings.getInstance().isLite()) { + // Send our height + Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip()); + if (!peer.sendMessage(heightMessage)) { + peer.disconnect("failed to send height/info"); + return; + } } // Send our peers list diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java index 714ada28..0d9325b9 100644 --- a/src/main/java/org/qortal/repository/RepositoryManager.java +++ b/src/main/java/org/qortal/repository/RepositoryManager.java @@ -62,6 +62,11 @@ public abstract class RepositoryManager { } public static boolean archive(Repository repository) { + if (Settings.getInstance().isLite()) { + // Lite nodes have no blockchain + return false; + } + // Bulk archive the database the first time we use archive mode if (Settings.getInstance().isArchiveEnabled()) { if (RepositoryManager.canArchiveOrPrune()) { @@ -82,6 +87,11 @@ public abstract class RepositoryManager { } public static boolean prune(Repository repository) { + if (Settings.getInstance().isLite()) { + // Lite nodes have no blockchain + return false; + } + // Bulk prune the database the first time we use top-only or block archive mode if (Settings.getInstance().isTopOnly() || Settings.getInstance().isArchiveEnabled()) { diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 79a6478b..8f2c7e97 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -530,11 +530,6 @@ public abstract class Transaction { if (now >= this.getDeadline()) return ValidationResult.TIMESTAMP_TOO_OLD; - // Transactions with a expiry prior to latest block's timestamp are too old - BlockData latestBlock = repository.getBlockRepository().getLastBlock(); - if (this.getDeadline() <= latestBlock.getTimestamp()) - return ValidationResult.TIMESTAMP_TOO_OLD; - // Transactions with a timestamp too far into future are too new long maxTimestamp = now + Settings.getInstance().getMaxTransactionTimestampFuture(); if (this.transactionData.getTimestamp() > maxTimestamp) @@ -545,14 +540,29 @@ public abstract class Transaction { if (feeValidationResult != ValidationResult.OK) return feeValidationResult; - PublicKeyAccount creator = this.getCreator(); - if (creator == null) - return ValidationResult.MISSING_CREATOR; + if (Settings.getInstance().isLite()) { + // Everything from this point is difficult to validate for a lite node, since it has no blocks. + // For now, we will assume it is valid, to allow it to move around the network easily. + // If it turns out to be invalid, other full/top-only nodes will reject it on receipt. + // Lite nodes would never mint a block, so there's not much risk of holding invalid transactions. + // TODO: implement lite-only validation for each transaction type + return ValidationResult.OK; + } // Reject if unconfirmed pile already has X transactions from same creator if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount()) return ValidationResult.TOO_MANY_UNCONFIRMED; + // Transactions with a expiry prior to latest block's timestamp are too old + // Not relevant for lite nodes, as they don't have any blocks + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + if (this.getDeadline() <= latestBlock.getTimestamp()) + return ValidationResult.TIMESTAMP_TOO_OLD; + + PublicKeyAccount creator = this.getCreator(); + if (creator == null) + return ValidationResult.MISSING_CREATOR; + // Check transaction's txGroupId if (!this.isValidTxGroupId()) return ValidationResult.INVALID_TX_GROUP_ID; diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties index 1880aa27..b949ca8c 100644 --- a/src/main/resources/i18n/SysTray_de.properties +++ b/src/main/resources/i18n/SysTray_de.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Datenbank Instandhaltung EXIT = Verlassen +LITE_NODE = Lite node + MINTING_DISABLED = NOT minting MINTING_ENABLED = \u2714 Minting diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties index 296f9760..204f0df2 100644 --- a/src/main/resources/i18n/SysTray_en.properties +++ b/src/main/resources/i18n/SysTray_en.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Maintenance EXIT = Exit +LITE_NODE = Lite node + MINTING_DISABLED = NOT minting MINTING_ENABLED = \u2714 Minting diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties index 07289551..bc787715 100644 --- a/src/main/resources/i18n/SysTray_fi.properties +++ b/src/main/resources/i18n/SysTray_fi.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Tietokannan ylläpito EXIT = Pois +LITE_NODE = Lite node + MINTING_DISABLED = EI lyö rahaa MINTING_ENABLED = \u2714 Lyö rahaa diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties index 61e8002a..6e60713c 100644 --- a/src/main/resources/i18n/SysTray_fr.properties +++ b/src/main/resources/i18n/SysTray_fr.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Maintenance de la base de données EXIT = Quitter +LITE_NODE = Lite node + MINTING_DISABLED = NE mint PAS MINTING_ENABLED = \u2714 Minting diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties index 5a082b6d..9bc51ff5 100644 --- a/src/main/resources/i18n/SysTray_hu.properties +++ b/src/main/resources/i18n/SysTray_hu.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Adatbázis karbantartás EXIT = Kilépés +LITE_NODE = Lite node + MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties index dd02aefa..bf61cc46 100644 --- a/src/main/resources/i18n/SysTray_it.properties +++ b/src/main/resources/i18n/SysTray_it.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Manutenzione del database EXIT = Uscita +LITE_NODE = Lite node + MINTING_DISABLED = Conio disabilitato MINTING_ENABLED = \u2714 Conio abilitato diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties index 996c946f..8a4f112b 100644 --- a/src/main/resources/i18n/SysTray_nl.properties +++ b/src/main/resources/i18n/SysTray_nl.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Onderhoud EXIT = Verlaten +LITE_NODE = Lite node + MINTING_DISABLED = Minten is uitgeschakeld MINTING_ENABLED = \u2714 Minten is ingeschakeld diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties index efcc723c..fc3d8648 100644 --- a/src/main/resources/i18n/SysTray_ru.properties +++ b/src/main/resources/i18n/SysTray_ru.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = Обслуживание базы данных EXIT = Выход +LITE_NODE = Lite node + MINTING_DISABLED = Чеканка отключена MINTING_ENABLED = \u2714 Чеканка активна diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties index 1216368b..c103d24b 100644 --- a/src/main/resources/i18n/SysTray_zh_CN.properties +++ b/src/main/resources/i18n/SysTray_zh_CN.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = 数据库维护 EXIT = 退出核心 +LITE_NODE = Lite node + MINTING_DISABLED = 没有铸币 MINTING_ENABLED = \u2714 铸币 diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties index 1b2d0d90..5e6ccc3e 100644 --- a/src/main/resources/i18n/SysTray_zh_TW.properties +++ b/src/main/resources/i18n/SysTray_zh_TW.properties @@ -27,6 +27,8 @@ DB_MAINTENANCE = 數據庫維護 EXIT = 退出核心 +LITE_NODE = Lite node + MINTING_DISABLED = 沒有鑄幣 MINTING_ENABLED = \u2714 鑄幣 diff --git a/src/test/java/org/qortal/test/apps/CheckTranslations.java b/src/test/java/org/qortal/test/apps/CheckTranslations.java index 2b59ce84..b8008c78 100644 --- a/src/test/java/org/qortal/test/apps/CheckTranslations.java +++ b/src/test/java/org/qortal/test/apps/CheckTranslations.java @@ -15,7 +15,7 @@ public class CheckTranslations { private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" }; private static final Set SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT", "BUILD_VERSION", "CHECK_TIME_ACCURACY", "CONNECTING", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES", - "DB_BACKUP", "DB_CHECKPOINT", "EXIT", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "PERFORMING_DB_CHECKPOINT", + "DB_BACKUP", "DB_CHECKPOINT", "EXIT", "LITE_NODE", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "PERFORMING_DB_CHECKPOINT", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK"); private static String failurePrefix; From 64ff3ac67208688db38d9c44edfead73eb0fd917 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 18:30:40 +0000 Subject: [PATCH 04/20] Improved comment --- src/main/java/org/qortal/controller/TransactionImporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/TransactionImporter.java b/src/main/java/org/qortal/controller/TransactionImporter.java index c23316cd..22893bbd 100644 --- a/src/main/java/org/qortal/controller/TransactionImporter.java +++ b/src/main/java/org/qortal/controller/TransactionImporter.java @@ -122,7 +122,7 @@ public class TransactionImporter extends Thread { Boolean isSigValid = transactionEntry.getValue(); if (!Boolean.TRUE.equals(isSigValid)) { if (isLiteNode) { - // Lite nodes can't validate transactions, so can only assume that everything is valid + // Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid sigValidTransactions.add(transaction); continue; } From 8c3e0adf35322f4369635205090eb82fc9bce2b1 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 20:08:21 +0000 Subject: [PATCH 05/20] Added message types to fetch account details and account balances, and use these in various APIs. This should bring in enough data for very basic chat and wallet functionality (using addresses rather than registered names). Data currently comes from a single random peer, however this can be expanded to request from multiple peers to gain confidence in the accuracy of the data. If bad data is returned from a peer, it's not the end of the world since the transaction would just be considered invalid by full nodes and would be thrown out. But this should be mostly avoidable by taking data from multiple sources to improve confidence in its accuracy. --- src/main/java/org/qortal/account/Account.java | 14 ++- .../api/resource/AddressesResource.java | 29 +++-- .../org/qortal/controller/Controller.java | 103 +++++++++++++++ .../java/org/qortal/controller/LiteNode.java | 117 ++++++++++++++++++ .../message/AccountBalanceMessage.java | 78 ++++++++++++ .../network/message/AccountMessage.java | 101 +++++++++++++++ .../message/GetAccountBalanceMessage.java | 65 ++++++++++ .../network/message/GetAccountMessage.java | 57 +++++++++ .../org/qortal/network/message/Message.java | 7 +- 9 files changed, 559 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/qortal/controller/LiteNode.java create mode 100644 src/main/java/org/qortal/network/message/AccountBalanceMessage.java create mode 100644 src/main/java/org/qortal/network/message/AccountMessage.java create mode 100644 src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java create mode 100644 src/main/java/org/qortal/network/message/GetAccountMessage.java diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index df14d88f..c3a25fb6 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -8,11 +8,13 @@ import javax.xml.bind.annotation.XmlAccessorType; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.block.BlockChain; +import org.qortal.controller.LiteNode; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.account.RewardShareData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.settings.Settings; import org.qortal.utils.Base58; @XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config @@ -59,7 +61,17 @@ public class Account { // Balance manipulations - assetId is 0 for QORT public long getConfirmedBalance(long assetId) throws DataException { - AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId); + AccountBalanceData accountBalanceData; + + if (Settings.getInstance().isLite()) { + // Lite nodes request data from peers instead of the local db + accountBalanceData = LiteNode.getInstance().fetchAccountBalance(this.address, assetId); + } + else { + // All other node types fetch from the local db + accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId); + } + if (accountBalanceData == null) return 0; diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index b5268db7..4de8d908 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -30,6 +30,7 @@ import org.qortal.api.Security; import org.qortal.api.model.ApiOnlineAccount; import org.qortal.api.model.RewardShareKeyRequest; import org.qortal.asset.Asset; +import org.qortal.controller.LiteNode; import org.qortal.controller.OnlineAccountsManager; import org.qortal.crypto.Crypto; import org.qortal.data.account.AccountData; @@ -109,18 +110,26 @@ public class AddressesResource { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - byte[] lastReference = null; + AccountData accountData; - try (final Repository repository = RepositoryManager.getRepository()) { - AccountData accountData = repository.getAccountRepository().getAccount(address); - // Not found? - if (accountData == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); - - lastReference = accountData.getReference(); - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + if (Settings.getInstance().isLite()) { + // Lite nodes request data from peers instead of the local db + accountData = LiteNode.getInstance().fetchAccountData(address); } + else { + // All other node types request data from local db + try (final Repository repository = RepositoryManager.getRepository()) { + accountData = repository.getAccountRepository().getAccount(address); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + // Not found? + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + byte[] lastReference = accountData.getReference(); if (lastReference == null || lastReference.length == 0) return "false"; diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index e2425ba6..e73eecbd 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -39,6 +39,8 @@ import org.qortal.controller.arbitrary.*; import org.qortal.controller.repository.PruneManager; import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.controller.tradebot.TradeBot; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.data.account.AccountData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; import org.qortal.data.network.PeerChainTipData; @@ -178,6 +180,28 @@ public class Controller extends Thread { } public GetArbitraryMetadataMessageStats getArbitraryMetadataMessageStats = new GetArbitraryMetadataMessageStats(); + public static class GetAccountMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong cacheHits = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + public AtomicLong cacheFills = new AtomicLong(); + + public GetAccountMessageStats() { + } + } + public GetAccountMessageStats getAccountMessageStats = new GetAccountMessageStats(); + + public static class GetAccountBalanceMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong cacheHits = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + public AtomicLong cacheFills = new AtomicLong(); + + public GetAccountBalanceMessageStats() { + } + } + public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public AtomicLong latestBlocksCacheRefills = new AtomicLong(); public StatsSnapshot() { @@ -1232,6 +1256,14 @@ public class Controller extends Thread { TradeBot.getInstance().onTradePresencesMessage(peer, message); break; + case GET_ACCOUNT: + onNetworkGetAccountMessage(peer, message); + break; + + case GET_ACCOUNT_BALANCE: + onNetworkGetAccountBalanceMessage(peer, message); + break; + default: LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1483,6 +1515,77 @@ public class Controller extends Thread { Synchronizer.getInstance().requestSync(); } + private void onNetworkGetAccountMessage(Peer peer, Message message) { + GetAccountMessage getAccountMessage = (GetAccountMessage) message; + String address = getAccountMessage.getAddress(); + this.stats.getAccountMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + + if (accountData == null) { + // We don't have this account + this.stats.getAccountMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + AccountMessage accountMessage = new AccountMessage(accountData); + accountMessage.setId(message.getId()); + + if (!peer.sendMessage(accountMessage)) { + peer.disconnect("failed to send account"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send account %s to peer %s", address, peer), e); + } + } + + private void onNetworkGetAccountBalanceMessage(Peer peer, Message message) { + GetAccountBalanceMessage getAccountBalanceMessage = (GetAccountBalanceMessage) message; + String address = getAccountBalanceMessage.getAddress(); + long assetId = getAccountBalanceMessage.getAssetId(); + this.stats.getAccountBalanceMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(address, assetId); + + if (accountBalanceData == null) { + // We don't have this account + this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s and asset ID %d", peer, address, assetId)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + AccountBalanceMessage accountMessage = new AccountBalanceMessage(accountBalanceData); + accountMessage.setId(message.getId()); + + if (!peer.sendMessage(accountMessage)) { + peer.disconnect("failed to send account"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send balance for account %s and asset ID %d to peer %s", address, assetId, peer), e); + } + } + // Utilities diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java new file mode 100644 index 00000000..c5e6affe --- /dev/null +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -0,0 +1,117 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.data.account.AccountData; +import org.qortal.network.Network; +import org.qortal.network.Peer; +import org.qortal.network.message.*; + +import java.security.SecureRandom; +import java.util.*; + +import static org.qortal.network.message.Message.MessageType; +import static org.qortal.network.message.Message.MessageType.*; + +public class LiteNode { + + private static final Logger LOGGER = LogManager.getLogger(LiteNode.class); + + private static LiteNode instance; + + + public Map pendingRequests = Collections.synchronizedMap(new HashMap<>()); + + + public LiteNode() { + + } + + public static synchronized LiteNode getInstance() { + if (instance == null) { + instance = new LiteNode(); + } + + return instance; + } + + + /** + * Fetch account data from peers for given QORT address + * @param address - the QORT address to query + * @return accountData - the account data for this address, or null if not retrieved + */ + public AccountData fetchAccountData(String address) { + GetAccountMessage getAccountMessage = new GetAccountMessage(address); + AccountMessage accountMessage = (AccountMessage) this.sendMessage(getAccountMessage, ACCOUNT); + if (accountMessage == null) { + return null; + } + return accountMessage.getAccountData(); + } + + /** + * Fetch account balance data from peers for given QORT address and asset ID + * @param address - the QORT address to query + * @return balance - the balance for this address and assetId, or null if not retrieved + */ + public AccountBalanceData fetchAccountBalance(String address, long assetId) { + GetAccountBalanceMessage getAccountMessage = new GetAccountBalanceMessage(address, assetId); + AccountBalanceMessage accountMessage = (AccountBalanceMessage) this.sendMessage(getAccountMessage, ACCOUNT_BALANCE); + if (accountMessage == null) { + return null; + } + return accountMessage.getAccountBalanceData(); + } + + + private Message sendMessage(Message message, MessageType expectedResponseMessageType) { + // This asks a random peer for the data + // TODO: ask multiple peers, and disregard everything if there are any significant differences in the responses + + // Needs a mutable copy of the unmodifiableList + List peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers()); + + // Disregard peers that have "misbehaved" recently + peers.removeIf(Controller.hasMisbehaved); + + // Disregard peers that only have genesis block + peers.removeIf(Controller.hasOnlyGenesisBlock); + + // Disregard peers that are on an old version + peers.removeIf(Controller.hasOldVersion); + + // Disregard peers that are on a known inferior chain tip + peers.removeIf(Controller.hasInferiorChainTip); + + if (peers.isEmpty()) { + LOGGER.info("No peers available to send {} message to", message.getType()); + return null; + } + + // Pick random peer + int index = new SecureRandom().nextInt(peers.size()); + Peer peer = peers.get(index); + + LOGGER.info("Sending {} message to peer {}...", message.getType(), peer); + + Message responseMessage; + + try { + responseMessage = peer.getResponse(message); + + } catch (InterruptedException e) { + return null; + } + + if (responseMessage == null || responseMessage.getType() != expectedResponseMessageType) { + return null; + } + + LOGGER.info("Peer {} responded with {} message", peer, responseMessage.getType()); + + return responseMessage; + } + +} diff --git a/src/main/java/org/qortal/network/message/AccountBalanceMessage.java b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java new file mode 100644 index 00000000..5e64d2b5 --- /dev/null +++ b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java @@ -0,0 +1,78 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Longs; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class AccountBalanceMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private final AccountBalanceData accountBalanceData; + + public AccountBalanceMessage(AccountBalanceData accountBalanceData) { + super(MessageType.ACCOUNT_BALANCE); + + this.accountBalanceData = accountBalanceData; + } + + public AccountBalanceMessage(int id, AccountBalanceData accountBalanceData) { + super(id, MessageType.ACCOUNT_BALANCE); + + this.accountBalanceData = accountBalanceData; + } + + public AccountBalanceData getAccountBalanceData() { + return this.accountBalanceData; + } + + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + byteBuffer.get(addressBytes); + String address = Base58.encode(addressBytes); + + long assetId = byteBuffer.getLong(); + + long balance = byteBuffer.getLong(); + + AccountBalanceData accountBalanceData = new AccountBalanceData(address, assetId, balance); + return new AccountBalanceMessage(id, accountBalanceData); + } + + @Override + protected byte[] toData() { + if (this.accountBalanceData == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.accountBalanceData.getAddress()); + bytes.write(address); + + bytes.write(Longs.toByteArray(this.accountBalanceData.getAssetId())); + + bytes.write(Longs.toByteArray(this.accountBalanceData.getBalance())); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public AccountBalanceMessage cloneWithNewId(int newId) { + AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData); + clone.setId(newId); + return clone; + } + +} diff --git a/src/main/java/org/qortal/network/message/AccountMessage.java b/src/main/java/org/qortal/network/message/AccountMessage.java new file mode 100644 index 00000000..749ec01e --- /dev/null +++ b/src/main/java/org/qortal/network/message/AccountMessage.java @@ -0,0 +1,101 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.data.account.AccountData; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class AccountMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + private static final int REFERENCE_LENGTH = Transformer.SIGNATURE_LENGTH; + private static final int PUBLIC_KEY_LENGTH = Transformer.PUBLIC_KEY_LENGTH; + + private final AccountData accountData; + + public AccountMessage(AccountData accountData) { + super(MessageType.ACCOUNT); + + this.accountData = accountData; + } + + public AccountMessage(int id, AccountData accountData) { + super(id, MessageType.ACCOUNT); + + this.accountData = accountData; + } + + public AccountData getAccountData() { + return this.accountData; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + byteBuffer.get(addressBytes); + String address = Base58.encode(addressBytes); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] publicKey = new byte[PUBLIC_KEY_LENGTH]; + byteBuffer.get(publicKey); + + int defaultGroupId = byteBuffer.getInt(); + + int flags = byteBuffer.getInt(); + + int level = byteBuffer.getInt(); + + int blocksMinted = byteBuffer.getInt(); + + int blocksMintedAdjustment = byteBuffer.getInt(); + + AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment); + return new AccountMessage(id, accountData); + } + + @Override + protected byte[] toData() { + if (this.accountData == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(accountData.getAddress()); + bytes.write(address); + + bytes.write(accountData.getReference()); + + bytes.write(accountData.getPublicKey()); + + bytes.write(Ints.toByteArray(accountData.getDefaultGroupId())); + + bytes.write(Ints.toByteArray(accountData.getFlags())); + + bytes.write(Ints.toByteArray(accountData.getLevel())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMinted())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment())); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public AccountMessage cloneWithNewId(int newId) { + AccountMessage clone = new AccountMessage(this.accountData); + clone.setId(newId); + return clone; + } + +} diff --git a/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java new file mode 100644 index 00000000..c4caaa34 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java @@ -0,0 +1,65 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Longs; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountBalanceMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + private long assetId; + + public GetAccountBalanceMessage(String address, long assetId) { + this(-1, address, assetId); + } + + private GetAccountBalanceMessage(int id, String address, long assetId) { + super(id, MessageType.GET_ACCOUNT_BALANCE); + + this.address = address; + this.assetId = assetId; + } + + public String getAddress() { + return this.address; + } + + public long getAssetId() { + return this.assetId; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + long assetId = bytes.getLong(); + + return new GetAccountBalanceMessage(id, address, assetId); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + bytes.write(Longs.toByteArray(this.assetId)); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/GetAccountMessage.java b/src/main/java/org/qortal/network/message/GetAccountMessage.java new file mode 100644 index 00000000..1d18117c --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountMessage.java @@ -0,0 +1,57 @@ +package org.qortal.network.message; + +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + + public GetAccountMessage(String address) { + this(-1, address); + } + + private GetAccountMessage(int id, String address) { + super(id, MessageType.GET_ACCOUNT); + + this.address = address; + } + + public String getAddress() { + return this.address; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + if (bytes.remaining() != ADDRESS_LENGTH) + return null; + + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + return new GetAccountMessage(id, address); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index b06a5133..a4667df7 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -99,7 +99,12 @@ public abstract class Message { GET_TRADE_PRESENCES(141), ARBITRARY_METADATA(150), - GET_ARBITRARY_METADATA(151); + GET_ARBITRARY_METADATA(151), + + ACCOUNT(160), + GET_ACCOUNT(161), + ACCOUNT_BALANCE(162), + GET_ACCOUNT_BALANCE(163); public final int value; public final Method fromByteBufferMethod; From c482e5b5ca70590c42f03784193b87eaddccc388 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 21:35:13 +0000 Subject: [PATCH 06/20] Added GET_ACCOUNT_NAMES message to request names for an address, and a generic NAMES message to return a list of NameData objects. The generic NAMES message can be reused for many other responses, such as requesting the various lists of names that the API supports. --- .../qortal/api/resource/NamesResource.java | 10 +- .../org/qortal/controller/Controller.java | 49 ++++++ .../java/org/qortal/controller/LiteNode.java | 15 ++ .../message/GetAccountNamesMessage.java | 55 +++++++ .../org/qortal/network/message/Message.java | 9 +- .../qortal/network/message/NamesMessage.java | 149 ++++++++++++++++++ 6 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/qortal/network/message/GetAccountNamesMessage.java create mode 100644 src/main/java/org/qortal/network/message/NamesMessage.java diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index e380ab55..1b6f0bc8 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -26,6 +26,7 @@ import org.qortal.api.ApiErrors; import org.qortal.api.ApiException; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.model.NameSummary; +import org.qortal.controller.LiteNode; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; @@ -101,7 +102,14 @@ public class NamesResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - List names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse); + List names; + + if (Settings.getInstance().isLite()) { + names = LiteNode.getInstance().fetchAccountNames(address); + } + else { + names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse); + } return names.stream().map(NameSummary::new).collect(Collectors.toList()); } catch (DataException e) { diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index e73eecbd..a3e2befb 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -43,6 +43,7 @@ import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; +import org.qortal.data.naming.NameData; import org.qortal.data.network.PeerChainTipData; import org.qortal.data.network.PeerData; import org.qortal.data.transaction.ChatTransactionData; @@ -202,6 +203,15 @@ public class Controller extends Thread { } public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public static class GetAccountNamesMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + + public GetAccountNamesMessageStats() { + } + } + public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats(); + public AtomicLong latestBlocksCacheRefills = new AtomicLong(); public StatsSnapshot() { @@ -1264,6 +1274,10 @@ public class Controller extends Thread { onNetworkGetAccountBalanceMessage(peer, message); break; + case GET_ACCOUNT_NAMES: + onNetworkGetAccountNamesMessage(peer, message); + break; + default: LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1586,6 +1600,41 @@ public class Controller extends Thread { } } + private void onNetworkGetAccountNamesMessage(Peer peer, Message message) { + GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message; + String address = getAccountNamesMessage.getAddress(); + this.stats.getAccountNamesMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + List namesDataList = repository.getNameRepository().getNamesByOwner(address); + + if (namesDataList == null) { + // We don't have this account + this.stats.getAccountNamesMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + NamesMessage namesMessage = new NamesMessage(namesDataList); + namesMessage.setId(message.getId()); + + if (!peer.sendMessage(namesMessage)) { + peer.disconnect("failed to send account names"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e); + } + } + // Utilities diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index c5e6affe..8d36b414 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; +import org.qortal.data.naming.NameData; import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.network.message.*; @@ -65,6 +66,20 @@ public class LiteNode { return accountMessage.getAccountBalanceData(); } + /** + * Fetch list of names for given QORT address + * @param address - the QORT address to query + * @return a list of NameData objects, or null if not retrieved + */ + public List fetchAccountNames(String address) { + GetAccountNamesMessage getAccountNamesMessage = new GetAccountNamesMessage(address); + NamesMessage namesMessage = (NamesMessage) this.sendMessage(getAccountNamesMessage, NAMES); + if (namesMessage == null) { + return null; + } + return namesMessage.getNameDataList(); + } + private Message sendMessage(Message message, MessageType expectedResponseMessageType) { // This asks a random peer for the data diff --git a/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java new file mode 100644 index 00000000..b95c7eb0 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java @@ -0,0 +1,55 @@ +package org.qortal.network.message; + +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountNamesMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + + public GetAccountNamesMessage(String address) { + this(-1, address); + } + + private GetAccountNamesMessage(int id, String address) { + super(id, MessageType.GET_ACCOUNT_NAMES); + + this.address = address; + } + + public String getAddress() { + return this.address; + } + + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + return new GetAccountNamesMessage(id, address); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index a4667df7..f01ac8c3 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -101,10 +101,15 @@ public abstract class Message { ARBITRARY_METADATA(150), GET_ARBITRARY_METADATA(151), + // Lite node support ACCOUNT(160), GET_ACCOUNT(161), - ACCOUNT_BALANCE(162), - GET_ACCOUNT_BALANCE(163); + + ACCOUNT_BALANCE(170), + GET_ACCOUNT_BALANCE(171), + + NAMES(180), + GET_ACCOUNT_NAMES(181); public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/NamesMessage.java b/src/main/java/org/qortal/network/message/NamesMessage.java new file mode 100644 index 00000000..d8f0b857 --- /dev/null +++ b/src/main/java/org/qortal/network/message/NamesMessage.java @@ -0,0 +1,149 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; +import org.qortal.data.naming.NameData; +import org.qortal.naming.Name; +import org.qortal.transform.TransformationException; +import org.qortal.transform.Transformer; +import org.qortal.utils.Serialization; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class NamesMessage extends Message { + + private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH; + + private final List nameDataList; + + public NamesMessage(List nameDataList) { + super(MessageType.NAMES); + + this.nameDataList = nameDataList; + } + + public NamesMessage(int id, List nameDataList) { + super(id, MessageType.NAMES); + + this.nameDataList = nameDataList; + } + + public List getNameDataList() { + return this.nameDataList; + } + + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + try { + final int nameCount = bytes.getInt(); + + List nameDataList = new ArrayList<>(nameCount); + + for (int i = 0; i < nameCount; ++i) { + String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); + + String reducedName = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); + + String owner = Serialization.deserializeAddress(bytes); + + String data = Serialization.deserializeSizedStringV2(bytes, Name.MAX_DATA_SIZE); + + long registered = bytes.getLong(); + + int wasUpdated = bytes.getInt(); + + Long updated = null; + if (wasUpdated == 1) { + updated = bytes.getLong(); + } + + boolean isForSale = (bytes.getInt() == 1); + + Long salePrice = null; + if (isForSale) { + salePrice = bytes.getLong(); + } + + byte[] reference = new byte[SIGNATURE_LENGTH]; + bytes.get(reference); + + int creationGroupId = bytes.getInt(); + + NameData nameData = new NameData(name, reducedName, owner, data, registered, updated, + isForSale, salePrice, reference, creationGroupId); + nameDataList.add(nameData); + } + + if (bytes.hasRemaining()) { + return null; + } + + return new NamesMessage(id, nameDataList); + + } catch (TransformationException e) { + return null; + } + } + + @Override + protected byte[] toData() { + if (this.nameDataList == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(this.nameDataList.size())); + + for (int i = 0; i < this.nameDataList.size(); ++i) { + NameData nameData = this.nameDataList.get(i); + + Serialization.serializeSizedStringV2(bytes, nameData.getName()); + + Serialization.serializeSizedStringV2(bytes, nameData.getReducedName()); + + Serialization.serializeAddress(bytes, nameData.getOwner()); + + Serialization.serializeSizedStringV2(bytes, nameData.getData()); + + bytes.write(Longs.toByteArray(nameData.getRegistered())); + + Long updated = nameData.getUpdated(); + int wasUpdated = (updated != null) ? 1 : 0; + bytes.write(Ints.toByteArray(wasUpdated)); + + if (updated != null) { + bytes.write(Longs.toByteArray(nameData.getUpdated())); + } + + int isForSale = nameData.isForSale() ? 1 : 0; + bytes.write(Ints.toByteArray(isForSale)); + + if (nameData.isForSale()) { + bytes.write(Longs.toByteArray(nameData.getSalePrice())); + } + + bytes.write(nameData.getReference()); + + bytes.write(Ints.toByteArray(nameData.getCreationGroupId())); + } + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public NamesMessage cloneWithNewId(int newId) { + NamesMessage clone = new NamesMessage(this.nameDataList); + clone.setId(newId); + return clone; + } + +} From 276f1b7e68ad9034b85deabac01db982df47f0d2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 21:35:32 +0000 Subject: [PATCH 07/20] Fixed small errors in earlier commits. --- src/main/java/org/qortal/controller/Controller.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index a3e2befb..ce856ab5 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -185,7 +185,6 @@ public class Controller extends Thread { public AtomicLong requests = new AtomicLong(); public AtomicLong cacheHits = new AtomicLong(); public AtomicLong unknownAccounts = new AtomicLong(); - public AtomicLong cacheFills = new AtomicLong(); public GetAccountMessageStats() { } @@ -194,9 +193,7 @@ public class Controller extends Thread { public static class GetAccountBalanceMessageStats { public AtomicLong requests = new AtomicLong(); - public AtomicLong cacheHits = new AtomicLong(); public AtomicLong unknownAccounts = new AtomicLong(); - public AtomicLong cacheFills = new AtomicLong(); public GetAccountBalanceMessageStats() { } @@ -1578,7 +1575,7 @@ public class Controller extends Thread { this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement(); // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout - LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s and asset ID %d", peer, address, assetId)); + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId)); // We'll send empty block summaries message as it's very short Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); @@ -1592,7 +1589,7 @@ public class Controller extends Thread { accountMessage.setId(message.getId()); if (!peer.sendMessage(accountMessage)) { - peer.disconnect("failed to send account"); + peer.disconnect("failed to send account balance"); } } catch (DataException e) { From 59119ebc3ba4c4e9f1878908c2e22fa4308a7059 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 20 Mar 2022 21:59:36 +0000 Subject: [PATCH 08/20] Added GET_NAME message to allow lookups from name to owner (or any other name data). --- .../qortal/api/resource/NamesResource.java | 12 +++- .../org/qortal/controller/Controller.java | 48 ++++++++++++++++ .../java/org/qortal/controller/LiteNode.java | 19 +++++++ .../network/message/GetNameMessage.java | 55 +++++++++++++++++++ .../org/qortal/network/message/Message.java | 3 +- 5 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/qortal/network/message/GetNameMessage.java diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index 1b6f0bc8..a900d6bf 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -134,10 +134,18 @@ public class NamesResource { @ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE}) public NameData getName(@PathParam("name") String name) { try (final Repository repository = RepositoryManager.getRepository()) { - NameData nameData = repository.getNameRepository().fromName(name); + NameData nameData; - if (nameData == null) + if (Settings.getInstance().isLite()) { + nameData = LiteNode.getInstance().fetchNameData(name); + } + else { + nameData = repository.getNameRepository().fromName(name); + } + + if (nameData == null) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN); + } return nameData; } catch (ApiException e) { diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index ce856ab5..b6297fe2 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -209,6 +209,15 @@ public class Controller extends Thread { } public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats(); + public static class GetNameMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + + public GetNameMessageStats() { + } + } + public GetNameMessageStats getNameMessageStats = new GetNameMessageStats(); + public AtomicLong latestBlocksCacheRefills = new AtomicLong(); public StatsSnapshot() { @@ -1275,6 +1284,10 @@ public class Controller extends Thread { onNetworkGetAccountNamesMessage(peer, message); break; + case GET_NAME: + onNetworkGetNameMessage(peer, message); + break; + default: LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1632,6 +1645,41 @@ public class Controller extends Thread { } } + private void onNetworkGetNameMessage(Peer peer, Message message) { + GetNameMessage getNameMessage = (GetNameMessage) message; + String name = getNameMessage.getName(); + this.stats.getNameMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + NameData nameData = repository.getNameRepository().fromName(name); + + if (nameData == null) { + // We don't have this account + this.stats.getNameMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name)); + + // We'll send empty block summaries message as it's very short + Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + nameUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(nameUnknownMessage)) + peer.disconnect("failed to send name-unknown response"); + return; + } + + NamesMessage namesMessage = new NamesMessage(Arrays.asList(nameData)); + namesMessage.setId(message.getId()); + + if (!peer.sendMessage(namesMessage)) { + peer.disconnect("failed to send name data"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send name %s to peer %s", name, peer), e); + } + } + // Utilities diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index 8d36b414..b047d295 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -80,6 +80,25 @@ public class LiteNode { return namesMessage.getNameDataList(); } + /** + * Fetch info about a registered name + * @param name - the name to query + * @return a NameData object, or null if not retrieved + */ + public NameData fetchNameData(String name) { + GetNameMessage getNameMessage = new GetNameMessage(name); + NamesMessage namesMessage = (NamesMessage) this.sendMessage(getNameMessage, NAMES); + if (namesMessage == null) { + return null; + } + List nameDataList = namesMessage.getNameDataList(); + if (nameDataList == null || nameDataList.size() != 1) { + return null; + } + // We are only expecting a single item in the list + return nameDataList.get(0); + } + private Message sendMessage(Message message, MessageType expectedResponseMessageType) { // This asks a random peer for the data diff --git a/src/main/java/org/qortal/network/message/GetNameMessage.java b/src/main/java/org/qortal/network/message/GetNameMessage.java new file mode 100644 index 00000000..bdef5170 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetNameMessage.java @@ -0,0 +1,55 @@ +package org.qortal.network.message; + +import org.qortal.naming.Name; +import org.qortal.transform.TransformationException; +import org.qortal.transform.Transformer; +import org.qortal.utils.Serialization; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetNameMessage extends Message { + + private String name; + + public GetNameMessage(String address) { + this(-1, address); + } + + private GetNameMessage(int id, String name) { + super(id, MessageType.GET_NAME); + + this.name = name; + } + + public String getName() { + return this.name; + } + + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + try { + String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); + + return new GetNameMessage(id, name); + } catch (TransformationException e) { + return null; + } + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + Serialization.serializeSizedStringV2(bytes, this.name); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index f01ac8c3..c675cd96 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -109,7 +109,8 @@ public abstract class Message { GET_ACCOUNT_BALANCE(171), NAMES(180), - GET_ACCOUNT_NAMES(181); + GET_ACCOUNT_NAMES(181), + GET_NAME(182); public final int value; public final Method fromByteBufferMethod; From a63fa1cce5b6d44574a014689f8a7dd7ca9ea173 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 22 Mar 2022 08:46:10 +0000 Subject: [PATCH 09/20] Added GET_ACCOUNT_TRANSACTIONS message, as well as a generic TRANSACTIONS message for responses. --- .../org/qortal/controller/Controller.java | 58 ++++++++++++++ .../java/org/qortal/controller/LiteNode.java | 34 ++++++++ .../GetAccountTransactionsMessage.java | 71 +++++++++++++++++ .../org/qortal/network/message/Message.java | 5 +- .../network/message/TransactionsMessage.java | 77 +++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java create mode 100644 src/main/java/org/qortal/network/message/TransactionsMessage.java diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index b6297fe2..02dddcc9 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -32,6 +32,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.qortal.api.ApiService; import org.qortal.api.DomainMapService; import org.qortal.api.GatewayService; +import org.qortal.api.resource.TransactionsResource; import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.BlockTimingByHeight; @@ -200,6 +201,15 @@ public class Controller extends Thread { } public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public static class GetAccountTransactionsMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + + public GetAccountTransactionsMessageStats() { + } + } + public GetAccountTransactionsMessageStats getAccountTransactionsMessageStats = new GetAccountTransactionsMessageStats(); + public static class GetAccountNamesMessageStats { public AtomicLong requests = new AtomicLong(); public AtomicLong unknownAccounts = new AtomicLong(); @@ -1280,6 +1290,10 @@ public class Controller extends Thread { onNetworkGetAccountBalanceMessage(peer, message); break; + case GET_ACCOUNT_TRANSACTIONS: + onNetworkGetAccountTransactionsMessage(peer, message); + break; + case GET_ACCOUNT_NAMES: onNetworkGetAccountNamesMessage(peer, message); break; @@ -1610,6 +1624,50 @@ public class Controller extends Thread { } } + private void onNetworkGetAccountTransactionsMessage(Peer peer, Message message) { + GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message; + String address = getAccountTransactionsMessage.getAddress(); + int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100); + int offset = getAccountTransactionsMessage.getOffset(); + this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, + null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false); + + // Expand signatures to transactions + List transactions = new ArrayList<>(signatures.size()); + for (byte[] signature : signatures) { + transactions.add(repository.getTransactionRepository().fromSignature(signature)); + } + + if (transactions == null) { + // We don't have this account + this.stats.getAccountTransactionsMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + TransactionsMessage transactionsMessage = new TransactionsMessage(transactions); + transactionsMessage.setId(message.getId()); + + if (!peer.sendMessage(transactionsMessage)) { + peer.disconnect("failed to send account transactions"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send transactions for account %s %d to peer %s", address, peer), e); + } + } + private void onNetworkGetAccountNamesMessage(Peer peer, Message message) { GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message; String address = getAccountNamesMessage.getAddress(); diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index b047d295..535142b0 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.naming.NameData; +import org.qortal.data.transaction.TransactionData; import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.network.message.*; @@ -24,6 +25,8 @@ public class LiteNode { public Map pendingRequests = Collections.synchronizedMap(new HashMap<>()); + public int MAX_TRANSACTIONS_PER_MESSAGE = 100; + public LiteNode() { @@ -66,6 +69,37 @@ public class LiteNode { return accountMessage.getAccountBalanceData(); } + /** + * Fetch list of transactions for given QORT address + * @param address - the QORT address to query + * @param limit - the maximum number of results to return + * @param offset - the starting index + * @return a list of TransactionData objects, or null if not retrieved + */ + public List fetchAccountTransactions(String address, int limit, int offset) { + List allTransactions = new ArrayList<>(); + if (limit == 0) { + limit = Integer.MAX_VALUE; + } + int batchSize = Math.min(limit, MAX_TRANSACTIONS_PER_MESSAGE); + + while (allTransactions.size() < limit) { + GetAccountTransactionsMessage getAccountTransactionsMessage = new GetAccountTransactionsMessage(address, batchSize, offset); + TransactionsMessage transactionsMessage = (TransactionsMessage) this.sendMessage(getAccountTransactionsMessage, TRANSACTIONS); + if (transactionsMessage == null) { + // An error occurred, so give up instead of returning partial results + return null; + } + allTransactions.addAll(transactionsMessage.getTransactions()); + if (transactionsMessage.getTransactions().size() < batchSize) { + // No more transactions to fetch + break; + } + offset += batchSize; + } + return allTransactions; + } + /** * Fetch list of names for given QORT address * @param address - the QORT address to query diff --git a/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java new file mode 100644 index 00000000..3ca802b7 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java @@ -0,0 +1,71 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountTransactionsMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + private int limit; + private int offset; + + public GetAccountTransactionsMessage(String address, int limit, int offset) { + this(-1, address, limit, offset); + } + + private GetAccountTransactionsMessage(int id, String address, int limit, int offset) { + super(id, MessageType.GET_ACCOUNT_TRANSACTIONS); + + this.address = address; + this.limit = limit; + this.offset = offset; + } + + public String getAddress() { + return this.address; + } + + public int getLimit() { return this.limit; } + + public int getOffset() { return this.offset; } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + int limit = bytes.getInt(); + + int offset = bytes.getInt(); + + return new GetAccountTransactionsMessage(id, address, limit, offset); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + bytes.write(Ints.toByteArray(this.limit)); + + bytes.write(Ints.toByteArray(this.offset)); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index c675cd96..747988be 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -110,7 +110,10 @@ public abstract class Message { NAMES(180), GET_ACCOUNT_NAMES(181), - GET_NAME(182); + GET_NAME(182), + + TRANSACTIONS(190), + GET_ACCOUNT_TRANSACTIONS(191); public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/TransactionsMessage.java b/src/main/java/org/qortal/network/message/TransactionsMessage.java new file mode 100644 index 00000000..8a8cf11e --- /dev/null +++ b/src/main/java/org/qortal/network/message/TransactionsMessage.java @@ -0,0 +1,77 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.data.transaction.TransactionData; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.TransactionTransformer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class TransactionsMessage extends Message { + + private List transactions; + + public TransactionsMessage(List transactions) { + this(-1, transactions); + } + + private TransactionsMessage(int id, List transactions) { + super(id, MessageType.TRANSACTIONS); + + this.transactions = transactions; + } + + public List getTransactions() { + return this.transactions; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + try { + final int transactionCount = byteBuffer.getInt(); + + List transactions = new ArrayList<>(); + + for (int i = 0; i < transactionCount; ++i) { + TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer); + transactions.add(transactionData); + } + + if (byteBuffer.hasRemaining()) { + return null; + } + + return new TransactionsMessage(id, transactions); + } catch (TransformationException e) { + return null; + } + } + + @Override + protected byte[] toData() { + if (this.transactions == null) + return null; + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(this.transactions.size())); + + for (int i = 0; i < this.transactions.size(); ++i) { + TransactionData transactionData = this.transactions.get(i); + + byte[] serializedTransactionData = TransactionTransformer.toBytes(transactionData); + bytes.write(serializedTransactionData); + } + + return bytes.toByteArray(); + } catch (TransformationException | IOException e) { + return null; + } + } + +} From 3484047ad48fa4eaea03be7504c9c220b012a919 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 22 Mar 2022 08:51:01 +0000 Subject: [PATCH 10/20] Added GET /transactions/address/{address} API endpoint This is a more standardized alternative to using GET /transactions/search?address=xyz. This avoids the need to build full transaction search ability into the lite node protocols right away. --- .../api/resource/TransactionsResource.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/TransactionsResource.java b/src/main/java/org/qortal/api/resource/TransactionsResource.java index 55ad7cde..79195d7c 100644 --- a/src/main/java/org/qortal/api/resource/TransactionsResource.java +++ b/src/main/java/org/qortal/api/resource/TransactionsResource.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -32,6 +33,8 @@ import org.qortal.api.ApiException; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.model.SimpleTransactionSignRequest; import org.qortal.controller.Controller; +import org.qortal.controller.LiteNode; +import org.qortal.crypto.Crypto; import org.qortal.data.transaction.TransactionData; import org.qortal.globalization.Translator; import org.qortal.repository.DataException; @@ -366,6 +369,73 @@ public class TransactionsResource { } } + @GET + @Path("/address/{address}") + @Operation( + summary = "Returns transactions for given address", + responses = { + @ApiResponse( + description = "transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + public List getAddressTransactions(@PathParam("address") String address, + @Parameter(ref = "limit") @QueryParam("limit") Integer limit, + @Parameter(ref = "offset") @QueryParam("offset") Integer offset, + @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) { + if (!Crypto.isValidAddress(address)) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + if (limit == null) { + limit = 0; + } + if (offset == null) { + offset = 0; + } + + List transactions; + + if (Settings.getInstance().isLite()) { + // Fetch from network + transactions = LiteNode.getInstance().fetchAccountTransactions(address, limit, offset); + + // Sort the data, since we can't guarantee the order that a peer sent it in + if (reverse) { + transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp).reversed()); + } else { + transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp)); + } + } + else { + // Fetch from local db + try (final Repository repository = RepositoryManager.getRepository()) { + List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, + null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, reverse); + + // Expand signatures to transactions + transactions = new ArrayList<>(signatures.size()); + for (byte[] signature : signatures) { + transactions.add(repository.getTransactionRepository().fromSignature(signature)); + } + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + return transactions; + } + @GET @Path("/unitfee") @Operation( From 0815ad2cf0e975e47dc3b6f2e72c44692c541ba0 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 22 Mar 2022 08:54:10 +0000 Subject: [PATCH 11/20] Added AT transaction deserialization, to all them to be sent in messages for lite nodes. Note that it is currently not easy to distinguish between MESSAGE-type and PAYMENT-type AT transactions, so PAYMENT-type is currently the only one supported (and used). A hard fork will likely be needed in order to specify the type within each message. --- .../transaction/AtTransactionTransformer.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java index 6b3c93f0..76b0c74d 100644 --- a/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java @@ -4,8 +4,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import org.qortal.account.NullAccount; import org.qortal.data.transaction.ATTransactionData; +import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.TransactionData; +import org.qortal.group.Group; import org.qortal.transform.TransformationException; import org.qortal.utils.Serialization; @@ -18,11 +21,34 @@ public class AtTransactionTransformer extends TransactionTransformer { // Property lengths public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - throw new TransformationException("Serialized AT transactions should not exist!"); + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + String atAddress = Serialization.deserializeAddress(byteBuffer); + + String recipient = Serialization.deserializeAddress(byteBuffer); + + // Assume PAYMENT-type, as these are the only ones used in ACCTs + // TODO: add support for MESSAGE-type + long assetId = byteBuffer.getLong(); + + long amount = byteBuffer.getLong(); + + long fee = byteBuffer.getLong(); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, fee, signature); + + return new ATTransactionData(baseTransactionData, atAddress, recipient, amount, assetId); } public static int getDataLength(TransactionData transactionData) throws TransformationException { - throw new TransformationException("Serialized AT transactions should not exist!"); + return TYPE_LENGTH + TIMESTAMP_LENGTH + REFERENCE_LENGTH + ADDRESS_LENGTH + ADDRESS_LENGTH + + ASSET_ID_LENGTH + AMOUNT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; } // Used for generating fake transaction signatures From a1be66f02bd03a51a4398e604e3f0c327a70f8bf Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 22 Mar 2022 08:57:33 +0000 Subject: [PATCH 12/20] Temporarily ease the filtering of lite node peers, in order to make development easier. --- src/main/java/org/qortal/controller/LiteNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index 535142b0..b706c17f 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -145,13 +145,13 @@ public class LiteNode { peers.removeIf(Controller.hasMisbehaved); // Disregard peers that only have genesis block - peers.removeIf(Controller.hasOnlyGenesisBlock); + // TODO: peers.removeIf(Controller.hasOnlyGenesisBlock); // Disregard peers that are on an old version peers.removeIf(Controller.hasOldVersion); // Disregard peers that are on a known inferior chain tip - peers.removeIf(Controller.hasInferiorChainTip); + // TODO: peers.removeIf(Controller.hasInferiorChainTip); if (peers.isEmpty()) { LOGGER.info("No peers available to send {} message to", message.getType()); From f8a5ded0bab66af8823c8fd357b0c612372d2a17 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 16 Apr 2022 23:20:49 +0100 Subject: [PATCH 13/20] Fix for bug introduced in commit cfe9252 --- src/main/java/org/qortal/transaction/Transaction.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 8f2c7e97..8bb90464 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -549,6 +549,10 @@ public abstract class Transaction { return ValidationResult.OK; } + PublicKeyAccount creator = this.getCreator(); + if (creator == null) + return ValidationResult.MISSING_CREATOR; + // Reject if unconfirmed pile already has X transactions from same creator if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount()) return ValidationResult.TOO_MANY_UNCONFIRMED; @@ -559,10 +563,6 @@ public abstract class Transaction { if (this.getDeadline() <= latestBlock.getTimestamp()) return ValidationResult.TIMESTAMP_TOO_OLD; - PublicKeyAccount creator = this.getCreator(); - if (creator == null) - return ValidationResult.MISSING_CREATOR; - // Check transaction's txGroupId if (!this.isValidTxGroupId()) return ValidationResult.INVALID_TX_GROUP_ID; From 54ff564bb198704ed3eae09fc6f3491fff59ab92 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 16 Apr 2022 23:32:42 +0100 Subject: [PATCH 14/20] Set name for transaction importer thread --- src/main/java/org/qortal/controller/TransactionImporter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/qortal/controller/TransactionImporter.java b/src/main/java/org/qortal/controller/TransactionImporter.java index 22893bbd..233887f7 100644 --- a/src/main/java/org/qortal/controller/TransactionImporter.java +++ b/src/main/java/org/qortal/controller/TransactionImporter.java @@ -55,6 +55,8 @@ public class TransactionImporter extends Thread { @Override public void run() { + Thread.currentThread().setName("Transaction Importer"); + try { while (!Controller.isStopping()) { Thread.sleep(1000L); From b5522ea260cb0c59f2cc3f34a47ce8545d57619c Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 22 Apr 2022 20:06:37 +0100 Subject: [PATCH 15/20] Added support for PAYMENT-type AT transactions in serialization tests --- src/test/java/org/qortal/test/SerializationTests.java | 1 - .../qortal/test/common/transaction/AtTestTransaction.java | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/qortal/test/SerializationTests.java b/src/test/java/org/qortal/test/SerializationTests.java index 5a9fffa8..d9fe978c 100644 --- a/src/test/java/org/qortal/test/SerializationTests.java +++ b/src/test/java/org/qortal/test/SerializationTests.java @@ -47,7 +47,6 @@ public class SerializationTests extends Common { switch (txType) { case GENESIS: case ACCOUNT_FLAGS: - case AT: case CHAT: case PUBLICIZE: case AIRDROP: diff --git a/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java index 74216f2f..38c1c7f3 100644 --- a/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java +++ b/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java @@ -18,10 +18,9 @@ public class AtTestTransaction extends TestTransaction { String recipient = account.getAddress(); long amount = 123L * Amounts.MULTIPLIER; final long assetId = Asset.QORT; - byte[] message = new byte[32]; - random.nextBytes(message); - return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId, message); + // Use PAYMENT-type - i.e. a null message + return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId, null); } } From 522ef282c81afdbe6e94df71b84069b2b8b9769b Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 22 Apr 2022 20:34:42 +0100 Subject: [PATCH 16/20] Added support for deserialization of MESSAGE-type AT transactions (requires transaction version 6) --- .../transaction/AtTransactionTransformer.java | 91 +++++++++++++++++-- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java index 76b0c74d..bb1381b2 100644 --- a/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/AtTransactionTransformer.java @@ -9,6 +9,7 @@ import org.qortal.data.transaction.ATTransactionData; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.group.Group; +import org.qortal.transaction.Transaction; import org.qortal.transform.TransformationException; import org.qortal.utils.Serialization; @@ -20,9 +21,16 @@ public class AtTransactionTransformer extends TransactionTransformer { protected static final TransactionLayout layout = null; // Property lengths + + private static final int MESSAGE_SIZE_LENGTH = INT_LENGTH; + private static final int TYPE_LENGTH = INT_LENGTH; + + public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { long timestamp = byteBuffer.getLong(); + int version = Transaction.getVersionByTimestamp(timestamp); + byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); @@ -30,11 +38,34 @@ public class AtTransactionTransformer extends TransactionTransformer { String recipient = Serialization.deserializeAddress(byteBuffer); - // Assume PAYMENT-type, as these are the only ones used in ACCTs - // TODO: add support for MESSAGE-type - long assetId = byteBuffer.getLong(); + // Default to PAYMENT-type, as there were no MESSAGE-type transactions before transaction v6 + boolean isMessageType = false; - long amount = byteBuffer.getLong(); + if (version >= 6) { + // Version 6 supports both PAYMENT-type and MESSAGE-type, specified using an integer. + // This could be extended to support additional types at a later date, simply by adding + // additional integer values. + int type = byteBuffer.getInt(); + isMessageType = (type == 1); + } + + int messageLength = 0; + byte[] message = null; + long assetId = 0L; + long amount = 0L; + + if (isMessageType) { + messageLength = byteBuffer.getInt(); + + message = new byte[messageLength]; + byteBuffer.get(message); + } + else { + // Assume PAYMENT-type, as there were no MESSAGE-type transactions until this time + assetId = byteBuffer.getLong(); + + amount = byteBuffer.getLong(); + } long fee = byteBuffer.getLong(); @@ -43,12 +74,44 @@ public class AtTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, fee, signature); - return new ATTransactionData(baseTransactionData, atAddress, recipient, amount, assetId); + if (isMessageType) { + // MESSAGE-type + return new ATTransactionData(baseTransactionData, atAddress, recipient, message); + } + else { + // PAYMENT-type + return new ATTransactionData(baseTransactionData, atAddress, recipient, amount, assetId); + } + } public static int getDataLength(TransactionData transactionData) throws TransformationException { - return TYPE_LENGTH + TIMESTAMP_LENGTH + REFERENCE_LENGTH + ADDRESS_LENGTH + ADDRESS_LENGTH + - ASSET_ID_LENGTH + AMOUNT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; + ATTransactionData atTransactionData = (ATTransactionData) transactionData; + int version = Transaction.getVersionByTimestamp(transactionData.getTimestamp()); + + final int baseLength = TYPE_LENGTH + TIMESTAMP_LENGTH + REFERENCE_LENGTH + ADDRESS_LENGTH + ADDRESS_LENGTH + + FEE_LENGTH + SIGNATURE_LENGTH; + + int typeSpecificLength = 0; + + byte[] message = atTransactionData.getMessage(); + boolean isMessageType = (message != null); + + // MESSAGE-type and PAYMENT-type transactions will have differing lengths + if (isMessageType) { + typeSpecificLength = MESSAGE_SIZE_LENGTH + message.length; + } + else { + typeSpecificLength = ASSET_ID_LENGTH + AMOUNT_LENGTH; + } + + // V6 transactions include an extra integer to denote the type + int versionSpecificLength = 0; + if (version >= 6) { + versionSpecificLength = TYPE_LENGTH; + } + + return baseLength + typeSpecificLength + versionSpecificLength; } // Used for generating fake transaction signatures @@ -56,6 +119,8 @@ public class AtTransactionTransformer extends TransactionTransformer { try { ATTransactionData atTransactionData = (ATTransactionData) transactionData; + int version = Transaction.getVersionByTimestamp(atTransactionData.getTimestamp()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(Ints.toByteArray(atTransactionData.getType().value)); @@ -68,7 +133,17 @@ public class AtTransactionTransformer extends TransactionTransformer { byte[] message = atTransactionData.getMessage(); - if (message != null) { + boolean isMessageType = (message != null); + int type = isMessageType ? 1 : 0; + + if (version >= 6) { + // Version 6 supports both PAYMENT-type and MESSAGE-type, specified using an integer. + // This could be extended to support additional types at a later date, simply by adding + // additional integer values. + bytes.write(Ints.toByteArray(type)); + } + + if (isMessageType) { // MESSAGE-type bytes.write(Ints.toByteArray(message.length)); bytes.write(message); From de4f004a088d8621eba5408fac8dacef5411e0d7 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 22 Apr 2022 20:35:17 +0100 Subject: [PATCH 17/20] Bump to transaction version 6 at a future undecided timestamp. --- src/main/java/org/qortal/block/BlockChain.java | 7 ++++++- src/main/java/org/qortal/transaction/Transaction.java | 5 ++++- src/main/resources/blockchain.json | 3 ++- src/test/resources/test-chain-v2-founder-rewards.json | 3 ++- src/test/resources/test-chain-v2-leftover-reward.json | 3 ++- src/test/resources/test-chain-v2-minting.json | 3 ++- src/test/resources/test-chain-v2-qora-holder-extremes.json | 3 ++- src/test/resources/test-chain-v2-qora-holder.json | 3 ++- src/test/resources/test-chain-v2-reward-levels.json | 3 ++- src/test/resources/test-chain-v2-reward-scaling.json | 3 ++- src/test/resources/test-chain-v2.json | 3 ++- 11 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 86a00574..fcae749f 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -69,7 +69,8 @@ public class BlockChain { newBlockSigHeight, shareBinFix, calcChainWeightTimestamp, - transactionV5Timestamp; + transactionV5Timestamp, + transactionV6Timestamp; } // Custom transaction fees @@ -405,6 +406,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue(); } + public long getTransactionV6Timestamp() { + return this.featureTriggers.get(FeatureTrigger.transactionV6Timestamp.name()).longValue(); + } + // More complex getters for aspects that change by height or timestamp public long getRewardAtHeight(int ourHeight) { diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 8bb90464..a1fd6baa 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -393,7 +393,10 @@ public abstract class Transaction { * @return transaction version number */ public static int getVersionByTimestamp(long timestamp) { - if (timestamp >= BlockChain.getInstance().getTransactionV5Timestamp()) { + if (timestamp >= BlockChain.getInstance().getTransactionV6Timestamp()) { + return 6; + } + else if (timestamp >= BlockChain.getInstance().getTransactionV5Timestamp()) { return 5; } return 4; diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index be62aee4..bba5d3b9 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -57,7 +57,8 @@ "newBlockSigHeight": 320000, "shareBinFix": 399000, "calcChainWeightTimestamp": 1620579600000, - "transactionV5Timestamp": 1642176000000 + "transactionV5Timestamp": 1642176000000, + "transactionV6Timestamp": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index c0ea8fe5..e8a22dfc 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 01505af0..eb6f56a9 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index fcabe4bf..d9893cb2 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 8ec94631..3fbb1375 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 38a563b2..b14dc8b7 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index ab934d26..932a8cda 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 6, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index b3e358b2..6244ea58 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 20ff391c..f308712d 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -52,7 +52,8 @@ "newBlockSigHeight": 999999, "shareBinFix": 999999, "calcChainWeightTimestamp": 0, - "transactionV5Timestamp": 0 + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0 }, "genesisInfo": { "version": 4, From 1da157d33f8f2ca4c76944f2bfb09815e2ca68ad Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 22 Apr 2022 20:36:22 +0100 Subject: [PATCH 18/20] Added separate AT serialization tests, based on generic transaction serialization tests. This allows for testing both MESSAGE-type and PAYMENT-type AT transactions. --- .../qortal/test/at/AtSerializationTests.java | 96 +++++++++++++++++++ .../common/transaction/AtTestTransaction.java | 22 ++++- 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/qortal/test/at/AtSerializationTests.java diff --git a/src/test/java/org/qortal/test/at/AtSerializationTests.java b/src/test/java/org/qortal/test/at/AtSerializationTests.java new file mode 100644 index 00000000..3953bcdf --- /dev/null +++ b/src/test/java/org/qortal/test/at/AtSerializationTests.java @@ -0,0 +1,96 @@ +package org.qortal.test.at; + +import com.google.common.hash.HashCode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.ATTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.Common; +import org.qortal.test.common.transaction.AtTestTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.TransactionTransformer; +import org.qortal.utils.Base58; + +import static org.junit.Assert.assertEquals; + +public class AtSerializationTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @After + public void afterTest() throws DataException { + Common.orphanCheck(); + } + + + @Test + public void testPaymentTypeAtSerialization() throws DataException, TransformationException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Build PAYMENT-type AT transaction + PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice"); + ATTransactionData transactionData = (ATTransactionData) AtTestTransaction.paymentType(repository, signingAccount, true); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(signingAccount); + + final int claimedLength = TransactionTransformer.getDataLength(transactionData); + byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData); + assertEquals("Serialized PAYMENT-type AT transaction length differs from declared length", claimedLength, serializedTransaction.length); + + TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction); + // Re-sign + Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData); + deserializedTransaction.sign(signingAccount); + assertEquals("Deserialized PAYMENT-type AT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature())); + + // Re-serialize to check new length and bytes + final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData); + assertEquals("Reserialized PAYMENT-type AT transaction declared length differs", claimedLength, reclaimedLength); + + byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData); + assertEquals("Reserialized PAYMENT-type AT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString()); + } + } + + @Test + public void testMessageTypeAtSerialization() throws DataException, TransformationException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Build MESSAGE-type AT transaction + PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice"); + ATTransactionData transactionData = (ATTransactionData) AtTestTransaction.messageType(repository, signingAccount, true); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(signingAccount); + + // MESSAGE-type AT transactions are only fully supported since transaction V6 + assertEquals(6, Transaction.getVersionByTimestamp(transactionData.getTimestamp())); + + final int claimedLength = TransactionTransformer.getDataLength(transactionData); + byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData); + assertEquals("Serialized MESSAGE-type AT transaction length differs from declared length", claimedLength, serializedTransaction.length); + + TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction); + // Re-sign + Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData); + deserializedTransaction.sign(signingAccount); + assertEquals("Deserialized MESSAGE-type AT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature())); + + // Re-serialize to check new length and bytes + final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData); + assertEquals("Reserialized MESSAGE-type AT transaction declared length differs", claimedLength, reclaimedLength); + + byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData); + assertEquals("Reserialized MESSAGE-type AT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString()); + } + } + +} diff --git a/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java index 38c1c7f3..f827c396 100644 --- a/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java +++ b/src/test/java/org/qortal/test/common/transaction/AtTestTransaction.java @@ -12,15 +12,33 @@ import org.qortal.utils.Amounts; public class AtTestTransaction extends TestTransaction { public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { + return AtTestTransaction.paymentType(repository, account, wantValid); + } + + public static TransactionData paymentType(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { byte[] signature = new byte[64]; random.nextBytes(signature); String atAddress = Crypto.toATAddress(signature); String recipient = account.getAddress(); + + // Use PAYMENT-type long amount = 123L * Amounts.MULTIPLIER; final long assetId = Asset.QORT; - // Use PAYMENT-type - i.e. a null message - return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId, null); + return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId); + } + + public static TransactionData messageType(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { + byte[] signature = new byte[64]; + random.nextBytes(signature); + String atAddress = Crypto.toATAddress(signature); + String recipient = account.getAddress(); + + // Use MESSAGE-type + byte[] message = new byte[32]; + random.nextBytes(message); + + return new ATTransactionData(generateBase(account), atAddress, recipient, message); } } From 52904db41316207d0bf80eb7f2c4113ad428cad3 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 30 Apr 2022 15:22:50 +0100 Subject: [PATCH 19/20] Migrated new lite node message types to new format. --- .../org/qortal/controller/Controller.java | 4 +- .../java/org/qortal/controller/LiteNode.java | 3 +- .../message/AccountBalanceMessage.java | 46 ++++---- .../network/message/AccountMessage.java | 66 +++++------ .../message/GetAccountBalanceMessage.java | 38 +++---- .../network/message/GetAccountMessage.java | 37 +++--- .../message/GetAccountNamesMessage.java | 34 +++--- .../GetAccountTransactionsMessage.java | 42 ++++--- .../network/message/GetNameMessage.java | 34 +++--- .../qortal/network/message/MessageType.java | 16 ++- .../qortal/network/message/NamesMessage.java | 105 ++++++++---------- .../network/message/TransactionsMessage.java | 57 +++++----- 12 files changed, 232 insertions(+), 250 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 7aae376a..81067315 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1666,7 +1666,9 @@ public class Controller extends Thread { } } catch (DataException e) { - LOGGER.error(String.format("Repository issue while send transactions for account %s %d to peer %s", address, peer), e); + LOGGER.error(String.format("Repository issue while sending transactions for account %s %d to peer %s", address, peer), e); + } catch (MessageException e) { + LOGGER.error(String.format("Message serialization issue while sending transactions for account %s %d to peer %s", address, peer), e); } } diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index b706c17f..fb958ea6 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -13,8 +13,7 @@ import org.qortal.network.message.*; import java.security.SecureRandom; import java.util.*; -import static org.qortal.network.message.Message.MessageType; -import static org.qortal.network.message.Message.MessageType.*; +import static org.qortal.network.message.MessageType.*; public class LiteNode { diff --git a/src/main/java/org/qortal/network/message/AccountBalanceMessage.java b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java index 5e64d2b5..7a9ad725 100644 --- a/src/main/java/org/qortal/network/message/AccountBalanceMessage.java +++ b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java @@ -7,19 +7,34 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class AccountBalanceMessage extends Message { private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; - private final AccountBalanceData accountBalanceData; + private AccountBalanceData accountBalanceData; public AccountBalanceMessage(AccountBalanceData accountBalanceData) { super(MessageType.ACCOUNT_BALANCE); - this.accountBalanceData = accountBalanceData; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(accountBalanceData.getAddress()); + bytes.write(address); + + bytes.write(Longs.toByteArray(accountBalanceData.getAssetId())); + + bytes.write(Longs.toByteArray(accountBalanceData.getBalance())); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } public AccountBalanceMessage(int id, AccountBalanceData accountBalanceData) { @@ -33,7 +48,7 @@ public class AccountBalanceMessage extends Message { } - public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) { byte[] addressBytes = new byte[ADDRESS_LENGTH]; byteBuffer.get(addressBytes); String address = Base58.encode(addressBytes); @@ -46,29 +61,6 @@ public class AccountBalanceMessage extends Message { return new AccountBalanceMessage(id, accountBalanceData); } - @Override - protected byte[] toData() { - if (this.accountBalanceData == null) { - return null; - } - - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(this.accountBalanceData.getAddress()); - bytes.write(address); - - bytes.write(Longs.toByteArray(this.accountBalanceData.getAssetId())); - - bytes.write(Longs.toByteArray(this.accountBalanceData.getBalance())); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - public AccountBalanceMessage cloneWithNewId(int newId) { AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData); clone.setId(newId); diff --git a/src/main/java/org/qortal/network/message/AccountMessage.java b/src/main/java/org/qortal/network/message/AccountMessage.java index 749ec01e..d22ef879 100644 --- a/src/main/java/org/qortal/network/message/AccountMessage.java +++ b/src/main/java/org/qortal/network/message/AccountMessage.java @@ -7,7 +7,6 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class AccountMessage extends Message { @@ -16,12 +15,38 @@ public class AccountMessage extends Message { private static final int REFERENCE_LENGTH = Transformer.SIGNATURE_LENGTH; private static final int PUBLIC_KEY_LENGTH = Transformer.PUBLIC_KEY_LENGTH; - private final AccountData accountData; + private AccountData accountData; public AccountMessage(AccountData accountData) { super(MessageType.ACCOUNT); - this.accountData = accountData; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(accountData.getAddress()); + bytes.write(address); + + bytes.write(accountData.getReference()); + + bytes.write(accountData.getPublicKey()); + + bytes.write(Ints.toByteArray(accountData.getDefaultGroupId())); + + bytes.write(Ints.toByteArray(accountData.getFlags())); + + bytes.write(Ints.toByteArray(accountData.getLevel())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMinted())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment())); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } public AccountMessage(int id, AccountData accountData) { @@ -34,7 +59,7 @@ public class AccountMessage extends Message { return this.accountData; } - public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) { byte[] addressBytes = new byte[ADDRESS_LENGTH]; byteBuffer.get(addressBytes); String address = Base58.encode(addressBytes); @@ -59,39 +84,6 @@ public class AccountMessage extends Message { return new AccountMessage(id, accountData); } - @Override - protected byte[] toData() { - if (this.accountData == null) { - return null; - } - - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(accountData.getAddress()); - bytes.write(address); - - bytes.write(accountData.getReference()); - - bytes.write(accountData.getPublicKey()); - - bytes.write(Ints.toByteArray(accountData.getDefaultGroupId())); - - bytes.write(Ints.toByteArray(accountData.getFlags())); - - bytes.write(Ints.toByteArray(accountData.getLevel())); - - bytes.write(Ints.toByteArray(accountData.getBlocksMinted())); - - bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment())); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - public AccountMessage cloneWithNewId(int newId) { AccountMessage clone = new AccountMessage(this.accountData); clone.setId(newId); diff --git a/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java index c4caaa34..43892b83 100644 --- a/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java +++ b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java @@ -6,7 +6,6 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class GetAccountBalanceMessage extends Message { @@ -17,7 +16,23 @@ public class GetAccountBalanceMessage extends Message { private long assetId; public GetAccountBalanceMessage(String address, long assetId) { - this(-1, address, assetId); + super(MessageType.GET_ACCOUNT_BALANCE); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] addressBytes = Base58.decode(address); + bytes.write(addressBytes); + + bytes.write(Longs.toByteArray(assetId)); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private GetAccountBalanceMessage(int id, String address, long assetId) { @@ -35,7 +50,7 @@ public class GetAccountBalanceMessage extends Message { return this.assetId; } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) { byte[] addressBytes = new byte[ADDRESS_LENGTH]; bytes.get(addressBytes); String address = Base58.encode(addressBytes); @@ -45,21 +60,4 @@ public class GetAccountBalanceMessage extends Message { return new GetAccountBalanceMessage(id, address, assetId); } - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(this.address); - bytes.write(address); - - bytes.write(Longs.toByteArray(this.assetId)); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - } diff --git a/src/main/java/org/qortal/network/message/GetAccountMessage.java b/src/main/java/org/qortal/network/message/GetAccountMessage.java index 1d18117c..4f2a6dec 100644 --- a/src/main/java/org/qortal/network/message/GetAccountMessage.java +++ b/src/main/java/org/qortal/network/message/GetAccountMessage.java @@ -5,7 +5,7 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; public class GetAccountMessage extends Message { @@ -15,7 +15,21 @@ public class GetAccountMessage extends Message { private String address; public GetAccountMessage(String address) { - this(-1, address); + super(MessageType.GET_ACCOUNT); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] addressBytes = Base58.decode(address); + bytes.write(addressBytes); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private GetAccountMessage(int id, String address) { @@ -28,9 +42,9 @@ public class GetAccountMessage extends Message { return this.address; } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) { if (bytes.remaining() != ADDRESS_LENGTH) - return null; + throw new BufferUnderflowException(); byte[] addressBytes = new byte[ADDRESS_LENGTH]; bytes.get(addressBytes); @@ -39,19 +53,4 @@ public class GetAccountMessage extends Message { return new GetAccountMessage(id, address); } - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(this.address); - bytes.write(address); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - } diff --git a/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java index b95c7eb0..bde697c5 100644 --- a/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java +++ b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java @@ -5,7 +5,6 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class GetAccountNamesMessage extends Message { @@ -15,7 +14,21 @@ public class GetAccountNamesMessage extends Message { private String address; public GetAccountNamesMessage(String address) { - this(-1, address); + super(MessageType.GET_ACCOUNT_NAMES); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] addressBytes = Base58.decode(address); + bytes.write(addressBytes); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private GetAccountNamesMessage(int id, String address) { @@ -29,7 +42,7 @@ public class GetAccountNamesMessage extends Message { } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) { byte[] addressBytes = new byte[ADDRESS_LENGTH]; bytes.get(addressBytes); String address = Base58.encode(addressBytes); @@ -37,19 +50,4 @@ public class GetAccountNamesMessage extends Message { return new GetAccountNamesMessage(id, address); } - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(this.address); - bytes.write(address); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - } diff --git a/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java index 3ca802b7..fe921cc9 100644 --- a/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java +++ b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java @@ -6,7 +6,6 @@ import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class GetAccountTransactionsMessage extends Message { @@ -18,7 +17,25 @@ public class GetAccountTransactionsMessage extends Message { private int offset; public GetAccountTransactionsMessage(String address, int limit, int offset) { - this(-1, address, limit, offset); + super(MessageType.GET_ACCOUNT_TRANSACTIONS); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Send raw address instead of base58 encoded + byte[] addressBytes = Base58.decode(address); + bytes.write(addressBytes); + + bytes.write(Ints.toByteArray(limit)); + + bytes.write(Ints.toByteArray(offset)); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private GetAccountTransactionsMessage(int id, String address, int limit, int offset) { @@ -37,7 +54,7 @@ public class GetAccountTransactionsMessage extends Message { public int getOffset() { return this.offset; } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) { byte[] addressBytes = new byte[ADDRESS_LENGTH]; bytes.get(addressBytes); String address = Base58.encode(addressBytes); @@ -49,23 +66,4 @@ public class GetAccountTransactionsMessage extends Message { return new GetAccountTransactionsMessage(id, address, limit, offset); } - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - // Send raw address instead of base58 encoded - byte[] address = Base58.decode(this.address); - bytes.write(address); - - bytes.write(Ints.toByteArray(this.limit)); - - bytes.write(Ints.toByteArray(this.offset)); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - } diff --git a/src/main/java/org/qortal/network/message/GetNameMessage.java b/src/main/java/org/qortal/network/message/GetNameMessage.java index bdef5170..10fae08a 100644 --- a/src/main/java/org/qortal/network/message/GetNameMessage.java +++ b/src/main/java/org/qortal/network/message/GetNameMessage.java @@ -2,12 +2,10 @@ package org.qortal.network.message; import org.qortal.naming.Name; import org.qortal.transform.TransformationException; -import org.qortal.transform.Transformer; import org.qortal.utils.Serialization; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; public class GetNameMessage extends Message { @@ -15,7 +13,19 @@ public class GetNameMessage extends Message { private String name; public GetNameMessage(String address) { - this(-1, address); + super(MessageType.GET_NAME); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + Serialization.serializeSizedStringV2(bytes, name); + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private GetNameMessage(int id, String name) { @@ -29,26 +39,14 @@ public class GetNameMessage extends Message { } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException { try { String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); return new GetNameMessage(id, name); + } catch (TransformationException e) { - return null; - } - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - Serialization.serializeSizedStringV2(bytes, this.name); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; + throw new MessageException(e.getMessage(), e); } } diff --git a/src/main/java/org/qortal/network/message/MessageType.java b/src/main/java/org/qortal/network/message/MessageType.java index 48039a4d..8ad7a0da 100644 --- a/src/main/java/org/qortal/network/message/MessageType.java +++ b/src/main/java/org/qortal/network/message/MessageType.java @@ -61,7 +61,21 @@ public enum MessageType { GET_TRADE_PRESENCES(141, GetTradePresencesMessage::fromByteBuffer), ARBITRARY_METADATA(150, ArbitraryMetadataMessage::fromByteBuffer), - GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer); + GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer), + + // Lite node support + ACCOUNT(160, AccountMessage::fromByteBuffer), + GET_ACCOUNT(161, GetAccountMessage::fromByteBuffer), + + ACCOUNT_BALANCE(170, AccountBalanceMessage::fromByteBuffer), + GET_ACCOUNT_BALANCE(171, GetAccountBalanceMessage::fromByteBuffer), + + NAMES(180, NamesMessage::fromByteBuffer), + GET_ACCOUNT_NAMES(181, GetAccountNamesMessage::fromByteBuffer), + GET_NAME(182, GetNameMessage::fromByteBuffer), + + TRANSACTIONS(190, TransactionsMessage::fromByteBuffer), + GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer); public final int value; public final MessageProducer fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/NamesMessage.java b/src/main/java/org/qortal/network/message/NamesMessage.java index d8f0b857..942818cc 100644 --- a/src/main/java/org/qortal/network/message/NamesMessage.java +++ b/src/main/java/org/qortal/network/message/NamesMessage.java @@ -10,7 +10,7 @@ import org.qortal.utils.Serialization; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -19,12 +19,55 @@ public class NamesMessage extends Message { private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH; - private final List nameDataList; + private List nameDataList; public NamesMessage(List nameDataList) { super(MessageType.NAMES); - this.nameDataList = nameDataList; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + bytes.write(Ints.toByteArray(nameDataList.size())); + + for (int i = 0; i < nameDataList.size(); ++i) { + NameData nameData = nameDataList.get(i); + + Serialization.serializeSizedStringV2(bytes, nameData.getName()); + + Serialization.serializeSizedStringV2(bytes, nameData.getReducedName()); + + Serialization.serializeAddress(bytes, nameData.getOwner()); + + Serialization.serializeSizedStringV2(bytes, nameData.getData()); + + bytes.write(Longs.toByteArray(nameData.getRegistered())); + + Long updated = nameData.getUpdated(); + int wasUpdated = (updated != null) ? 1 : 0; + bytes.write(Ints.toByteArray(wasUpdated)); + + if (updated != null) { + bytes.write(Longs.toByteArray(nameData.getUpdated())); + } + + int isForSale = nameData.isForSale() ? 1 : 0; + bytes.write(Ints.toByteArray(isForSale)); + + if (nameData.isForSale()) { + bytes.write(Longs.toByteArray(nameData.getSalePrice())); + } + + bytes.write(nameData.getReference()); + + bytes.write(Ints.toByteArray(nameData.getCreationGroupId())); + } + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } public NamesMessage(int id, List nameDataList) { @@ -38,7 +81,7 @@ public class NamesMessage extends Message { } - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException { try { final int nameCount = bytes.getInt(); @@ -80,63 +123,13 @@ public class NamesMessage extends Message { } if (bytes.hasRemaining()) { - return null; + throw new BufferUnderflowException(); } return new NamesMessage(id, nameDataList); } catch (TransformationException e) { - return null; - } - } - - @Override - protected byte[] toData() { - if (this.nameDataList == null) { - return null; - } - - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.nameDataList.size())); - - for (int i = 0; i < this.nameDataList.size(); ++i) { - NameData nameData = this.nameDataList.get(i); - - Serialization.serializeSizedStringV2(bytes, nameData.getName()); - - Serialization.serializeSizedStringV2(bytes, nameData.getReducedName()); - - Serialization.serializeAddress(bytes, nameData.getOwner()); - - Serialization.serializeSizedStringV2(bytes, nameData.getData()); - - bytes.write(Longs.toByteArray(nameData.getRegistered())); - - Long updated = nameData.getUpdated(); - int wasUpdated = (updated != null) ? 1 : 0; - bytes.write(Ints.toByteArray(wasUpdated)); - - if (updated != null) { - bytes.write(Longs.toByteArray(nameData.getUpdated())); - } - - int isForSale = nameData.isForSale() ? 1 : 0; - bytes.write(Ints.toByteArray(isForSale)); - - if (nameData.isForSale()) { - bytes.write(Longs.toByteArray(nameData.getSalePrice())); - } - - bytes.write(nameData.getReference()); - - bytes.write(Ints.toByteArray(nameData.getCreationGroupId())); - } - - return bytes.toByteArray(); - } catch (IOException e) { - return null; + throw new MessageException(e.getMessage(), e); } } diff --git a/src/main/java/org/qortal/network/message/TransactionsMessage.java b/src/main/java/org/qortal/network/message/TransactionsMessage.java index 8a8cf11e..d7d60331 100644 --- a/src/main/java/org/qortal/network/message/TransactionsMessage.java +++ b/src/main/java/org/qortal/network/message/TransactionsMessage.java @@ -7,7 +7,7 @@ import org.qortal.transform.transaction.TransactionTransformer; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -16,8 +16,29 @@ public class TransactionsMessage extends Message { private List transactions; - public TransactionsMessage(List transactions) { - this(-1, transactions); + public TransactionsMessage(List transactions) throws MessageException { + super(MessageType.TRANSACTIONS); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + bytes.write(Ints.toByteArray(transactions.size())); + + for (int i = 0; i < transactions.size(); ++i) { + TransactionData transactionData = transactions.get(i); + + byte[] serializedTransactionData = TransactionTransformer.toBytes(transactionData); + bytes.write(serializedTransactionData); + } + + } catch (IOException e) { + throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream"); + } catch (TransformationException e) { + throw new MessageException(e.getMessage(), e); + } + + this.dataBytes = bytes.toByteArray(); + this.checksumBytes = Message.generateChecksum(this.dataBytes); } private TransactionsMessage(int id, List transactions) { @@ -30,7 +51,7 @@ public class TransactionsMessage extends Message { return this.transactions; } - public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException { try { final int transactionCount = byteBuffer.getInt(); @@ -42,35 +63,13 @@ public class TransactionsMessage extends Message { } if (byteBuffer.hasRemaining()) { - return null; + throw new BufferUnderflowException(); } return new TransactionsMessage(id, transactions); + } catch (TransformationException e) { - return null; - } - } - - @Override - protected byte[] toData() { - if (this.transactions == null) - return null; - - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.transactions.size())); - - for (int i = 0; i < this.transactions.size(); ++i) { - TransactionData transactionData = this.transactions.get(i); - - byte[] serializedTransactionData = TransactionTransformer.toBytes(transactionData); - bytes.write(serializedTransactionData); - } - - return bytes.toByteArray(); - } catch (TransformationException | IOException e) { - return null; + throw new MessageException(e.getMessage(), e); } } From 7f9d26799253fcbcfa28cb334a1eef06a20c1b82 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 30 Apr 2022 15:32:23 +0100 Subject: [PATCH 20/20] Improved lite node response error logging. --- src/main/java/org/qortal/controller/LiteNode.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index fb958ea6..cfbe8321 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -172,7 +172,12 @@ public class LiteNode { return null; } - if (responseMessage == null || responseMessage.getType() != expectedResponseMessageType) { + if (responseMessage == null) { + LOGGER.info("Peer didn't respond to {} message", peer, message.getType()); + return null; + } + else if (responseMessage.getType() != expectedResponseMessageType) { + LOGGER.info("Peer responded with unexpected message type {} (should be {})", peer, responseMessage.getType(), expectedResponseMessageType); return null; }