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.
This commit is contained in:
CalDescent 2022-03-20 18:28:51 +00:00
parent 0e3a9ee2b2
commit cfe92525ed
23 changed files with 142 additions and 37 deletions

View File

@ -61,6 +61,11 @@ public class BlockMinter extends Thread {
public void run() { public void run() {
Thread.currentThread().setName("BlockMinter"); Thread.currentThread().setName("BlockMinter");
if (Settings.getInstance().isLite()) {
// Lite nodes do not mint
return;
}
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) { if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
// Wipe existing unconfirmed transactions // Wipe existing unconfirmed transactions

View File

@ -362,23 +362,27 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error return; // Not System.exit() so that GUI can display error
} }
// Rebuild Names table and check database integrity (if enabled) // If we have a non-lite node, we need to perform some startup actions
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); if (!Settings.getInstance().isLite()) {
namesDatabaseIntegrityCheck.rebuildAllNames();
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
namesDatabaseIntegrityCheck.runIntegrityCheck();
}
LOGGER.info("Validating blockchain"); // Rebuild Names table and check database integrity (if enabled)
try { NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
BlockChain.validate(); namesDatabaseIntegrityCheck.rebuildAllNames();
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
namesDatabaseIntegrityCheck.runIntegrityCheck();
}
Controller.getInstance().refillLatestBlocksCache(); LOGGER.info("Validating blockchain");
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight())); try {
} catch (DataException e) { BlockChain.validate();
LOGGER.error("Couldn't validate blockchain", e);
Gui.getInstance().fatalError("Blockchain validation issue", e); Controller.getInstance().refillLatestBlocksCache();
return; // Not System.exit() so that GUI can display error 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 // 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); final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
synchronized (Synchronizer.getInstance().syncLock) { 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"); actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
SysTray.getInstance().setTrayIcon(2); 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); SysTray.getInstance().setToolTipText(tooltip);
this.callbackExecutor.execute(() -> { this.callbackExecutor.execute(() -> {
@ -933,6 +945,11 @@ public class Controller extends Thread {
// Callbacks for/from network // Callbacks for/from network
public void doNetworkBroadcast() { public void doNetworkBroadcast() {
if (Settings.getInstance().isLite()) {
// Lite nodes have nothing to broadcast
return;
}
Network network = Network.getInstance(); Network network = Network.getInstance();
// Send (if outbound) / Request peer lists // Send (if outbound) / Request peer lists
@ -1450,11 +1467,13 @@ public class Controller extends Thread {
private void onNetworkHeightV2Message(Peer peer, Message message) { private void onNetworkHeightV2Message(Peer peer, Message message) {
HeightV2Message heightV2Message = (HeightV2Message) message; HeightV2Message heightV2Message = (HeightV2Message) message;
// If peer is inbound and we've not updated their height if (!Settings.getInstance().isLite()) {
// then this is probably their initial HEIGHT_V2 message // If peer is inbound and we've not updated their height
// so they need a corresponding HEIGHT_V2 message from us // then this is probably their initial HEIGHT_V2 message
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null)) // so they need a corresponding HEIGHT_V2 message from us
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip())); if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
}
// Update peer chain tip data // Update peer chain tip data
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey()); 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 * @return boolean - whether our node's blockchain is up to date or not
*/ */
public boolean isUpToDate(Long minLatestBlockTimestamp) { 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? // Do we even have a vaguely recent block?
if (minLatestBlockTimestamp == null) if (minLatestBlockTimestamp == null)
return false; return false;

View File

@ -134,6 +134,11 @@ public class Synchronizer extends Thread {
public void run() { public void run() {
Thread.currentThread().setName("Synchronizer"); Thread.currentThread().setName("Synchronizer");
if (Settings.getInstance().isLite()) {
// Lite nodes don't need to sync
return;
}
try { try {
while (running && !Controller.isStopping()) { while (running && !Controller.isStopping()) {
Thread.sleep(1000); Thread.sleep(1000);

View File

@ -11,6 +11,7 @@ import org.qortal.network.message.TransactionSignaturesMessage;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
@ -105,6 +106,8 @@ public class TransactionImporter extends Thread {
List<Transaction> sigValidTransactions = new ArrayList<>(); List<Transaction> sigValidTransactions = new ArrayList<>();
boolean isLiteNode = Settings.getInstance().isLite();
// Signature validation round - does not require blockchain lock // Signature validation round - does not require blockchain lock
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) { for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
// Quick exit? // Quick exit?
@ -118,6 +121,12 @@ public class TransactionImporter extends Thread {
// Only validate signature if we haven't already done so // Only validate signature if we haven't already done so
Boolean isSigValid = transactionEntry.getValue(); Boolean isSigValid = transactionEntry.getValue();
if (!Boolean.TRUE.equals(isSigValid)) { 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()) { if (!transaction.isSignatureValid()) {
String signature58 = Base58.encode(transactionData.getSignature()); String signature58 = Base58.encode(transactionData.getSignature());

View File

@ -19,6 +19,11 @@ public class AtStatesPruner implements Runnable {
public void run() { public void run() {
Thread.currentThread().setName("AT States pruner"); Thread.currentThread().setName("AT States pruner");
if (Settings.getInstance().isLite()) {
// Nothing to prune in lite mode
return;
}
boolean archiveMode = false; boolean archiveMode = false;
if (!Settings.getInstance().isTopOnly()) { if (!Settings.getInstance().isTopOnly()) {
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving // Top-only mode isn't enabled, but we might want to prune for the purposes of archiving

View File

@ -19,6 +19,11 @@ public class AtStatesTrimmer implements Runnable {
public void run() { public void run() {
Thread.currentThread().setName("AT States trimmer"); Thread.currentThread().setName("AT States trimmer");
if (Settings.getInstance().isLite()) {
// Nothing to trim in lite mode
return;
}
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
int trimStartHeight = repository.getATRepository().getAtTrimHeight(); int trimStartHeight = repository.getATRepository().getAtTrimHeight();

View File

@ -21,7 +21,7 @@ public class BlockArchiver implements Runnable {
public void run() { public void run() {
Thread.currentThread().setName("Block archiver"); Thread.currentThread().setName("Block archiver");
if (!Settings.getInstance().isArchiveEnabled()) { if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
return; return;
} }

View File

@ -19,6 +19,11 @@ public class BlockPruner implements Runnable {
public void run() { public void run() {
Thread.currentThread().setName("Block pruner"); Thread.currentThread().setName("Block pruner");
if (Settings.getInstance().isLite()) {
// Nothing to prune in lite mode
return;
}
boolean archiveMode = false; boolean archiveMode = false;
if (!Settings.getInstance().isTopOnly()) { if (!Settings.getInstance().isTopOnly()) {
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving // Top-only mode isn't enabled, but we might want to prune for the purposes of archiving

View File

@ -21,6 +21,11 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
public void run() { public void run() {
Thread.currentThread().setName("Online Accounts trimmer"); Thread.currentThread().setName("Online Accounts trimmer");
if (Settings.getInstance().isLite()) {
// Nothing to trim in lite mode
return;
}
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
// Don't even start trimming until initial rush has ended // Don't even start trimming until initial rush has ended
Thread.sleep(INITIAL_SLEEP_PERIOD); Thread.sleep(INITIAL_SLEEP_PERIOD);

View File

@ -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 inbound sent anything here, it's possible it could be processed out-of-order with handshake message).
if (peer.isOutbound()) { if (peer.isOutbound()) {
// Send our height if (!Settings.getInstance().isLite()) {
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip()); // Send our height
if (!peer.sendMessage(heightMessage)) { Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
peer.disconnect("failed to send height/info"); if (!peer.sendMessage(heightMessage)) {
return; peer.disconnect("failed to send height/info");
return;
}
} }
// Send our peers list // Send our peers list

View File

@ -62,6 +62,11 @@ public abstract class RepositoryManager {
} }
public static boolean archive(Repository repository) { 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 // Bulk archive the database the first time we use archive mode
if (Settings.getInstance().isArchiveEnabled()) { if (Settings.getInstance().isArchiveEnabled()) {
if (RepositoryManager.canArchiveOrPrune()) { if (RepositoryManager.canArchiveOrPrune()) {
@ -82,6 +87,11 @@ public abstract class RepositoryManager {
} }
public static boolean prune(Repository repository) { 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 // Bulk prune the database the first time we use top-only or block archive mode
if (Settings.getInstance().isTopOnly() || if (Settings.getInstance().isTopOnly() ||
Settings.getInstance().isArchiveEnabled()) { Settings.getInstance().isArchiveEnabled()) {

View File

@ -530,11 +530,6 @@ public abstract class Transaction {
if (now >= this.getDeadline()) if (now >= this.getDeadline())
return ValidationResult.TIMESTAMP_TOO_OLD; 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 // Transactions with a timestamp too far into future are too new
long maxTimestamp = now + Settings.getInstance().getMaxTransactionTimestampFuture(); long maxTimestamp = now + Settings.getInstance().getMaxTransactionTimestampFuture();
if (this.transactionData.getTimestamp() > maxTimestamp) if (this.transactionData.getTimestamp() > maxTimestamp)
@ -545,14 +540,29 @@ public abstract class Transaction {
if (feeValidationResult != ValidationResult.OK) if (feeValidationResult != ValidationResult.OK)
return feeValidationResult; return feeValidationResult;
PublicKeyAccount creator = this.getCreator(); if (Settings.getInstance().isLite()) {
if (creator == null) // Everything from this point is difficult to validate for a lite node, since it has no blocks.
return ValidationResult.MISSING_CREATOR; // 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 // Reject if unconfirmed pile already has X transactions from same creator
if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount()) if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
return ValidationResult.TOO_MANY_UNCONFIRMED; 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 // Check transaction's txGroupId
if (!this.isValidTxGroupId()) if (!this.isValidTxGroupId())
return ValidationResult.INVALID_TX_GROUP_ID; return ValidationResult.INVALID_TX_GROUP_ID;

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Datenbank Instandhaltung
EXIT = Verlassen EXIT = Verlassen
LITE_NODE = Lite node
MINTING_DISABLED = NOT minting MINTING_DISABLED = NOT minting
MINTING_ENABLED = \u2714 Minting MINTING_ENABLED = \u2714 Minting

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Maintenance
EXIT = Exit EXIT = Exit
LITE_NODE = Lite node
MINTING_DISABLED = NOT minting MINTING_DISABLED = NOT minting
MINTING_ENABLED = \u2714 Minting MINTING_ENABLED = \u2714 Minting

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Tietokannan ylläpito
EXIT = Pois EXIT = Pois
LITE_NODE = Lite node
MINTING_DISABLED = EI lyö rahaa MINTING_DISABLED = EI lyö rahaa
MINTING_ENABLED = \u2714 Lyö rahaa MINTING_ENABLED = \u2714 Lyö rahaa

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Maintenance de la base de données
EXIT = Quitter EXIT = Quitter
LITE_NODE = Lite node
MINTING_DISABLED = NE mint PAS MINTING_DISABLED = NE mint PAS
MINTING_ENABLED = \u2714 Minting MINTING_ENABLED = \u2714 Minting

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Adatbázis karbantartás
EXIT = Kilépés EXIT = Kilépés
LITE_NODE = Lite node
MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban
MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Manutenzione del database
EXIT = Uscita EXIT = Uscita
LITE_NODE = Lite node
MINTING_DISABLED = Conio disabilitato MINTING_DISABLED = Conio disabilitato
MINTING_ENABLED = \u2714 Conio abilitato MINTING_ENABLED = \u2714 Conio abilitato

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Onderhoud
EXIT = Verlaten EXIT = Verlaten
LITE_NODE = Lite node
MINTING_DISABLED = Minten is uitgeschakeld MINTING_DISABLED = Minten is uitgeschakeld
MINTING_ENABLED = \u2714 Minten is ingeschakeld MINTING_ENABLED = \u2714 Minten is ingeschakeld

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = Обслуживание базы данных
EXIT = Выход EXIT = Выход
LITE_NODE = Lite node
MINTING_DISABLED = Чеканка отключена MINTING_DISABLED = Чеканка отключена
MINTING_ENABLED = \u2714 Чеканка активна MINTING_ENABLED = \u2714 Чеканка активна

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = 数据库维护
EXIT = 退出核心 EXIT = 退出核心
LITE_NODE = Lite node
MINTING_DISABLED = 没有铸币 MINTING_DISABLED = 没有铸币
MINTING_ENABLED = \u2714 铸币 MINTING_ENABLED = \u2714 铸币

View File

@ -27,6 +27,8 @@ DB_MAINTENANCE = 數據庫維護
EXIT = 退出核心 EXIT = 退出核心
LITE_NODE = Lite node
MINTING_DISABLED = 沒有鑄幣 MINTING_DISABLED = 沒有鑄幣
MINTING_ENABLED = \u2714 鑄幣 MINTING_ENABLED = \u2714 鑄幣

View File

@ -15,7 +15,7 @@ public class CheckTranslations {
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" }; private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
private static final Set<String> SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT", private static final Set<String> 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", "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"); "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK");
private static String failurePrefix; private static String failurePrefix;