RetestethSubCommand.java

/*
 * Copyright ConsenSys AG.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.cli.subcommands;

import static org.hyperledger.besu.cli.subcommands.RetestethSubCommand.COMMAND_NAME;

import org.hyperledger.besu.BesuInfo;
import org.hyperledger.besu.cli.DefaultCommandValues;
import org.hyperledger.besu.cli.custom.JsonRPCAllowlistHostsProperty;
import org.hyperledger.besu.cli.options.stable.LoggingLevelOption;
import org.hyperledger.besu.cli.util.VersionProvider;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.retesteth.RetestethConfiguration;
import org.hyperledger.besu.ethereum.retesteth.RetestethService;
import org.hyperledger.besu.util.LogConfigurator;

import java.net.InetAddress;
import java.nio.file.Path;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

/** Subcommand to run a Retesteth compatible server for reference tests. */
@Command(
    name = COMMAND_NAME,
    description = "Run a Retesteth compatible server for reference tests.",
    mixinStandardHelpOptions = true,
    versionProvider = VersionProvider.class)
@SuppressWarnings("unused")
public class RetestethSubCommand implements Runnable {

  private static final Logger LOG = LoggerFactory.getLogger(RetestethSubCommand.class);

  /** The constant COMMAND_NAME. */
  public static final String COMMAND_NAME = "retesteth";

  /**
   * Using a distinct port for retesteth will result in less testing collisions and accidental RPC
   * calls. This is <code>0xba5e</code> in hex, a hex speak play on the english translation of
   * "Besu."
   */
  public static final int RETESTETH_PORT = 47710;

  @Option(
      names = {"--data-path"},
      paramLabel = DefaultCommandValues.MANDATORY_PATH_FORMAT_HELP,
      description = "The path to Besu data directory (default: ${DEFAULT-VALUE})")
  private final Path dataPath = DefaultCommandValues.getDefaultBesuDataPath(this);

  @Mixin private LoggingLevelOption loggingLevelOption;

  @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
  @Option(
      names = {"--rpc-http-host"},
      paramLabel = DefaultCommandValues.MANDATORY_HOST_FORMAT_HELP,
      description = "Host for Retesteth JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})",
      arity = "1")
  private String rpcHttpHost = autoDiscoverDefaultIP().getHostAddress();

  @Option(
      names = {"--rpc-http-port"},
      paramLabel = DefaultCommandValues.MANDATORY_PORT_FORMAT_HELP,
      description = "Port for Retesteth JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})",
      arity = "1")
  private final Integer rpcHttpPort = RETESTETH_PORT;

  @Option(
      names = {"--host-allowlist", "--host-whitelist"},
      paramLabel = "<hostname>[,<hostname>...]... or * or all",
      description =
          "Comma separated list of hostnames to allow for RPC access, or * to accept any host (default: ${DEFAULT-VALUE})",
      defaultValue = "localhost,127.0.0.1")
  private final JsonRPCAllowlistHostsProperty hostsAllowlist = new JsonRPCAllowlistHostsProperty();

  private InetAddress autoDiscoveredDefaultIP;

  // Used to discover the default IP of the client.
  // Loopback IP is used by default as this is how smokeTests require it to be
  // and it's probably a good security behaviour to default only on the localhost.
  private InetAddress autoDiscoverDefaultIP() {

    if (autoDiscoveredDefaultIP != null) {
      return autoDiscoveredDefaultIP;
    }

    autoDiscoveredDefaultIP = InetAddress.getLoopbackAddress();

    return autoDiscoveredDefaultIP;
  }

  private void prepareLogging() {
    // set log level per CLI flags
    final String logLevel = loggingLevelOption.getLogLevel();
    if (logLevel != null) {
      System.out.println("Setting logging level to " + logLevel);
      LogConfigurator.setLevel("", logLevel);
    }
  }

  @Override
  public void run() {
    prepareLogging();

    final RetestethConfiguration retestethConfiguration = new RetestethConfiguration(dataPath);
    final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault();
    jsonRpcConfiguration.setHost(rpcHttpHost);
    jsonRpcConfiguration.setPort(rpcHttpPort);
    jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist);

    final RetestethService retestethService =
        new RetestethService(BesuInfo.version(), retestethConfiguration, jsonRpcConfiguration);

    Runtime.getRuntime()
        .addShutdownHook(
            new Thread(
                () -> {
                  try {
                    retestethService.close();
                    LogConfigurator.shutdown();
                  } catch (final Exception e) {
                    LOG.error("Failed to stop Besu Retesteth");
                  }
                }));
    retestethService.start();
    try {
      Thread.sleep(Long.MAX_VALUE); // Is there a better way?
    } catch (final InterruptedException e) {
      // e.printStackTrace();
    }
  }
}