diff --git a/.gitmodules b/.gitmodules index 741201947a0..df73cf6108e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,6 @@ [submodule "serialization"] path = submodules/serialization url = https://github.com/bosagora/serialization.git +[submodule "semver"] + path = submodules/semver + url = https://github.com/dcarp/semver.git diff --git a/dub.json b/dub.json index 409671df76c..d193c845077 100644 --- a/dub.json +++ b/dub.json @@ -174,6 +174,7 @@ "libsodiumd": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }, "localrest": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }, "ocean": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }, + "semver": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }, "serialization": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }, "vibe-d": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] } } diff --git a/dub.selections.json b/dub.selections.json index a6c62051c1f..09232b44d6a 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -17,6 +17,7 @@ "mir-linux-kernel": {"path":"submodules/mir-linux-kernel/"}, "ocean": {"path":"submodules/ocean/"}, "openssl": {"path":"submodules/openssl/"}, + "semver": {"path":"submodules/semver/"}, "serialization": {"path":"submodules/serialization/"}, "stdx-allocator": {"path":"submodules/stdx-allocator/"}, "taggedalgebraic": {"path":"submodules/taggedalgebraic/"}, diff --git a/source/agora/node/FullNode.d b/source/agora/node/FullNode.d index 88ed1dcbd20..ea78cad6fa7 100644 --- a/source/agora/node/FullNode.d +++ b/source/agora/node/FullNode.d @@ -246,17 +246,29 @@ public class FullNode : API import std.datetime.systime : Clock, SysTime, unixTimeToStdTime; import std.datetime.timezone: UTC; + enum build_version = import(VersionFileName); + this.config = config; this.log = this.makeLogger(); this.params = FullNode.makeConsensusParams(config); this.stateDB = this.makeStateDB(); this.cacheDB = this.makeCacheDB(); + this.storage = this.makeBlockStorage(); + + // We need to apply updates early on - At the very least, + // before the Ledger is instantiated. + // It would be better if this could be moved out of the ctor, + // but that would require delaying most of the initialization + // to another function. + import agora.node.Versioning; + applyVersionDifferences(this.stateDB, this.cacheDB, this.storage, + build_version, this.config, this.log); + this.taskman = this.makeTaskManager(); this.clock = this.makeClock(this.taskman); this.metadata = this.makeMetadata(); this.network = this.makeNetworkManager(this.metadata, this.taskman, this.clock); - this.storage = this.makeBlockStorage(); this.fee_man = this.makeFeeManager(); this.utxo_set = this.makeUTXOSet(); this.pool = this.makeTransactionPool(); @@ -300,7 +312,6 @@ public class FullNode : API Utils.getCollectorRegistry().addCollector(&this.collectBlockStats); Utils.getCollectorRegistry().addCollector(&this.collectValidatorStats); - enum build_version = import(VersionFileName); this.app_stats.setMetricTo!"agora_application_info"( 1, // Unused, see article linked in the struct's documentationx build_version, diff --git a/source/agora/node/Versioning.d b/source/agora/node/Versioning.d new file mode 100644 index 00000000000..764d99f61f9 --- /dev/null +++ b/source/agora/node/Versioning.d @@ -0,0 +1,160 @@ +/******************************************************************************* + + Apply version upgrades to a node + + To allow for a smooth upgrade path for Agora node, there is a built-in + upgrade system: When a node starts, it checks a specific table in stateDB, + and gradually run upgrade scripts as needed. + + Copyright: + Copyright (c) 2019-2021 BOSAGORA Foundation + All rights reserved. + + License: + MIT License. See LICENSE for details. + +*******************************************************************************/ + +module agora.node.Versioning; + +import agora.common.Config; +import agora.common.Ensure; +import agora.common.ManagedDatabase; +import agora.node.BlockStorage; +import agora.utils.Log; + +import semver; + +import std.path; + +/******************************************************************************* + + Apply all required version upgrades to the state + + This is called from the FullNode's constructor after all the state handling + members (`stateDB`, `cacheDB`, block `storage`) have been initialized. + + If any upgrade is needed at all (the version stored in the metadata + table is different from the one we're running), we apply the required + fix(es). This process can also handle downgrades. + + The logger is also initialized so we can provide feedback to the user. + The config has already been parsed, so any fix needed would need to + happen in two steps (one version allowing the new syntax and doing the + fix, then a new version rejecting the old syntax). + + Params: + stateDB = The node's `stateDB`, holding the blockchain state + cacheDB = The node's `cacheDB`, holding the node's cached data + storage = The node's block storage, holding known blocks + current = The version we are currently running, as read from + the version file + config = The parsed configuration + log = Logger to output any message to, if any + + Throws: + If an error happened during the upgrade, as we don't want to continue + and throw random errors to the user in a seemingly unrelated place. + + ***************************************************************************/ + +package void applyVersionDifferences ( + ManagedDatabase stateDB, ManagedDatabase cacheDB, IBlockStorage storage, + string current, in Config config, Logger log) +{ + // We currently use this in a few places, allow for a transition period + if (current == "HEAD") + { + log.info("Current version is set to HEAD - cannot check for upgrades"); + return; + } + + void printFatalMessages () + { + log.fatal("Cannot continue initialization - State DB is in an inconsistent state"); + log.fatal("Please fix your installation if possible, or remove {} ", + config.node.data_dir.buildPath("state.db")); + log.fatal("This will rebuild your blockchain state from scratch, and may take some time"); + log.fatal("It is recommended to also remove the cache DB at {} if the state DB is removed", + config.node.data_dir.buildPath("state.db")); + log.fatal("If you believe this is an issue with Agora, please report an issue at " ~ + "https://github.com/bosagora/agora/"); + } + + const vers = SemVer(current); + ensure(vers.isValid, "Version '{}' is not a valid version", current); + + const exists = stateDB.execute( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='metadata'") + .oneValue!int; + + // TODO: Remove once v0.17.0 has been deployed and all nodes have a metadata table + version (none) + { + if (!exists) + { + stateDB.execute("CREATE TABLE metadata (key TEXT UNIQUE NOT NULL, value TEXT NOT NULL)"); + stateDB.setMetadataVersion(current); + return; + } + } + else + { + if (!exists) + { + // If the `metadata` table doesn't exists, it means either the DB + // is compromised or this is the first run - Let's check if it's the later. + const tables = stateDB.execute( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table'") + .oneValue!int; + + if (tables == 0) + { + // Create it and return, since there is no existing state + stateDB.execute("CREATE TABLE metadata (key TEXT UNIQUE NOT NULL, value TEXT NOT NULL)"); + stateDB.setMetadataVersion(current); + return; + } + + log.fatal("No 'metadata' table exists in stateDB, but stateDB has {} tables!", tables); + printFatalMessages(); + ensure(false, "Could not determine previous state of the node - Check logs for more infos"); + } + } + + auto results = stateDB.execute("SELECT value FROM metadata WHERE key='version'"); + if (results.empty()) + { + printFatalMessages(); + ensure(false, "Could not find version information in metadata table - Check logs for more infos"); + } + const oldVersStr = results.oneValue!string; + const oldVers = SemVer(oldVersStr); + if (!oldVers.isValid()) + { + printFatalMessages(); + ensure(false, "Version stored in metadata ({}) is not a valid version - " ~ + "Check logs for more infos", oldVersStr); + } + + // Most common case, do not output any message + if (oldVers == vers) return; + + if (oldVers < vers) + { + const size_t upgrades = 1; + log.info("Need to apply {} upgrades from {} to {}", upgrades, oldVers, vers); + } + else + { + const size_t downgrades = 1; + log.info("Need to apply {} downgrades from {} to {}", downgrades, oldVers, vers); + } + stateDB.setMetadataVersion(current); +} + +/// Set the current version in the metadata +private void setMetadataVersion (ManagedDatabase stateDB, string version_) +{ + stateDB.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('version', ?)", version_); +} diff --git a/submodules/semver b/submodules/semver new file mode 160000 index 00000000000..1012043e111 --- /dev/null +++ b/submodules/semver @@ -0,0 +1 @@ +Subproject commit 1012043e1111aa3fcca168049b66d8b1aaacab75