diff --git a/README.md b/README.md index edf99c4df92..29513c30098 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,7 @@ The java-tron project comes with several runnable artifacts and helper scripts f | :---------------------- | :---------- | | **`FullNode.jar`** | Main TRON node executable (generated in `build/libs/` after a successful build following the above guidance). Runs as a full node by default. `java -jar FullNode.jar --help` for command line options| | **`Toolkit.jar`** | Node management utility (generated in `build/libs/`): partition, prune, copy, convert DBs; shadow-fork tool. [Usage](https://tronprotocol.github.io/documentation-en/using_javatron/toolkit/#toolkit-a-java-tron-node-maintenance-suite) | -| **`start.sh`** | Quick start script (x86_64, JDK 8) to download/build/run `FullNode.jar`. See the tool [guide](./shell.md). | -| **`start.sh.simple`** | Quick start script template (ARM64, JDK 17). See usage notes inside the script. | +| **`start.sh`** | Quick start script to download/build/run `FullNode.jar` on x86_64 (JDK 8) and ARM64 (JDK 17). See the tool [guide](./shell.md). | # Running java-tron diff --git a/chainbase/src/main/java/org/tron/core/db/KhaosDatabase.java b/chainbase/src/main/java/org/tron/core/db/KhaosDatabase.java index c1d3bceeb4b..e634194b683 100644 --- a/chainbase/src/main/java/org/tron/core/db/KhaosDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/KhaosDatabase.java @@ -205,37 +205,6 @@ private void checkNull(Object o) throws NonCommonBlockException { } } - /** - * Find two block's most recent common parent block. - */ - @Deprecated - public Pair, LinkedList> getBranch( - BlockId block1, BlockId block2) { - LinkedList list1 = new LinkedList<>(); - LinkedList list2 = new LinkedList<>(); - KhaosBlock kblk1 = miniStore.getByHash(block1); - KhaosBlock kblk2 = miniStore.getByHash(block2); - - if (kblk1 != null && kblk2 != null) { - while (!Objects.equals(kblk1, kblk2)) { - if (kblk1.num > kblk2.num) { - list1.add(kblk1.blk); - kblk1 = kblk1.getParent(); - } else if (kblk1.num < kblk2.num) { - list2.add(kblk2.blk); - kblk2 = kblk2.getParent(); - } else { - list1.add(kblk1.blk); - list2.add(kblk2.blk); - kblk1 = kblk1.getParent(); - kblk2 = kblk2.getParent(); - } - } - } - - return new Pair<>(list1, list2); - } - // only for unit test public BlockCapsule getParentBlock(Sha256Hash hash) { return Stream.of(miniStore.getByHash(hash), miniUnlinkedStore.getByHash(hash)) diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 1dea271ec69..6f369c1a0d3 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -43,6 +43,10 @@ public abstract class HttpService extends AbstractService { protected long maxRequestSize = 4 * 1024 * 1024; // 4MB + // Once maxHttpConnectNumber is reached, open connections time out after this much + // inactivity, so connections that send nothing cannot hold every slot. + private static final long CONNECTION_LIMIT_IDLE_TIMEOUT_MS = 10_000; + @VisibleForTesting public long getMaxRequestSize() { return this.maxRequestSize; @@ -80,7 +84,9 @@ protected void initServer() { this.apiServer = new Server(this.port); int maxHttpConnectNumber = Args.getInstance().getMaxHttpConnectNumber(); if (maxHttpConnectNumber > 0) { - this.apiServer.addBean(new ConnectionLimit(maxHttpConnectNumber, this.apiServer)); + ConnectionLimit connectionLimit = new ConnectionLimit(maxHttpConnectNumber, this.apiServer); + connectionLimit.setIdleTimeout(CONNECTION_LIMIT_IDLE_TIMEOUT_MS); + this.apiServer.addBean(connectionLimit); } this.apiServer.setErrorHandler(new OversizedRequestErrorHandler()); } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 8d56a2193f0..301508288c0 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -1017,14 +1017,16 @@ private static void loadDnsPublishParameters(NodeConfig.DnsConfig dns, if (dns.getChangeThreshold() > 0) { publishConfig.setChangeThreshold(dns.getChangeThreshold()); } else if (Double.compare(dns.getChangeThreshold(), 0.0) != 0) { - logger.error("Check node.dns.changeThreshold, should be bigger than 0, default 0.1"); + throw new TronError("Check node.dns.changeThreshold, should be bigger than 0, default 0.1", + TronError.ErrCode.PARAMETER_INIT); } int maxMergeSize = dns.getMaxMergeSize(); if (maxMergeSize >= 1 && maxMergeSize <= 5) { publishConfig.setMaxMergeSize(maxMergeSize); } else if (maxMergeSize != 0) { - logger.error("Check node.dns.maxMergeSize, should be [1~5], default 5"); + throw new TronError("Check node.dns.maxMergeSize, should be [1~5], default 5", + TronError.ErrCode.PARAMETER_INIT); } if (StringUtils.isNotEmpty(dns.getDnsPrivate())) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 9d7a7c979b9..73317ed58a4 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -5,7 +5,6 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.utils.Commons.adjustBalance; import static org.tron.core.Constant.TRANSACTION_MAX_BYTE_SIZE; -import static org.tron.core.exception.BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED; import static org.tron.protos.Protocol.Transaction.Contract.ContractType.TransferContract; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; diff --git a/framework/src/test/java/org/tron/common/jetty/ConnectionLimitTest.java b/framework/src/test/java/org/tron/common/jetty/ConnectionLimitTest.java new file mode 100644 index 00000000000..ef220749f44 --- /dev/null +++ b/framework/src/test/java/org/tron/common/jetty/ConnectionLimitTest.java @@ -0,0 +1,148 @@ +package org.tron.common.jetty; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; +import org.tron.common.utils.PublicMethod; +import org.tron.core.config.args.Args; + +/** + * Tests the connection limit configured in {@link HttpService}: once connections that send + * nothing hold every slot, the server closes them after the limit's idle timeout and serves + * new clients, instead of waiting for the connector's 30-second idle timeout. + */ +public class ConnectionLimitTest { + + private static final int MAX_CONNECTIONS = 2; + + // Below the connector's default 30-second idle timeout, above the limit's 10-second one + // applied twice (Jetty half-closes an idle connection before closing it). + private static final int TIMEOUT_MS = 25_000; + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static TestHttpService httpService; + private static int port; + + public static class OkServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.getWriter().print("ok"); + } + } + + static class TestHttpService extends HttpService { + TestHttpService(int port) { + this.port = port; + this.contextPath = "/"; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new OkServlet()), "/*"); + } + + int connectedEndPoints() { + return ((AbstractConnector) apiServer.getConnectors()[0]).getConnectedEndPoints().size(); + } + } + + @BeforeClass + public static void setup() throws Exception { + Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + Args.getInstance().setMaxHttpConnectNumber(MAX_CONNECTIONS); + port = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(port); + httpService.start().get(10, TimeUnit.SECONDS); + } + + @AfterClass + public static void teardown() throws Exception { + try { + if (httpService != null) { + httpService.stop(); + } + } finally { + Args.clearParam(); + } + } + + @Test(timeout = 60_000) + public void testIdleConnectionsDoNotLockOutClients() throws Exception { + List idleSockets = new ArrayList<>(); + try { + for (int i = 0; i < MAX_CONNECTIONS; i++) { + Socket socket = new Socket(); + socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS); + idleSockets.add(socket); + awaitConnectedEndPoints(i + 1); + } + + Assert.assertEquals("HTTP/1.1 200 OK", get()); + for (Socket socket : idleSockets) { + assertClosedByServer(socket); + } + } finally { + for (Socket socket : idleSockets) { + socket.close(); + } + } + } + + private static void awaitConnectedEndPoints(int expected) throws InterruptedException { + long deadline = System.currentTimeMillis() + TIMEOUT_MS; + while (httpService.connectedEndPoints() < expected) { + Assert.assertTrue("server did not accept connection " + expected, + System.currentTimeMillis() < deadline); + Thread.sleep(10); + } + } + + private static String get() throws IOException { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS); + socket.setSoTimeout(TIMEOUT_MS); + OutputStream out = socket.getOutputStream(); + out.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" + .getBytes(StandardCharsets.US_ASCII)); + out.flush(); + BufferedReader in = new BufferedReader( + new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); + return in.readLine(); + } + } + + private static void assertClosedByServer(Socket socket) throws IOException { + socket.setSoTimeout(TIMEOUT_MS); + try { + Assert.assertEquals(-1, socket.getInputStream().read()); + } catch (SocketException e) { + // a reset also means the server dropped the connection + } + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 2118140b45b..f366945be16 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -544,6 +544,30 @@ public void testDnsPublishRejectsEmptyRequiredParameterWithParameterInitError() error.getMessage()); } + @Test + public void testDnsPublishRejectsInvalidChangeThresholdWithParameterInitError() { + Config config = dnsPublishConfig("node.dns.changeThreshold", "-0.1"); + + TronError error = Assert.assertThrows(TronError.class, + () -> Args.loadDnsPublishConfig(NodeConfig.fromConfig(config))); + + Assert.assertEquals(TronError.ErrCode.PARAMETER_INIT, error.getErrCode()); + Assert.assertEquals("Check node.dns.changeThreshold, should be bigger than 0, default 0.1", + error.getMessage()); + } + + @Test + public void testDnsPublishRejectsInvalidMaxMergeSizeWithParameterInitError() { + Config config = dnsPublishConfig("node.dns.maxMergeSize", "6"); + + TronError error = Assert.assertThrows(TronError.class, + () -> Args.loadDnsPublishConfig(NodeConfig.fromConfig(config))); + + Assert.assertEquals(TronError.ErrCode.PARAMETER_INIT, error.getErrCode()); + Assert.assertEquals("Check node.dns.maxMergeSize, should be [1~5], default 5", + error.getMessage()); + } + @Test public void testCommitteeConfigRejectsOldRewardOptimizationWithoutPrerequisite() { Map configMap = new HashMap<>(); diff --git a/shell.md b/shell.md index 700067a9aa3..8a69cd2b170 100644 --- a/shell.md +++ b/shell.md @@ -8,6 +8,10 @@ If you already downloaded the `FullNode.jar`, you can use `start.sh` to run it, The script is available in the java-tron project at [github](https://github.com/tronprotocol/java-tron), or if you need a separate script: [start.sh](https://github.com/tronprotocol/java-tron/blob/develop/start.sh) +The script runs on x86_64 with JDK 8 and on ARM64 with JDK 17. It picks the JVM options for the Java version it finds, and downloads the release jars built for that architecture (`FullNode-aarch64.jar` on ARM64). + +Downloaded release jars are verified against the GPG signature published with each release, made by the key listed under "Integrity Check" in the [README](./README.md), so `gpg` must be installed; a jar that fails verification is not used. The mainnet config is downloaded from java-tron and the Nile testnet config from [nile-testnet](https://github.com/tron-nile-testnet/nile-testnet). + *** # Usage @@ -32,6 +36,8 @@ The script is available in the java-tron project at [github](https://github.com/ sh start.sh --stop ``` + `--run` records the process id in `.pid` (`FullNode.jar.pid` by default) next to `start.log`, and `--stop` reads it. Run `--stop` in the directory the node was started from, with the same `-j` name if one was given. After `--release` or `-cb` the node runs in `FullNode/`; `--stop` finds it there from the parent directory as well. A node started by an earlier version of the script (a `start.log` but no pid file) is still found by its jar name. + * Get the latest version of `FullNode.jar` and start it ``` @@ -58,17 +64,21 @@ The script is available in the java-tron project at [github](https://github.com/ start the service -* `--stop` +* `--stop` or `-s` + + stop the service started from the current directory + +* `--` - stop the service + Everything after it is passed to `FullNode.jar` unchanged. Options the script does not know are passed on as well, so `--` is only needed when a value of such an option looks like a script option or a jar name. * `-c` - Specify the configuration file, by default it will load the `config.conf` in the same directory as `FullNode.jar` + Specify the configuration file, by default it will load the `config.conf` in the current directory * `-d` - Specify the database storage path, The default path is the same directory where `FullNode.jar` is located. + Specify the database storage path. The default is `output-directory` in the directory the script is run from (`FullNode/` after `--release` or `-cb`). * `-j` @@ -79,7 +89,7 @@ The script is available in the java-tron project at [github](https://github.com/ Specify the maximum memory of the `FullNode.jar` service in`MB`, jvm's startup maximum memory will be adjusted according to this parameter. * `--net` - Select test and private networks. + Select test (Nile) and private networks. ### build project @@ -91,6 +101,14 @@ The script is available in the java-tron project at [github](https://github.com/ Get the latest released version of the `jar` package from github. +* `--upgrade` + + Replace the local `jar` package with the latest release; the previous one is kept as `FullNode.jar_bak`. + +* `--download` + + Download the latest released `jar` package into the current directory without starting it. + ### rebuild the manifest @@ -100,7 +118,7 @@ The script is available in the java-tron project at [github](https://github.com/ * `-m` - specify the minimum required manifest file size ,unit:M,default:0 + specify the minimum required manifest file size ,unit:M,default:128 * `-b` @@ -152,7 +170,7 @@ sh start.sh --stop Format: ``` -sh start.sh <[--release | -cb]> <--run> [-m ] | [-b ] | [-d | [-dr | --disable-rewrite-manifes]] +sh start.sh <[--release | -cb]> <--run> [-m ] | [-b ] | [-d | [-dr | --disable-rewrite-manifest]] ``` Get the latest released version. @@ -162,13 +180,15 @@ Get the latest released version. sh start.sh --release --run ``` -Following file structure will be generated after executing the above command and the `FullNode.jar` will be started. +Following file structure will be generated after executing the above command and the `FullNode.jar` will be started. The node runs from `FullNode/`, so later `--run` commands are executed there with the copied script; `--stop` works both there and from the parent directory. ``` ├── ... ├── FullNode/ ├── config.conf ├── FullNode.jar + ├── FullNode.jar.pid + ├── start.log ├── start.sh ``` @@ -214,12 +234,14 @@ Following file structure will be created: ├── FullNode/ |── config.conf ├── FullNode.jar + ├── FullNode.jar.pid + ├── start.log ├── start.sh ``` ### 3. rebuild manifest tool -This tool provides the ability to reformat the manifest based on current database, Enabled by default. +This tool provides the ability to reformat the manifest based on current database, Enabled by default. It applies to LevelDB only and is skipped on ARM64, which runs RocksDB. 1.Local mode: diff --git a/start.sh b/start.sh index 1472a94dc62..1da7ae3915c 100644 --- a/start.sh +++ b/start.sh @@ -17,16 +17,22 @@ # ############################################################################## +# The script needs bash. Rerun under bash when it was started with sh and sh is another shell, +# such as dash on Debian and Ubuntu. +if [ -z "$BASH_VERSION" ]; then + exec bash "$0" "$@" +fi + # Build FullNode config FULL_NODE_DIR="FullNode" FULL_NODE_CONFIG_DIR="config" # config file -FULL_NODE_CONFIG_MAIN_NET="main_net_config.conf" FULL_NODE_CONFIG_TEST_NET="test_net_config.conf" FULL_NODE_CONFIG_PRIVATE_NET="private_net_config.conf" DEFAULT_FULL_NODE_CONFIG='config.conf' JAR_NAME="FullNode.jar" -FULL_START_OPT='' +# FullNode options given on the command line, passed on as they are. +FULL_START_OPT=() # Github GITHUB_BRANCH='master' @@ -42,25 +48,31 @@ MAX_STOP_TIME=60 # Modify this option to allow the minimum memory to be started, unit MB ALLOW_MIN_MEMORY=8192 -# JVM option +# JVM option, adjust as needed MAX_DIRECT_MEMORY=1g JVM_MS=4g -JVM_MX=4g +JVM_MX=9g IS_BACKUP_GC_LOG=true SPECIFY_MEMORY=0 -RUN=false UPGRADE=false # Rebuild manifest +# REBUILD_DIR is relative to the directory the node is started from, like the -d option of +# FullNode, so it stays right after --release or -cb change into $FULL_NODE_DIR. REBUILD_MANIFEST=true -REBUILD_DIR="$PWD/output-directory/database" -REBUILD_MANIFEST_SIZE=0 +REBUILD_DIR="output-directory/database" +REBUILD_MANIFEST_SIZE=128 REBUILD_BATCH_SIZE=80000 # Download and upgrade DOWNLOAD=false RELEASE_URL='https://github.com/tronprotocol/java-tron/releases' +# Release jars are signed with this key, see "Integrity Check" in README.md +RELEASE_KEY_FINGERPRINT='1254F859D2B1BD9F66E7107DF859BCB44A28290B' +RELEASE_KEY_SERVERS='hkps://keys.openpgp.org hkps://keyserver.ubuntu.com' +MAIN_NET_CONFIG_URL='https://raw.githubusercontent.com/tronprotocol/java-tron/master/framework/src/main/resources/config.conf' +TEST_NET_CONFIG_URL='https://raw.githubusercontent.com/tron-nile-testnet/nile-testnet/master/framework/src/main/resources/config-nile.conf' QUICK_START=false CLONE_BUILD=false @@ -70,27 +82,40 @@ else GITHUB_REPOSITORY=$GITHUB_REPOSITORY_SSH_URL fi +# macOS derives JAVA_HOME in its own way, see below. +darwin=false +case "`uname`" in + Darwin*) darwin=true ;; +esac + # Determine the Java command to use to start the JVM. +# A JAVA_HOME from the environment is used as is. Otherwise it is derived from the installed JDK. if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else + if $darwin ; then + # /usr/bin/java and /usr/bin/javac are stubs that run the JDK selected by + # /usr/libexec/java_home. Deriving JAVA_HOME from their path gives /usr, and the + # stub then runs itself forever. + JAVA_HOME="`/usr/libexec/java_home 2>/dev/null`" + else + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then javaExecutable="`readlink -f \"$javaExecutable\"`" + javaHome="`dirname \"$javaExecutable\"`" + JAVA_HOME=`expr "$javaHome" : '\(.*\)/bin'` fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME fi fi + # Export it so that child processes, such as the gradle build of -cb, use the same JDK. + if [ -n "$JAVA_HOME" ]; then + export JAVA_HOME + fi fi +# A JAVACMD from the environment is used as is. Otherwise it is java under JAVA_HOME when JAVA_HOME +# is set, else java on PATH. if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -114,155 +139,250 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi +# JVM system properties of $JAVACMD, read once and looked up by javaProperty. JAVA_ERROR holds +# the first line of output when java cannot run, for example a JDK built for another CPU. +JAVA_ERROR='' +if ! JAVA_PROPERTIES=$("$JAVACMD" -XshowSettings:properties -version 2>&1); then + JAVA_ERROR=$(echo "$JAVA_PROPERTIES" | head -n 1) + JAVA_PROPERTIES='' +fi +# Prints one JVM system property from the -XshowSettings output above. +javaProperty() { + echo "$JAVA_PROPERTIES" | awk -F ' = ' -v key="$1" '$1 ~ "^ *" key "$" {print $2; exit}' +} + +# x86_64 runs on JDK 8 and ARM64 on JDK 17, each with its own release jars +# JAVA_SPEC_VERSION selects the GC options in startService. ARM64 release assets carry the +# -aarch64 suffix, and x86_64 assets have none. +JAVA_SPEC_VERSION=$(javaProperty java.specification.version) +case "$(javaProperty os.arch)" in + aarch64|arm64) RELEASE_ARCH_SUFFIX='-aarch64' ;; + *) RELEASE_ARCH_SUFFIX='' ;; +esac +# Release asset names of the node jar and of the manifest rebuild tool for this architecture. +RELEASE_JAR="FullNode$RELEASE_ARCH_SUFFIX.jar" +RELEASE_ARCHIVE_JAR="ArchiveManifest$RELEASE_ARCH_SUFFIX.jar" + +# Exits when $JAVACMD cannot run or its version is unknown. Called before anything that needs +# java, so that --stop still works with a broken JDK. +checkJava() { + if [ -n "$JAVA_ERROR" ]; then + echo "Error: $JAVACMD cannot run: $JAVA_ERROR" >&2 + exit 1 + fi + if [ -z "$JAVA_SPEC_VERSION" ]; then + echo "Error: cannot determine the Java version of $JAVACMD" >&2 + exit 1 + fi +} + +# Archives the gc.log of the stopped node into logs/gc_logs/ and keeps the newest 5 archives. backupGCLog() { local maxFile=5 - local gcLogDir=logs/gc_logs/ - if [ ! -d "$gcLogDir" ];then - mkdir -p 'logs/gc_logs' - fi + local gcLogDir=logs/gc_logs + local dateformat + local archives + local count + mkdir -p "$gcLogDir" if [ -f 'gc.log' ]; then echo '[info] backup gc.log' - local dateformat=`date "+%Y-%m-%d_%H-%M-%S"` - tar -czvf gc.log_$dateformat'.tar.gz' gc.log - mv gc.log_$dateformat'.tar.gz' $gcLogDir - rm -rf gc.log - - # checking the number of backups - local currentDirCount=`ls -l $gcLogDir | grep "gc.log*" | wc -l` - if [ $currentDirCount -gt $maxFile ]; then - local oldFileSize=`expr $currentDirCount - $maxFile` - local oldGcLogFiles=(`ls -1 $gcLogDir |head -n $oldFileSize`) + dateformat=$(date "+%Y-%m-%d_%H-%M-%S") + tar -czf "$gcLogDir/gc.log_$dateformat.tar.gz" gc.log && rm -f gc.log + + # Archive names sort by their timestamp, so the first ones listed are the oldest. Other + # files in the directory are left alone. + archives=$(ls -1 "$gcLogDir" | grep '^gc\.log_.*\.tar\.gz$') + count=$(echo "$archives" | grep -c .) + if [ "$count" -gt "$maxFile" ]; then + echo "$archives" | head -n $((count - maxFile)) | while read -r fileName; do + rm -f "$gcLogDir/$fileName" + done fi - - for fileName in ${oldGcLogFiles[@]}; do - rm -rf $gcLogDir$fileName - done fi } +# Prints the newest GreatVoyage release tag of the java-tron repository; prints nothing when +# the tags cannot be listed. getLatestReleaseVersion() { - full_node_version=`git ls-remote --tags $GITHUB_REPOSITORY |grep GreatVoyage- | awk -F '/' 'END{print $3}'` - if [[ -n $full_node_version ]]; then - echo $full_node_version - else - echo '' - fi -} - -checkVersion() { - github_release_version=$(`echo getLatestReleaseVersion`) - if [[ -n $github_release_version ]]; then - echo "info: github latest version: $github_release_version" - echo $github_release_version - else - echo 'info: not getting the latest version' - exit - fi + # List the remote tags, keep the GreatVoyage-vX.Y.Z ones and take the highest version. + git ls-remote --tags --refs $GITHUB_REPOSITORY 2>/dev/null | awk -F '/' '{print $3}' \ + | grep -E '^GreatVoyage-v[0-9]+(\.[0-9]+)*$' | sort -V | tail -1 } +# --upgrade: downloads and verifies the latest release jar, then replaces $JAR_NAME with it and +# keeps the previous file as ${JAR_NAME}_bak. Exits without touching $JAR_NAME if that fails. upgrade() { - latest_version=$(`echo getLatestReleaseVersion`) + latest_version=$(getLatestReleaseVersion) echo "info: latest version: $latest_version" if [[ -n $latest_version ]]; then - old_jar="$PWD/$JAR_NAME" - if [[ -f $old_jar ]]; then - echo "info: backup $old_jar" - mv $PWD/$JAR_NAME $PWD/$JAR_NAME'_bak' - fi - download $RELEASE_URL/download/$latest_version/$JAR_NAME $JAR_NAME - if [[ $? == 0 ]]; then + # Download to a temporary name first, so that a failed download or signature check leaves + # $JAR_NAME untouched. + if downloadRelease "$latest_version" "$RELEASE_JAR" "$JAR_NAME.download"; then + # Verified: keep the previous jar as ${JAR_NAME}_bak and move the new one into place. + if [[ -f $JAR_NAME ]]; then + echo "info: backup $JAR_NAME" + mv "$JAR_NAME" "${JAR_NAME}_bak" + fi + mv "$JAR_NAME.download" "$JAR_NAME" echo "info: download version $latest_version success" + else + # download or checkSign has already removed the temporary file. + echo "warn: upgrade aborted, $JAR_NAME is unchanged" + exit 1 fi else + # No release tag could be listed. The current jar is kept. echo 'info: nothing to upgrade' fi } +# Downloads $1 to file $2 with wget or curl, verifying the TLS certificate. Returns 1 and +# leaves no file behind when the download fails. download() { local url=$1 local file_name=$2 if type wget >/dev/null 2>&1; then - wget --no-check-certificate -q $url + # wget follows redirects by default. + wget -q -O "$file_name" "$url" || { rm -f "$file_name"; return 1; } elif type curl >/dev/null 2>&1; then - echo "curl -OLJ $url" - curl -OLJ $url + # -f fails on HTTP errors instead of saving the error page, -L follows redirects. + echo "curl -fsSL -o $file_name $url" + curl -fsSL -o "$file_name" "$url" || { rm -f "$file_name"; return 1; } else echo 'info: no exists wget or curl, make sure the system can use the "wget" or "curl" command' + return 1 fi } +# Downloads asset $2 of release $1 to file $3 and verifies its signature. +downloadRelease() { + download "$RELEASE_URL/download/$1/$2" "$3" && checkSign "$1" "$2" "$3" +} + +# Creates the $FULL_NODE_DIR directory with a copy of this script and changes into it. mkdirFullNode() { - if [ ! -d $FULL_NODE_DIR ]; then + if [ ! -d "$FULL_NODE_DIR" ]; then echo "info: create $FULL_NODE_DIR" - mkdir $FULL_NODE_DIR - $(cp $0 $FULL_NODE_DIR) - cd $FULL_NODE_DIR - elif [ -d $FULL_NODE_DIR ]; then - cd $FULL_NODE_DIR + mkdir "$FULL_NODE_DIR" + cp "$0" "$FULL_NODE_DIR" fi + cd "$FULL_NODE_DIR" || exit 1 } +# --release / --deploy: sets up $FULL_NODE_DIR with the mainnet config and the latest verified +# release jar. Exits when the version, the config or the jar cannot be fetched. quickStart() { - full_node_version=$(`echo getLatestReleaseVersion`) + full_node_version=$(getLatestReleaseVersion) if [[ -n $full_node_version ]]; then + # Enter $FULL_NODE_DIR, then download the mainnet config and the signed release jar for + # this architecture. mkdirFullNode echo "info: check latest version: $full_node_version" echo 'info: download config' - download https://raw.githubusercontent.com/tronprotocol/tron-deployment/$GITHUB_BRANCH/$FULL_NODE_CONFIG_MAIN_NET $FULL_NODE_CONFIG_MAIN_NET - mv $FULL_NODE_CONFIG_MAIN_NET 'config.conf' + download "$MAIN_NET_CONFIG_URL" config.conf || exit 1 + # Download to a temporary name first, so that a failed download or signature check leaves + # an existing $JAR_NAME untouched. echo "info: download $full_node_version" - download $RELEASE_URL/download/$full_node_version/$JAR_NAME $JAR_NAME - checkSign + downloadRelease "$full_node_version" "$RELEASE_JAR" "$JAR_NAME.download" || exit 1 + mv "$JAR_NAME.download" "$JAR_NAME" else + # Without a release tag there is nothing to download. echo 'info: not getting the latest version' - exit + exit 1 fi } +# --clone: clones branch $GITHUB_BRANCH of java-tron into the current directory. cloneCode() { if type git >/dev/null 2>&1; then - git_clone=$(git clone -b $GITHUB_BRANCH $GITHUB_REPOSITORY) - if [[ git_clone == 0 ]]; then + # The checkout goes to ./java-tron. + if git clone -b $GITHUB_BRANCH $GITHUB_REPOSITORY; then echo 'info: git clone java-tron success' + else + echo 'warn: git clone java-tron failed' + return 1 fi else echo 'info: no exists git, make sure the system can use the "git" command' + return 1 fi } +# -cb: clones and builds java-tron, then copies FullNode.jar and config.conf into +# $FULL_NODE_DIR. Exits when the clone or the build fails. cloneBuild() { local currentPwd=$PWD echo 'info: clone java-tron' - cloneCode + cloneCode || exit 1 echo 'info: build java-tron' - cd java-tron + cd java-tron || exit 1 sh gradlew clean build -x test if [[ $? == 0 ]];then - cd $currentPwd + cd "$currentPwd" || exit 1 mkdirFullNode - cp '../java-tron/build/libs/FullNode.jar' $PWD - cp '../java-tron/framework/src/main/resources/config.conf' $PWD + cp '../java-tron/build/libs/FullNode.jar' "$PWD" + cp '../java-tron/framework/src/main/resources/config.conf' "$PWD" else - exit + exit 1 fi } +# Prints the process ids of the "java ... -jar " lines in the ps output on stdin. +# The jar may be given with a directory, only its file name is compared. +matchNodePid() { + awk -v name="${JAR_NAME##*/}" '{ + for (i = 3; i <= NF; i++) { + if ($(i - 1) == "-jar" && ($i == name || substr($i, length($i) - length(name)) == "/" name)) { + print $1 + break + } + } + }' +} + +# Sets $pid to the process id recorded in .pid; empty when that process is gone. A +# directory that has a start.log but no pid file was used by an earlier version of this script, +# which kept no pid file; there every process on the machine that runs the jar name is taken, +# as that version did, so $pid may hold several ids. checkPid() { - if [[ $JAR_NAME =~ '/' ]]; then - JAR_NAME=$(echo $JAR_NAME |awk -F '/' '{print $NF}') + local pidFile="${JAR_NAME##*/}.pid" + local saved + pid='' + if [ -f "$pidFile" ]; then + # Only the recorded process counts, and only while it still runs the jar. Once it is found + # gone the file is emptied, so its id is not looked up again. + saved=$(cat "$pidFile" 2>/dev/null) + if [ -n "$saved" ]; then + pid=$(ps -o pid=,args= -p "$saved" 2>/dev/null | matchNodePid) + if [ -z "$pid" ]; then + : > "$pidFile" + fi + fi + elif [ -f start.log ]; then + pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid) fi - pid=$(ps -ef | grep -v start | grep $JAR_NAME | grep -v grep | awk '{print $2}') - return $pid } +# Stops the running node: sends SIGTERM once per second for up to MAX_STOP_TIME seconds, +# then SIGKILL. Returns 1 when no node was ever started from this directory. stopService() { + local pidFile="${JAR_NAME##*/}.pid" + # A node set up by --release or -cb runs in $FULL_NODE_DIR. Handle it from the parent + # directory as well, as long as no node was started from the parent itself. + if [ ! -f "$pidFile" ] && [ ! -f start.log ] && [ -f "$FULL_NODE_DIR/$pidFile" ]; then + cd "$FULL_NODE_DIR" || exit 1 + fi + if [ ! -f "$pidFile" ] && [ ! -f start.log ]; then + echo "info: no node was started from $PWD" + return 1 + fi count=1 while [ $count -le $MAX_STOP_TIME ]; do checkPid - if [ $pid ]; then + if [ -n "$pid" ]; then kill -15 $pid sleep 1 else @@ -278,6 +398,8 @@ stopService() { sleep 5 } +# Linux only: refuses to start with less than ALLOW_MIN_MEMORY MB of memory, or when -mem +# asks for less than that. checkAllowMemory() { os=`uname` totalMemory=$(`echo getTotalMemory`) @@ -288,15 +410,16 @@ checkAllowMemory() { if [[ $total -lt $ALLOW_MIN_MEMORY ]]; then echo "warn: the memory $total MB cannot be smaller than the minimum memory $ALLOW_MIN_MEMORY MB" - exit + exit 1 elif [[ $SPECIFY_MEMORY -gt 0 ]] && [[ $SPECIFY_MEMORY -lt $ALLOW_MIN_MEMORY ]]; then echo "warn: the specified memory $SPECIFY_MEMORY MB cannot be smaller than the minimum memory $ALLOW_MIN_MEMORY MB" echo 'warn: start abort' - exit + exit 1 fi } +# Linux only: preloads tcmalloc when it is installed at /usr/lib64/libtcmalloc.so. setTCMalloc() { os=`uname` if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then @@ -310,6 +433,7 @@ setTCMalloc() { fi } +# Prints the total memory of the machine in KB. getTotalMemory() { os=`uname` if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then @@ -317,143 +441,207 @@ getTotalMemory() { echo $total return elif [[ $os == 'Darwin' ]]; then - total=$(sysctl -a | grep mem |grep hw.memsize |awk -F ' ' '{print $2}') - echo `expr $total / 1024` + total=$(sysctl -n hw.memsize) + echo $((total / 1024)) fi } +# Linux only: sizes the heap to 60% and the direct memory to 10% of the total memory, or of +# the -mem value when given. macOS keeps JVM_MS / JVM_MX and uses 1g of direct memory. setJVMMemory() { os=`uname` if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then - if [[ $SPECIFY_MEMORY >0 ]]; then - max_direct=$(echo "$SPECIFY_MEMORY/1024*0.1" | bc | awk -F. '{print $1"g"}') - if [[ "$max_direct" != "g" ]]; then - MAX_DIRECT_MEMORY=$max_direct - fi - JVM_MX=$(echo "$SPECIFY_MEMORY/1024*0.6" | bc | awk -F. '{print $1"g"}') - JVM_MS=$JVM_MX + local total_gb + # The base is -mem when given, otherwise the total memory, in whole GB. + if [[ $SPECIFY_MEMORY -gt 0 ]]; then + total_gb=$((SPECIFY_MEMORY / 1024)) else - total=$(`echo getTotalMemory`) - MAX_DIRECT_MEMORY=$(echo "$total/1024/1024*0.1" | bc | awk -F. '{print $1"g"}') - JVM_MX=$(echo "$total/1024/1024*0.6" | bc | awk -F. '{print $1"g"}') + total_gb=$(($(getTotalMemory) / 1024 / 1024)) + fi + # Direct memory gets 10% and the heap 60%, with -Xms equal to -Xmx. A share that rounds + # down to 0 GB keeps its default. + if [[ $((total_gb / 10)) -gt 0 ]]; then + MAX_DIRECT_MEMORY="$((total_gb / 10))g" + fi + if [[ $((total_gb * 6 / 10)) -gt 0 ]]; then + JVM_MX="$((total_gb * 6 / 10))g" JVM_MS=$JVM_MX fi elif [[ $os == 'Darwin' ]]; then + # macOS keeps JVM_MS / JVM_MX and uses 1g of direct memory. MAX_DIRECT_MEMORY='1g' fi } +# Starts $JAR_NAME in the background with the JVM options for the detected Java version +# (CMS on JDK 8, ZGC on JDK 17), passing the FullNode options and the config file. Output goes +# to start.log, the process id to .pid. startService() { - echo $(date) >>start.log - if [[ ! $JAR_NAME =~ '-c' ]]; then - FULL_START_OPT="$FULL_START_OPT -c $DEFAULT_FULL_NODE_CONFIG" - fi - if [[ ! -f $JAR_NAME ]]; then echo "warn: jar file $JAR_NAME not exist" - exit + exit 1 + fi + echo $(date) >>start.log + + # ZGC with unified GC logging by default. JDK 8 has no ZGC, so it uses CMS with the JDK 8 + # GC log flags and NewRatio=2. + local gc_opts='-XX:+UseZGC -Xlog:gc,gc+heap:file=gc.log:time,tags,level:filecount=10,filesize=100M' + local tail_opts='' + if [[ $JAVA_SPEC_VERSION == '1.8' ]]; then + gc_opts='-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps -XX:+CMSParallelRemarkEnabled' + tail_opts='-XX:NewRatio=2' fi - nohup $JAVACMD -Xms$JVM_MS -Xmx$JVM_MX -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:./gc.log \ - -XX:+PrintGCDateStamps -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m -XX:+UseCodeCacheFlushing \ + # The node keeps many database and network files open. Raise the soft open file limit to + # 65535 when it is lower; a higher soft limit and the hard limit are kept. + local openFiles=$(ulimit -S -n) + if [[ $openFiles != unlimited && $openFiles -lt 65535 ]]; then + ulimit -S -n 65535 2>/dev/null || echo 'warn: failed to set ulimit -n 65535' + fi + # Run in the background, immune to hangups, appending all output to start.log. + nohup "$JAVACMD" -Xms$JVM_MS -Xmx$JVM_MX $gc_opts -XX:ReservedCodeCacheSize=256m -XX:+UseCodeCacheFlushing \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -XX:MaxDirectMemorySize=$MAX_DIRECT_MEMORY -Dio.netty.allocator.type=pooled \ -XX:+HeapDumpOnOutOfMemoryError \ - -XX:NewRatio=2 -jar \ - $JAR_NAME $FULL_START_OPT >>start.log 2>&1 & - checkPid + $tail_opts -jar \ + "$JAR_NAME" "${FULL_START_OPT[@]}" -c "$DEFAULT_FULL_NODE_CONFIG" >>start.log 2>&1 & + pid=$! + echo "$pid" > "${JAR_NAME##*/}.pid" + # A JVM that cannot start, for example on a bad option or a locked database, exits within + # moments. Report that instead of a successful start. + sleep 3 + if ! kill -0 "$pid" 2>/dev/null; then + echo "warn: java-tron exited right after the start, see start.log" + : > "${JAR_NAME##*/}.pid" + exit 1 + fi echo "info: start java-tron with pid $pid on $HOSTNAME" - echo "info: if you need to stop the service, execute: sh start.sh --stop" + echo "info: if you need to stop the service, execute in this directory: sh start.sh --stop" } +# Rewrites LevelDB manifests of at least REBUILD_MANIFEST_SIZE MB under $REBUILD_DIR before +# a start, so the databases open faster (TIP-298). Downloads and verifies ArchiveManifest.jar +# when it is missing. Skipped with -dr, on ARM64 (RocksDB only) and without a database. rebuildManifest() { if [[ $REBUILD_MANIFEST = false ]]; then echo 'info: disable rebuild manifest!' return fi + # The tool handles LevelDB only, and ARM64 runs RocksDB only. + if [[ -n $RELEASE_ARCH_SUFFIX ]]; then + echo 'info: ARM64 only supports RocksDB, skip rebuild manifest' + return + fi + if [[ ! -d $REBUILD_DIR ]]; then echo "info: database not exists, skip rebuild manifest" return fi ARCHIVE_JAR='ArchiveManifest.jar' - if [[ -f $ARCHIVE_JAR ]]; then - echo 'info: execute rebuild manifest.' - $JAVACMD -jar $ARCHIVE_JAR -d $REBUILD_DIR -m $REBUILD_MANIFEST_SIZE -b $REBUILD_BATCH_SIZE - else + # Download and verify the tool on first use. If that fails, the rebuild is skipped and the + # start goes on. + if [[ ! -f $ARCHIVE_JAR ]]; then echo 'info: download the rebuild manifest plugin from the github' - local latest=$(`echo getLatestReleaseVersion`) - download $RELEASE_URL/download/GreatVoyage-v"$latest"/$ARCHIVE_JAR $ARCHIVE_JAR - if [[ $download == 0 ]]; then - echo 'info: download success, rebuild manifest' - $JAVACMD -jar $ARCHIVE_JAR $REBUILD_DIR -m $REBUILD_MANIFEST_SIZE -b $REBUILD_BATCH_SIZE + if ! downloadRelease "$(getLatestReleaseVersion)" $RELEASE_ARCHIVE_JAR $ARCHIVE_JAR; then + echo 'warn: skip rebuild manifest' + return fi fi - if [[ $? == 0 ]]; then + echo 'info: execute rebuild manifest.' + # A failed rebuild does not block the start either. The tool writes its log to logs/toolkit.log. + if "$JAVACMD" -jar "$ARCHIVE_JAR" -d "$REBUILD_DIR" -m "$REBUILD_MANIFEST_SIZE" -b "$REBUILD_BATCH_SIZE"; then echo 'info: rebuild manifest success' else - echo 'info: rebuild manifest fail, log in logs/archive.log' + echo 'info: rebuild manifest fail, log in logs/toolkit.log' fi } +# --net: selects the test (Nile) or private network config under $FULL_NODE_CONFIG_DIR, +# downloading it when the file does not exist yet. Exits on an unknown network or a failed +# download. specifyConfig(){ echo "info: specify the net: $1" local netType=$1 local configName; + local configUrl; if [[ "$netType" = 'test' ]]; then + # Nile testnet config from the nile-testnet repository. configName=$FULL_NODE_CONFIG_TEST_NET + configUrl=$TEST_NET_CONFIG_URL elif [[ "$netType" = 'private' ]]; then + # Private network config from the tron-deployment repository. configName=$FULL_NODE_CONFIG_PRIVATE_NET + configUrl=https://raw.githubusercontent.com/tronprotocol/tron-deployment/$GITHUB_BRANCH/$configName else - echo "warn: no support config $nodeType" - exit + echo "warn: no support config $netType" + exit 1 fi if [[ ! -d $FULL_NODE_CONFIG_DIR ]]; then - mkdir -p $FULL_NODE_CONFIG_DIR - fi - - if [[ -d $FULL_NODE_CONFIG_DIR/$configName ]]; then - DEFAULT_FULL_NODE_CONFIG=$FULL_NODE_CONFIG_DIR/$configName - break + mkdir -p "$FULL_NODE_CONFIG_DIR" fi + # Download only when the file is missing. An existing file is used as is. if [[ ! -f $FULL_NODE_CONFIG_DIR/$configName ]]; then - download https://raw.githubusercontent.com/tronprotocol/tron-deployment/$GITHUB_BRANCH/$configName $configName - mv $configName $FULL_NODE_CONFIG_DIR/$configName - DEFAULT_FULL_NODE_CONFIG=$FULL_NODE_CONFIG_DIR/$configName + download "$configUrl" "$FULL_NODE_CONFIG_DIR/$configName" || exit 1 fi + # The selected config is passed to FullNode when it is started. The path is absolute, so it + # stays valid after --release or -cb change into $FULL_NODE_DIR. + DEFAULT_FULL_NODE_CONFIG=$PWD/$FULL_NODE_CONFIG_DIR/$configName } +# Verifies file $3 against the signature of asset $2 in release $1, which must be made +# by the release key. Removes $3 if the signature is missing or invalid. checkSign() { echo 'info: verify signature' - local latest_version=$(`echo getLatestReleaseVersion`) - download $RELEASE_URL/download/$latest_version/sha256sum.txt sha256sum.txt - fullNodeSha256=$(cat sha256sum.txt|grep 'FullNode'| awk -F ' ' '{print $1}') - - os=`uname` - if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then - releaseFullNodeSha256=$(sha256sum FullNode.jar| grep FullNode | awk -F ' ' '{print $1}') - elif [[ $os == 'Darwin' ]]; then - releaseFullNodeSha256=$(shasum -a 256 FullNode.jar| grep FullNode | awk -F ' ' '{print $1}') - cat $releaseFullNodeSha256 | awk -F ' ' '{print $0}' - fi - - echo "info: release sha256sum sign: $releaseFullNodeSha256" - echo "info: FullNode.jar sha256sum sign: $fullNodeSha256" - - if [[ "$fullNodeSha256" == "$releaseFullNodeSha256" ]]; then - echo 'info: sha256 signatures pass' + local version=$1 + local asset=$2 + local file=$3 + local gnupg_home + local server + local verified=false + if type gpg >/dev/null 2>&1; then + # Use a temporary keyring, so that the user's keyring is neither read nor changed. + gnupg_home=$(mktemp -d) + # Fetch the release key from the first key server that answers. + for server in $RELEASE_KEY_SERVERS; do + gpg --homedir "$gnupg_home" --batch --quiet --keyserver "$server" \ + --recv-keys "$RELEASE_KEY_FINGERPRINT" >/dev/null 2>&1 && break + done + # Download the detached signature. Only a VALIDSIG status line that carries the release + # key fingerprint counts as verified. + if download "$RELEASE_URL/download/$version/$asset.sig" "$file.sig" \ + && gpg --homedir "$gnupg_home" --batch --status-fd 1 --verify "$file.sig" "$file" 2>/dev/null \ + | grep '^\[GNUPG:\] VALIDSIG ' | grep -q -w "$RELEASE_KEY_FINGERPRINT"; then + verified=true + fi + # Stop the gpg daemons of the temporary keyring, then remove the keyring and the signature. + gpgconf --homedir "$gnupg_home" --kill all >/dev/null 2>&1 + rm -rf "$gnupg_home" "$file.sig" else - echo 'info: sha256 signature exception!!!' - echo 'info: please compile from the code or download the latest version from https://github.com/tronprotocol/java-tron' + # Without gpg the file cannot be verified and is removed below. + echo 'warn: gpg is required to verify the release signature' fi + # Anything other than a verified signature removes the file. + if [[ $verified == true ]]; then + echo 'info: signature verified' + return 0 + fi + echo "warn: signature verification failed, remove $file" + rm -f "$file" + return 1 } +# Stops a running node and starts it again with the current settings. restart() { + checkJava stopService + if [[ $IS_BACKUP_GC_LOG = true ]]; then + backupGCLog + fi checkAllowMemory rebuildManifest setTCMalloc @@ -461,46 +649,61 @@ restart() { startService } +# Exits when option $1 was given without a value. +needValue() { + if [ -z "$2" ]; then + echo "error: option $1 needs a value" >&2 + exit 1 + fi +} + +# Command line options. Anything else is passed to FullNode as is, except a single word given +# as the only argument, which names the jar to start. Everything after -- goes to FullNode +# unchanged. while [ -n "$1" ]; do case "$1" in + --) + shift 1 + FULL_START_OPT+=("$@") + break + ;; -c) + needValue "$1" "$2" DEFAULT_FULL_NODE_CONFIG=$2 shift 2 ;; -d) + needValue "$1" "$2" REBUILD_DIR=$2/database - FULL_START_OPT="$FULL_START_OPT $1 $2" + FULL_START_OPT+=("$1" "$2") shift 2 ;; - -j) + -j|-n) + needValue "$1" "$2" JAR_NAME=$2 shift 2 ;; -p) - FULL_START_OPT="$FULL_START_OPT $1 $2" + needValue "$1" "$2" + FULL_START_OPT+=("$1" "$2") shift 2 ;; - -w) - FULL_START_OPT="$FULL_START_OPT $1" - shift 1 - ;; - --witness) - FULL_START_OPT="$FULL_START_OPT $1" + -w|--witness) + FULL_START_OPT+=("$1") shift 1 ;; --net) - specifyConfig $2 + needValue "$1" "$2" + specifyConfig "$2" shift 2 ;; -m) + needValue "$1" "$2" REBUILD_MANIFEST_SIZE=$2 shift 2 ;; - -n) - JAR_NAME=$2 - shift 2 - ;; -b) + needValue "$1" "$2" REBUILD_BATCH_SIZE=$2 shift 2 ;; @@ -512,11 +715,7 @@ while [ -n "$1" ]; do DOWNLOAD=true shift 1 ;; - --deploy) - QUICK_START=true - shift 1 - ;; - --release) + --deploy|--release) QUICK_START=true shift 1 ;; @@ -525,14 +724,12 @@ while [ -n "$1" ]; do exit ;; -mem) + needValue "$1" "$2" SPECIFY_MEMORY=$2 shift 2 ;; - --disable-rewrite-manifes) - REBUILD_MANIFEST=false - shift 1 - ;; - -dr) + # --disable-rewrite-manifes is the spelling of earlier versions and stays accepted. + --disable-rewrite-manifest|--disable-rewrite-manifes|-dr) REBUILD_MANIFEST=false shift 1 ;; @@ -541,25 +738,17 @@ while [ -n "$1" ]; do shift 1 ;; --run) - if [[ $ALL_OPT_LENGTH -eq 1 ]]; then - restart - fi - RUN=true shift 1 ;; - --stop) + --stop|-s) stopService + exit $? ;; - FullNode) - RUN=true - shift 1 - ;; - FullNode.jar) - RUN=true + FullNode|FullNode.jar) shift 1 ;; *.jar) - RUN=true + JAR_NAME=$1 shift 1 ;; *) @@ -574,15 +763,17 @@ while [ -n "$1" ]; do exit fi fi - FULL_START_OPT="$FULL_START_OPT $@" - break + # A FullNode option, passed on as is. Parsing goes on, so script options may follow it. + FULL_START_OPT+=("$1") + shift 1 ;; esac done -if [[ $IS_BACKUP_GC_LOG = true ]]; then - backupGCLog -fi +# Main flow: optional clone / download steps, then one start of the node. Everything from here +# on runs java or picks release assets by the architecture that java reports, so a broken JDK +# is reported first. +checkJava if [[ $CLONE_BUILD == true ]];then cloneBuild @@ -590,13 +781,6 @@ fi if [[ $QUICK_START == true ]]; then quickStart - if [[ $? == 0 ]] ; then - if [[ $RUN == true ]]; then - cd $FULL_NODE_DIR - FULL_START_OPT='' - restart - fi - fi fi if [[ $UPGRADE == true ]]; then @@ -604,20 +788,15 @@ if [[ $UPGRADE == true ]]; then fi if [[ $DOWNLOAD == true ]]; then - latest=$(`echo getLatestReleaseVersion`) + latest=$(getLatestReleaseVersion) if [[ -n $latest ]]; then - download $RELEASE_URL/download/$latest/$JAR_NAME $latest + downloadRelease "$latest" "$RELEASE_JAR" "$JAR_NAME.download" && mv "$JAR_NAME.download" "$JAR_NAME" exit else echo 'info: not getting the latest version' + exit 1 fi fi -if [[ $ALL_OPT_LENGTH -eq 0 || $ALL_OPT_LENGTH -gt 0 ]]; then - restart -fi - -if [[ $RUN == true ]]; then - restart -fi +restart diff --git a/start.sh.simple b/start.sh.simple deleted file mode 100644 index 109f0dc85a3..00000000000 --- a/start.sh.simple +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash -############################################################################# -# -# GNU LESSER GENERAL PUBLIC LICENSE -# Version 3, 29 June 2007 -# -# Copyright (C) [2007] [TRON Foundation], Inc. -# Everyone is permitted to copy and distribute verbatim copies -# of this license document, but changing it is not allowed. -# -# -# This version of the GNU Lesser General Public License incorporates -# the terms and conditions of version 3 of the GNU General Public -# License, supplemented by the additional permissions listed below. -# -# You can find java-tron at https://github.com/tronprotocol/java-tron/ -# -############################################################################## -# TRON Full Node Management Simple Script -# -# NOTE: This is a simple and concise script to start and stop the java-tron full node, -# designed for developers to quickly get started and learn. -# It may not be suitable for production environments. -# -# Usage: -# sh start.sh # Start the java-tron FullNode -# sh start.sh -s # Stop the java-tron FullNode -# sh start.sh [options] # Start with additional java-tron options,such as: -c config.conf -d /path_to_data, etc. -# -############################################################################## - - -# adjust JVM start -# Set the maximum heap size to 9G, adjust as needed -VM_XMX="9G" -# adjust JVM end - -FULL_NODE_JAR="FullNode.jar" -FULL_START_OPT=() -PID="" -MAX_STOP_TIME=60 -JAVACMD="" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -log() { - local level="$1"; shift - local timestamp color="" - timestamp=$(date '+%Y-%m-%d %H:%M:%S') - case "$level" in - INFO) color="$GREEN" ;; - WARN) color="$YELLOW" ;; - ERROR) color="$RED" ;; - esac - printf "%b[%s] [%s]:%b %s\n" "$color" "$timestamp" "$level" "$NC" "$*" | tee -a "${SCRIPT_DIR}/start.log" -} - -info() { log INFO "$@"; } -warn() { log WARN "$@"; } -error() { log ERROR "$@"; } -die() { error "$@"; exit 1; } - -ulimit -n 65535 || warn "Failed to set ulimit -n 65535" - -findJava() { - if [ -n "${JAVA_HOME:-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - [ -x "$JAVACMD" ] || die "JAVA_HOME is invalid: $JAVA_HOME" - else - JAVACMD="java" - which java >/dev/null 2>&1 || die "JAVA_HOME not set and no 'java' in PATH" - fi - "$JAVACMD" -version > /dev/null 2>&1 || die "Java command not working" -} - -checkPid() { - # shellcheck disable=SC2009 - PID=$(ps -ef |grep $FULL_NODE_JAR |grep -v grep |awk '{print $2}') -} - - -stopService() { - checkPid - - if ! kill -0 "$PID" 2>/dev/null; then - info "java-tron is not running." - return 0 - fi - info "Stopping java-tron service (PID: $PID)" - - local count=1 - - while [ -n "$PID" ] && [ $count -le $MAX_STOP_TIME ]; do - kill -TERM "$PID" 2>/dev/null && info "Sent SIGTERM to java-tron (PID: $PID), attempt $count" - sleep 1 - checkPid - count=$((count + 1)) - done - - if [ -n "$PID" ]; then - warn "Forcing kill java-tron (PID: $PID) after $MAX_STOP_TIME seconds" - kill -KILL "$PID" 2>/dev/null - sleep 1 - checkPid - fi - - if [ -n "$PID" ]; then - die "Failed to stop the service (PID: $PID)" - else - info "java-tron stopped" - wait_with_info 2 "Cleaning up..." - fi -} - -startService() { - if [ -n "${FULL_START_OPT[*]}" ]; then - info "Starting java-tron service with options: ${FULL_START_OPT[*]}" - fi - if [ ! -f "$FULL_NODE_JAR" ]; then - die "$FULL_NODE_JAR not found in path $SCRIPT_DIR." - fi - - nohup "$JAVACMD" \ - -Xmx"$VM_XMX" \ - -XX:+UseZGC \ - -Xlog:gc,gc+heap:file=gc.log:time,tags,level:filecount=10,filesize=100M \ - -XX:ReservedCodeCacheSize=256m \ - -XX:+UseCodeCacheFlushing \ - -XX:MetaspaceSize=256m \ - -XX:MaxMetaspaceSize=512m \ - -XX:MaxDirectMemorySize=1g \ - -Dio.netty.allocator.type=pooled \ - -XX:+HeapDumpOnOutOfMemoryError \ - -jar "$FULL_NODE_JAR" "${FULL_START_OPT[@]}" \ - >> start.log 2>&1 & - - - info "Waiting for the service to start..." - wait_with_info 5 "Starting..." - - checkPid - - if [ -n "$PID" ]; then - info "Started java-tron with PID $PID on $HOSTNAME." - else - die "Failed to start java-tron, see start.log or logs/tron.log for details." - fi -} - -wait_with_info() { - local seconds=$1 - local message=$2 - for i in $(seq "$seconds" -1 1); do - info "$message wait ($i) s" - sleep 1 - done -} - - -start() { - checkPid - if [ -n "$PID" ]; then - info "java-tron is already running (PID: $PID), to stop the service: sh start.sh -s" - return - fi - findJava - startService -} - -while [ -n "$1" ]; do - case "$1" in - -s) - stopService - exit 0 - ;; - *) - FULL_START_OPT+=("$@") - break - ;; - esac -done - -start - -exit 0 \ No newline at end of file