From 4a7acdcc287ba9b1ba12ba6d5d8558ebce8092c9 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 23 Sep 2026 18:51:34 +0800 Subject: [PATCH 01/11] refactor(db): remove deprecated KhaosDatabase.getBranch overload The BlockId overload had no callers. Every caller uses getBranch(Sha256Hash, Sha256Hash), which rejects a branch whose parent is missing. --- .../java/org/tron/core/db/KhaosDatabase.java | 31 ------------------- 1 file changed, 31 deletions(-) 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)) From e56b0d3e524b8ef1a3eeafff4798a8815c280912 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 23 Sep 2026 18:51:34 +0800 Subject: [PATCH 02/11] style(db): remove unused import from Manager --- framework/src/main/java/org/tron/core/db/Manager.java | 1 - 1 file changed, 1 deletion(-) 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; From 7b8ca8889be5e44fea9ee6dc547244d03c65e97b Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 23 Sep 2026 18:51:34 +0800 Subject: [PATCH 03/11] fix: disable download in start.sh download() now prints a notice and returns non-zero; release files must be fetched and verified manually. It returns instead of exiting so that restart() still completes when rebuildManifest reaches it. --- start.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/start.sh b/start.sh index 1472a94dc62..ac11f6c9508 100644 --- a/start.sh +++ b/start.sh @@ -180,6 +180,8 @@ upgrade() { } download() { + echo "warn: download is disabled, fetch $1 manually and verify it" + return 1 local url=$1 local file_name=$2 if type wget >/dev/null 2>&1; then From bb50d9ea80652932623d2f64b2068b3ee1a20578 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 23 Sep 2026 18:51:34 +0800 Subject: [PATCH 04/11] fix(http): time out idle connections at the connection limit Connections that send nothing could hold every maxHttpConnectNumber slot until the connector's 30-second idle timeout and lock other clients out. When the limit is reached, the open connections now time out after 10 seconds of inactivity. Connections still being accepted at that moment keep the connector's default idle timeout. --- .../tron/common/application/HttpService.java | 8 +- .../common/jetty/ConnectionLimitTest.java | 148 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 framework/src/test/java/org/tron/common/jetty/ConnectionLimitTest.java 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/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 + } + } +} From 7dd470c42bd50f763aec1723b94ce7a5b6608671 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 29 Sep 2026 11:09:59 +0800 Subject: [PATCH 05/11] fix args init, should throw TronError --- framework/src/main/java/org/tron/core/config/args/Args.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..999de25ffb6 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,14 @@ 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())) { From 5dd60da7032f82ff2b03e5276f9b2f9ac6b837cd Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 29 Sep 2026 11:30:15 +0800 Subject: [PATCH 06/11] style(config): wrap long lines in Args --- framework/src/main/java/org/tron/core/config/args/Args.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 999de25ffb6..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) { - throw new TronError("Check node.dns.changeThreshold, should be bigger than 0, default 0.1", TronError.ErrCode.PARAMETER_INIT); + 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) { - throw new TronError("Check node.dns.maxMergeSize, should be [1~5], default 5", TronError.ErrCode.PARAMETER_INIT); + throw new TronError("Check node.dns.maxMergeSize, should be [1~5], default 5", + TronError.ErrCode.PARAMETER_INIT); } if (StringUtils.isNotEmpty(dns.getDnsPrivate())) { From 6025d6107826b23cea3f51a87d54adaa96d1e8de Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 29 Sep 2026 12:31:12 +0800 Subject: [PATCH 07/11] fix: verify start.sh downloads, fix its bugs and support ARM64 Downloads: - TLS certificates are verified and a failed request leaves no file. - Release jars must carry a valid GPG signature from the release key listed under "Integrity Check" in README.md. A jar that fails is not used: --upgrade keeps the old jar and --download the existing one. - The mainnet config comes from java-tron and the Nile config from nile-testnet. Bugs: - --stop looped forever, the node was restarted two or three times per run, and -c was passed again on every start. - A jar argument such as my.jar was ignored, and -j with a path lost the path before starting. - --net ignored an existing config file; --net and --release kept going after a failed download. - Linux heap sizing needed bc, and the macOS memory check failed with an expr error. - Manifest rebuilding used a wrong URL and never ran the plugin. It now runs and, by default, rewrites manifests of 128 MB or more. - The latest release was picked in name order, clone failures were ignored, $darwin was never set, and failures exited with status 0. Platforms: - start.sh reads the Java version and architecture of its JVM. JDK 8 keeps the CMS options and JDK 17 uses the ZGC options from start.sh.simple, which is removed. ARM64 downloads the aarch64 jars and skips manifest rebuilding. - -s stops the node and the open file limit is raised to 65535, as in start.sh.simple. The default JVM_MX, used on macOS, is 12g. README and shell.md describe the single script. --- README.md | 3 +- shell.md | 20 +++- start.sh | 278 ++++++++++++++++++++++++------------------------ start.sh.simple | 193 --------------------------------- 4 files changed, 158 insertions(+), 336 deletions(-) delete mode 100644 start.sh.simple 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/shell.md b/shell.md index 700067a9aa3..f1a66fb7c44 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 @@ -58,7 +62,7 @@ The script is available in the java-tron project at [github](https://github.com/ start the service -* `--stop` +* `--stop` or `-s` stop the service @@ -79,7 +83,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 +95,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 +112,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` @@ -219,7 +231,7 @@ Following file structure will be created: ### 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 ac11f6c9508..a7f3517e355 100644 --- a/start.sh +++ b/start.sh @@ -21,7 +21,6 @@ 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' @@ -42,25 +41,29 @@ 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=12g IS_BACKUP_GC_LOG=true SPECIFY_MEMORY=0 -RUN=false UPGRADE=false # Rebuild manifest REBUILD_MANIFEST=true REBUILD_DIR="$PWD/output-directory/database" -REBUILD_MANIFEST_SIZE=0 +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,6 +73,11 @@ else GITHUB_REPOSITORY=$GITHUB_REPOSITORY_SSH_URL fi +darwin=false +case "`uname`" in + Darwin*) darwin=true ;; +esac + # Determine the Java command to use to start the JVM. if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" @@ -114,6 +122,20 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi +JAVA_PROPERTIES=$("$JAVACMD" -XshowSettings:properties -version 2>&1) +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=$(javaProperty java.specification.version) +case "$(javaProperty os.arch)" in + aarch64|arm64) RELEASE_ARCH_SUFFIX='-aarch64' ;; + *) RELEASE_ARCH_SUFFIX='' ;; +esac +RELEASE_JAR="FullNode$RELEASE_ARCH_SUFFIX.jar" +RELEASE_ARCHIVE_JAR="ArchiveManifest$RELEASE_ARCH_SUFFIX.jar" + backupGCLog() { local maxFile=5 local gcLogDir=logs/gc_logs/ @@ -142,37 +164,24 @@ backupGCLog() { } 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 + 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() { - 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 + if downloadRelease $latest_version $RELEASE_JAR $JAR_NAME.download; then + 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 + echo "warn: upgrade aborted, $JAR_NAME is unchanged" + exit 1 fi else echo 'info: nothing to upgrade' @@ -180,20 +189,24 @@ upgrade() { } download() { - echo "warn: download is disabled, fetch $1 manually and verify it" - return 1 local url=$1 local file_name=$2 if type wget >/dev/null 2>&1; then - wget --no-check-certificate -q $url + 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 + 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" +} + mkdirFullNode() { if [ ! -d $FULL_NODE_DIR ]; then echo "info: create $FULL_NODE_DIR" @@ -206,41 +219,42 @@ mkdirFullNode() { } quickStart() { - full_node_version=$(`echo getLatestReleaseVersion`) + full_node_version=$(getLatestReleaseVersion) if [[ -n $full_node_version ]]; then 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 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 || exit 1 else echo 'info: not getting the latest version' - exit + exit 1 fi } cloneCode() { if type git >/dev/null 2>&1; then - git_clone=$(git clone -b $GITHUB_BRANCH $GITHUB_REPOSITORY) - if [[ git_clone == 0 ]]; then + 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 } 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 @@ -253,18 +267,14 @@ cloneBuild() { } checkPid() { - if [[ $JAR_NAME =~ '/' ]]; then - JAR_NAME=$(echo $JAR_NAME |awk -F '/' '{print $NF}') - fi - pid=$(ps -ef | grep -v start | grep $JAR_NAME | grep -v grep | awk '{print $2}') - return $pid + pid=$(ps -ef | grep -v start | grep "${JAR_NAME##*/}" | grep -v grep | awk '{print $2}') } stopService() { count=1 while [ $count -le $MAX_STOP_TIME ]; do checkPid - if [ $pid ]; then + if [ -n "$pid" ]; then kill -15 $pid sleep 1 else @@ -290,12 +300,12 @@ 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 } @@ -319,25 +329,25 @@ 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 } 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 + 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 + 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 @@ -348,22 +358,25 @@ setJVMMemory() { 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 - nohup $JAVACMD -Xms$JVM_MS -Xmx$JVM_MX -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:./gc.log \ - -XX:+PrintGCDateStamps -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m -XX:+UseCodeCacheFlushing \ + 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 + + ulimit -n 65535 2>/dev/null || echo 'warn: failed to set ulimit -n 65535' + 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 & + $tail_opts -jar \ + $JAR_NAME $FULL_START_OPT -c $DEFAULT_FULL_NODE_CONFIG >>start.log 2>&1 & checkPid echo "info: start java-tron with pid $pid on $HOSTNAME" echo "info: if you need to stop the service, execute: sh start.sh --stop" @@ -375,25 +388,26 @@ rebuildManifest() { return fi + 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 + 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.' + 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' @@ -404,54 +418,61 @@ specifyConfig(){ echo "info: specify the net: $1" local netType=$1 local configName; + local configUrl; if [[ "$netType" = 'test' ]]; then configName=$FULL_NODE_CONFIG_TEST_NET + configUrl=$TEST_NET_CONFIG_URL elif [[ "$netType" = 'private' ]]; then 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 - fi - 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 + DEFAULT_FULL_NODE_CONFIG=$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 + gnupg_home=$(mktemp -d) + 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 + 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 + 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' + echo 'warn: gpg is required to verify the release signature' + fi + if [[ $verified == true ]]; then + echo 'info: signature verified' + return 0 fi + echo "warn: signature verification failed, remove $file" + rm -f "$file" + return 1 } restart() { @@ -543,25 +564,20 @@ 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 0 ;; FullNode) - RUN=true shift 1 ;; FullNode.jar) - RUN=true shift 1 ;; *.jar) - RUN=true + JAR_NAME=$1 shift 1 ;; *) @@ -592,13 +608,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 @@ -606,20 +615,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 From 57da1fd0f956489b2d00ead1d0c64c6ee1e73b1d Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 29 Sep 2026 12:31:12 +0800 Subject: [PATCH 08/11] fix: verify start.sh downloads, fix its bugs and support ARM64 Downloads: - TLS certificates are verified and a failed request leaves no file. - Release jars must carry a valid GPG signature from the release key listed under "Integrity Check" in README.md. A jar that fails is not used: --upgrade keeps the old jar and --download the existing one. - The mainnet config comes from java-tron and the Nile config from nile-testnet. Bugs: - --stop looped forever, the node was restarted two or three times per run, and -c was passed again on every start. - A jar argument such as my.jar was ignored, and -j with a path lost the path before starting. - --net ignored an existing config file; --net and --release kept going after a failed download. - Linux heap sizing needed bc, and the macOS memory check failed with an expr error. - On macOS without JAVA_HOME the script derived JAVA_HOME=/usr from the /usr/bin/javac stub, and /usr/bin/java then ran itself forever; the JDK now comes from /usr/libexec/java_home. - Manifest rebuilding used a wrong URL and never ran the plugin. It now runs and, by default, rewrites manifests of 128 MB or more. - The latest release was picked in name order, clone failures were ignored, $darwin was never set, and failures exited with status 0. Platforms: - start.sh reads the Java version and architecture of its JVM. JDK 8 keeps the CMS options and JDK 17 uses the ZGC options from start.sh.simple, which is removed. ARM64 downloads the aarch64 jars and skips manifest rebuilding. - -s stops the node and the open file limit is raised to 65535, as in start.sh.simple. The default JVM_MX, used on macOS, is 12g. README and shell.md describe the single script. --- README.md | 3 +- shell.md | 20 ++- start.sh | 342 +++++++++++++++++++++++++++--------------------- start.sh.simple | 193 --------------------------- 4 files changed, 209 insertions(+), 349 deletions(-) delete mode 100644 start.sh.simple 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/shell.md b/shell.md index 700067a9aa3..f1a66fb7c44 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 @@ -58,7 +62,7 @@ The script is available in the java-tron project at [github](https://github.com/ start the service -* `--stop` +* `--stop` or `-s` stop the service @@ -79,7 +83,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 +95,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 +112,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` @@ -219,7 +231,7 @@ Following file structure will be created: ### 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 ac11f6c9508..35bc84184d3 100644 --- a/start.sh +++ b/start.sh @@ -21,7 +21,6 @@ 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' @@ -42,25 +41,29 @@ 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=12g IS_BACKUP_GC_LOG=true SPECIFY_MEMORY=0 -RUN=false UPGRADE=false # Rebuild manifest REBUILD_MANIFEST=true REBUILD_DIR="$PWD/output-directory/database" -REBUILD_MANIFEST_SIZE=0 +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,25 +73,33 @@ else GITHUB_REPOSITORY=$GITHUB_REPOSITORY_SSH_URL fi +darwin=false +case "`uname`" in + Darwin*) darwin=true ;; +esac + # Determine the Java command to use to start the JVM. 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 + if [ -n "$JAVA_HOME" ]; then + export JAVA_HOME + fi fi if [ -z "$JAVACMD" ] ; then @@ -114,6 +125,22 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi +JAVA_PROPERTIES=$("$JAVACMD" -XshowSettings:properties -version 2>&1) +# 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=$(javaProperty java.specification.version) +case "$(javaProperty os.arch)" in + aarch64|arm64) RELEASE_ARCH_SUFFIX='-aarch64' ;; + *) RELEASE_ARCH_SUFFIX='' ;; +esac +RELEASE_JAR="FullNode$RELEASE_ARCH_SUFFIX.jar" +RELEASE_ARCHIVE_JAR="ArchiveManifest$RELEASE_ARCH_SUFFIX.jar" + +# Archives the previous gc.log into logs/gc_logs/ before a start and keeps the newest 5 archives. backupGCLog() { local maxFile=5 local gcLogDir=logs/gc_logs/ @@ -141,59 +168,57 @@ backupGCLog() { 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 + 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 + if downloadRelease $latest_version $RELEASE_JAR $JAR_NAME.download; then + 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 + echo "warn: upgrade aborted, $JAR_NAME is unchanged" + exit 1 fi else 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() { - echo "warn: download is disabled, fetch $1 manually and verify it" - return 1 local url=$1 local file_name=$2 if type wget >/dev/null 2>&1; then - wget --no-check-certificate -q $url + 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 + 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 echo "info: create $FULL_NODE_DIR" @@ -205,42 +230,48 @@ mkdirFullNode() { fi } +# --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 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 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 || exit 1 else 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 + 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 @@ -252,19 +283,18 @@ cloneBuild() { fi } +# Sets $pid to the process ids of the running $JAR_NAME; empty when it is not running. checkPid() { - if [[ $JAR_NAME =~ '/' ]]; then - JAR_NAME=$(echo $JAR_NAME |awk -F '/' '{print $NF}') - fi - pid=$(ps -ef | grep -v start | grep $JAR_NAME | grep -v grep | awk '{print $2}') - return $pid + pid=$(ps -ef | grep -v start | grep "${JAR_NAME##*/}" | grep -v grep | awk '{print $2}') } +# Stops the running node: sends SIGTERM once per second for up to MAX_STOP_TIME seconds, +# then SIGKILL. stopService() { count=1 while [ $count -le $MAX_STOP_TIME ]; do checkPid - if [ $pid ]; then + if [ -n "$pid" ]; then kill -15 $pid sleep 1 else @@ -280,6 +310,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`) @@ -290,15 +322,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 @@ -312,6 +345,7 @@ setTCMalloc() { fi } +# Prints the total memory of the machine in KB. getTotalMemory() { os=`uname` if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then @@ -319,25 +353,27 @@ 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 + 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 + 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 @@ -346,114 +382,135 @@ setJVMMemory() { 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 $FULL_START_OPT and the config file. Output goes +# to start.log. 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 + + 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 \ + ulimit -n 65535 2>/dev/null || echo 'warn: failed to set ulimit -n 65535' + 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 & + $tail_opts -jar \ + $JAR_NAME $FULL_START_OPT -c $DEFAULT_FULL_NODE_CONFIG >>start.log 2>&1 & checkPid echo "info: start java-tron with pid $pid on $HOSTNAME" echo "info: if you need to stop the service, execute: 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 + 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 + 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.' + 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' 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 configName=$FULL_NODE_CONFIG_TEST_NET + configUrl=$TEST_NET_CONFIG_URL elif [[ "$netType" = 'private' ]]; then 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 - fi - 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 + DEFAULT_FULL_NODE_CONFIG=$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 + gnupg_home=$(mktemp -d) + 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 + 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 + 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' + echo 'warn: gpg is required to verify the release signature' fi + 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() { stopService checkAllowMemory @@ -463,6 +520,7 @@ restart() { startService } +# Command line options. Anything not listed here is passed to FullNode as is. while [ -n "$1" ]; do case "$1" in -c) @@ -543,25 +601,20 @@ 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 0 ;; FullNode) - RUN=true shift 1 ;; FullNode.jar) - RUN=true shift 1 ;; *.jar) - RUN=true + JAR_NAME=$1 shift 1 ;; *) @@ -582,6 +635,7 @@ while [ -n "$1" ]; do esac done +# Main flow: optional clone / download steps, then one start of the node. if [[ $IS_BACKUP_GC_LOG = true ]]; then backupGCLog fi @@ -592,13 +646,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 @@ -606,20 +653,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 From a1b0c836cc433507454255350f30909456a7c205 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 29 Sep 2026 14:46:17 +0800 Subject: [PATCH 09/11] test(config): cover invalid DNS publish parameters in ArgsTest Invalid node.dns.changeThreshold and node.dns.maxMergeSize now fail startup with TronError(PARAMETER_INIT); add tests for both. --- .../org/tron/core/config/args/ArgsTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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<>(); From e68a2e5fc3a724e7d22cc630dba6a7516e34edc4 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 30 Sep 2026 18:27:32 +0800 Subject: [PATCH 10/11] chore(config): comment start.sh and lower the default max heap to 9g Adds comments to the Java detection, the release download and signature check, the memory sizing, the manifest rebuild and the option handling of start.sh, describing what each branch does. JVM_MX drops from 12g to 9g; Linux derives the heap from the total memory, so the default applies to macOS only. --- start.sh | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/start.sh b/start.sh index 35bc84184d3..0f851d3fb02 100644 --- a/start.sh +++ b/start.sh @@ -44,7 +44,7 @@ ALLOW_MIN_MEMORY=8192 # JVM option, adjust as needed MAX_DIRECT_MEMORY=1g JVM_MS=4g -JVM_MX=12g +JVM_MX=9g IS_BACKUP_GC_LOG=true SPECIFY_MEMORY=0 @@ -73,12 +73,14 @@ 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 if $darwin ; then # /usr/bin/java and /usr/bin/javac are stubs that run the JDK selected by @@ -97,11 +99,14 @@ if [ -z "$JAVA_HOME" ]; then fi 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 @@ -125,6 +130,7 @@ 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_PROPERTIES=$("$JAVACMD" -XshowSettings:properties -version 2>&1) # Prints one JVM system property from the -XshowSettings output above. javaProperty() { @@ -132,11 +138,14 @@ javaProperty() { } # 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" @@ -171,6 +180,7 @@ backupGCLog() { # Prints the newest GreatVoyage release tag of the java-tron repository; prints nothing when # the tags cannot be listed. getLatestReleaseVersion() { + # 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 } @@ -181,7 +191,10 @@ upgrade() { latest_version=$(getLatestReleaseVersion) echo "info: latest version: $latest_version" if [[ -n $latest_version ]]; 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' @@ -189,10 +202,12 @@ upgrade() { 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 } @@ -203,8 +218,10 @@ download() { local url=$1 local file_name=$2 if type wget >/dev/null 2>&1; then + # 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 + # -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 @@ -235,6 +252,8 @@ mkdirFullNode() { quickStart() { 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' @@ -243,6 +262,7 @@ quickStart() { echo "info: download $full_node_version" downloadRelease $full_node_version $RELEASE_JAR $JAR_NAME || exit 1 else + # Without a release tag there is nothing to download. echo 'info: not getting the latest version' exit 1 fi @@ -251,6 +271,7 @@ quickStart() { # --clone: clones branch $GITHUB_BRANCH of java-tron into the current directory. cloneCode() { if type git >/dev/null 2>&1; then + # The checkout goes to ./java-tron. if git clone -b $GITHUB_BRANCH $GITHUB_REPOSITORY; then echo 'info: git clone java-tron success' else @@ -285,6 +306,8 @@ cloneBuild() { # Sets $pid to the process ids of the running $JAR_NAME; empty when it is not running. checkPid() { + # Match the jar file name without its directory. Lines containing "start" are dropped so + # that this script itself is not matched. pid=$(ps -ef | grep -v start | grep "${JAR_NAME##*/}" | grep -v grep | awk '{print $2}') } @@ -364,11 +387,14 @@ setJVMMemory() { os=`uname` if [[ $os == 'Linux' ]] || [[ $os == 'linux' ]] ; then 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_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 @@ -378,6 +404,7 @@ setJVMMemory() { fi elif [[ $os == 'Darwin' ]]; then + # macOS keeps JVM_MS / JVM_MX and uses 1g of direct memory. MAX_DIRECT_MEMORY='1g' fi } @@ -392,6 +419,8 @@ startService() { exit 1 fi + # 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 @@ -399,7 +428,10 @@ startService() { tail_opts='-XX:NewRatio=2' fi + # The node keeps many database and network files open. Set the open file limit to 65535 if the + # shell allows it. ulimit -n 65535 2>/dev/null || echo 'warn: failed to set ulimit -n 65535' + # 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 \ @@ -420,6 +452,7 @@ rebuildManifest() { 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 @@ -431,6 +464,8 @@ rebuildManifest() { fi ARCHIVE_JAR='ArchiveManifest.jar' + # 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' if ! downloadRelease "$(getLatestReleaseVersion)" $RELEASE_ARCHIVE_JAR $ARCHIVE_JAR; then @@ -439,6 +474,7 @@ rebuildManifest() { fi fi echo 'info: execute rebuild manifest.' + # A failed rebuild does not block the start either. The tool writes its log under logs/. if $JAVACMD -jar $ARCHIVE_JAR -d $REBUILD_DIR -m $REBUILD_MANIFEST_SIZE -b $REBUILD_BATCH_SIZE; then echo 'info: rebuild manifest success' else @@ -455,9 +491,11 @@ specifyConfig(){ 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 @@ -469,9 +507,11 @@ specifyConfig(){ 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 $configUrl $FULL_NODE_CONFIG_DIR/$configName || exit 1 fi + # The selected config is passed to FullNode when it is started. DEFAULT_FULL_NODE_CONFIG=$FULL_NODE_CONFIG_DIR/$configName } @@ -486,21 +526,28 @@ checkSign() { 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 + # 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 @@ -520,7 +567,6 @@ restart() { startService } -# Command line options. Anything not listed here is passed to FullNode as is. while [ -n "$1" ]; do case "$1" in -c) From 0479e69e55a1b3dcfe3ced67e549230eb1849803 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 30 Sep 2026 21:15:58 +0800 Subject: [PATCH 11/11] fix(config): stop only the own node in start.sh and harden its options start.sh had several problems that predate this branch: - --stop matched every process whose command line contained the jar name and dropped any line containing "start", so it could kill other nodes and miss its own. The node id is now kept in .pid; a directory with a start.log but no pid file was used by the old script and its node is still found by name. --stop also finds a node set up by --release or -cb from the parent directory, and says so when no node was started from the current one. - sh start.sh failed under dash; the script now reruns itself under bash. - an option without its value looped forever; needValue exits instead. - --disable-rewrite-manifest was only accepted as --disable-rewrite-manifes. - the first unknown option swallowed all following arguments; each unknown token is now passed on and parsing continues, with -- as an explicit end. - paths with spaces broke unquoted expansions; FullNode options are an array now. - --release downloaded straight onto FullNode.jar; it now uses a temporary file like --upgrade. - gc.log was archived before the node was stopped, and by --download. - a JDK that exists but cannot run went unnoticed, and a JVM that died right after the start was reported as running. - the manifest rebuild looked for the database next to the caller instead of the node directory, --net stored a relative config path, the gc log rotation deleted unrelated files, ulimit lowered a higher limit, and the rebuild message named logs/archive.log instead of logs/toolkit.log. shell.md documents the pid file, -- and the corrected option spelling. --- shell.md | 20 +++- start.sh | 281 ++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 200 insertions(+), 101 deletions(-) diff --git a/shell.md b/shell.md index f1a66fb7c44..8a69cd2b170 100644 --- a/shell.md +++ b/shell.md @@ -36,6 +36,8 @@ Downloaded release jars are verified against the GPG signature published with ea 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 ``` @@ -64,15 +66,19 @@ Downloaded release jars are verified against the GPG signature published with ea * `--stop` or `-s` - stop the service + stop the service started from the current directory + +* `--` + + 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` @@ -164,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. @@ -174,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 ``` @@ -226,6 +234,8 @@ Following file structure will be created: ├── FullNode/ |── config.conf ├── FullNode.jar + ├── FullNode.jar.pid + ├── start.log ├── start.sh ``` diff --git a/start.sh b/start.sh index 0f851d3fb02..1da7ae3915c 100644 --- a/start.sh +++ b/start.sh @@ -17,6 +17,12 @@ # ############################################################################## +# 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" @@ -25,7 +31,8 @@ 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' @@ -51,8 +58,10 @@ SPECIFY_MEMORY=0 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_DIR="output-directory/database" REBUILD_MANIFEST_SIZE=128 REBUILD_BATCH_SIZE=80000 @@ -130,8 +139,13 @@ 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_PROPERTIES=$("$JAVACMD" -XshowSettings:properties -version 2>&1) +# 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}' @@ -149,31 +163,42 @@ esac RELEASE_JAR="FullNode$RELEASE_ARCH_SUFFIX.jar" RELEASE_ARCHIVE_JAR="ArchiveManifest$RELEASE_ARCH_SUFFIX.jar" -# Archives the previous gc.log into logs/gc_logs/ before a start and keeps the newest 5 archives. +# 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 } @@ -193,13 +218,13 @@ upgrade() { if [[ -n $latest_version ]]; 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 + 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' + mv "$JAR_NAME" "${JAR_NAME}_bak" fi - mv $JAR_NAME.download $JAR_NAME + mv "$JAR_NAME.download" "$JAR_NAME" echo "info: download version $latest_version success" else # download or checkSign has already removed the temporary file. @@ -237,14 +262,12 @@ downloadRelease() { # 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 @@ -257,10 +280,13 @@ quickStart() { mkdirFullNode echo "info: check latest version: $full_node_version" echo 'info: download config' - download $MAIN_NET_CONFIG_URL config.conf || exit 1 + 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" - downloadRelease $full_node_version $RELEASE_JAR $JAR_NAME || exit 1 + 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' @@ -295,25 +321,64 @@ cloneBuild() { 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 } -# Sets $pid to the process ids of the running $JAR_NAME; empty when it is not running. +# 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() { - # Match the jar file name without its directory. Lines containing "start" are dropped so - # that this script itself is not matched. - pid=$(ps -ef | grep -v start | grep "${JAR_NAME##*/}" | grep -v grep | awk '{print $2}') + 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 } # Stops the running node: sends SIGTERM once per second for up to MAX_STOP_TIME seconds, -# then SIGKILL. +# 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 @@ -410,14 +475,14 @@ setJVMMemory() { } # Starts $JAR_NAME in the background with the JVM options for the detected Java version -# (CMS on JDK 8, ZGC on JDK 17), passing $FULL_START_OPT and the config file. Output goes -# to start.log. +# (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 [[ ! -f $JAR_NAME ]]; then echo "warn: jar file $JAR_NAME not exist" 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. @@ -428,19 +493,31 @@ startService() { tail_opts='-XX:NewRatio=2' fi - # The node keeps many database and network files open. Set the open file limit to 65535 if the - # shell allows it. - ulimit -n 65535 2>/dev/null || echo 'warn: failed to set ulimit -n 65535' + # 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 \ + 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 \ $tail_opts -jar \ - $JAR_NAME $FULL_START_OPT -c $DEFAULT_FULL_NODE_CONFIG >>start.log 2>&1 & - checkPid + "$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 @@ -474,11 +551,11 @@ rebuildManifest() { fi fi echo 'info: execute rebuild manifest.' - # A failed rebuild does not block the start either. The tool writes its log under logs/. - if $JAVACMD -jar $ARCHIVE_JAR -d $REBUILD_DIR -m $REBUILD_MANIFEST_SIZE -b $REBUILD_BATCH_SIZE; then + # 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 } @@ -504,15 +581,16 @@ specifyConfig(){ fi if [[ ! -d $FULL_NODE_CONFIG_DIR ]]; then - mkdir -p $FULL_NODE_CONFIG_DIR + 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 $configUrl $FULL_NODE_CONFIG_DIR/$configName || exit 1 + download "$configUrl" "$FULL_NODE_CONFIG_DIR/$configName" || exit 1 fi - # The selected config is passed to FullNode when it is started. - DEFAULT_FULL_NODE_CONFIG=$FULL_NODE_CONFIG_DIR/$configName + # 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 @@ -559,7 +637,11 @@ checkSign() { # 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 @@ -567,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 ;; @@ -618,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 ;; @@ -631,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 ;; @@ -651,12 +742,9 @@ while [ -n "$1" ]; do ;; --stop|-s) stopService - exit 0 - ;; - FullNode) - shift 1 + exit $? ;; - FullNode.jar) + FullNode|FullNode.jar) shift 1 ;; *.jar) @@ -675,16 +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 -# Main flow: optional clone / download steps, then one start of the node. -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 @@ -701,7 +790,7 @@ fi if [[ $DOWNLOAD == true ]]; then latest=$(getLatestReleaseVersion) if [[ -n $latest ]]; then - downloadRelease $latest $RELEASE_JAR $JAR_NAME.download && mv $JAR_NAME.download $JAR_NAME + downloadRelease "$latest" "$RELEASE_JAR" "$JAR_NAME.download" && mv "$JAR_NAME.download" "$JAR_NAME" exit else echo 'info: not getting the latest version'