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;