T8nServerSubCommand.java

/*
 * Copyright Hyperledger Besu Contributors.
 *
 * 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.evmtool;

import static org.hyperledger.besu.evmtool.T8nExecutor.extractTransactions;

import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.referencetests.ReferenceTestEnv;
import org.hyperledger.besu.ethereum.referencetests.ReferenceTestWorldState;
import org.hyperledger.besu.evm.EvmSpecVersion;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.evm.tracing.OperationTracer;
import org.hyperledger.besu.evmtool.T8nExecutor.RejectedTransaction;
import org.hyperledger.besu.util.LogConfigurator;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import picocli.CommandLine;

@CommandLine.Command(
    name = "t8n-server",
    description = "Run Ethereum State Test server",
    versionProvider = VersionProvider.class)
public class T8nServerSubCommand implements Runnable {

  @CommandLine.Option(
      names = {"--host"},
      description = "Host to bind to")
  private String host = "localhost";

  @CommandLine.Option(
      names = {"--port"},
      description = "Port to bind to")
  private int port = 3000;

  @Override
  public void run() {
    LogConfigurator.setLevel("", "OFF");
    // presume ethereum mainnet for reference and state tests
    SignatureAlgorithmFactory.setDefaultInstance();
    Vertx.vertx()
        .createHttpServer(
            new HttpServerOptions()
                .setHost(host)
                .setPort(port)
                .setHandle100ContinueAutomatically(true)
                .setCompressionSupported(true))
        .requestHandler(req -> req.bodyHandler(body -> handle(req, body)))
        .listen()
        .onSuccess(
            server -> System.out.println("Transition server listening on " + server.actualPort()))
        .onFailure(
            err -> System.err.println("Failed to start transition server: " + err.getMessage()));
  }

  void handle(final HttpServerRequest req, final Buffer body) {
    ObjectMapper objectMapper = JsonUtils.createObjectMapper();
    final ObjectReader t8nReader = objectMapper.reader();
    try {
      var t8nRequest = t8nReader.readTree(body.toString());
      JsonNode state = t8nRequest.get("state");
      JsonNode input = t8nRequest.get("input");

      if (state != null && input != null) {
        handleT8nRequest(req, objectMapper, state, input);
      } else {
        sendHelp(req, objectMapper);
      }
      req.response().send();
    } catch (JsonProcessingException e) {
      req.response().setStatusCode(500).end(e.getMessage());
    }
  }

  void handleT8nRequest(
      final HttpServerRequest req,
      final ObjectMapper objectMapper,
      final JsonNode state,
      final JsonNode input) {
    try {
      String fork = state.get("fork").asText();
      Long chainId = Long.valueOf(state.get("chainid").asText());
      String reward = state.get("reward").asText();

      ReferenceTestEnv referenceTestEnv =
          objectMapper.convertValue(input.get("env"), ReferenceTestEnv.class);
      Map<String, ReferenceTestWorldState.AccountMock> accounts =
          objectMapper.convertValue(input.get("alloc"), new TypeReference<>() {});

      final T8nExecutor.T8nResult result;
      try (ReferenceTestWorldState initialWorldState =
          ReferenceTestWorldState.create(accounts, EvmConfiguration.DEFAULT)) {
        initialWorldState.persist(null);
        List<Transaction> transactions = new ArrayList<>();
        List<RejectedTransaction> rejections = new ArrayList<>();
        JsonNode txs = input.get("txs");
        if (txs != null) {
          if (txs instanceof ArrayNode txsArray) {
            extractTransactions(
                new PrintWriter(System.err, true, StandardCharsets.UTF_8),
                txsArray.elements(),
                transactions,
                rejections);
          } else if (txs instanceof TextNode txt) {
            transactions =
                extractTransactions(
                    new PrintWriter(System.err, true, StandardCharsets.UTF_8),
                    List.<JsonNode>of(txt).iterator(),
                    transactions,
                    rejections);
          }
        }

        result =
            T8nExecutor.runTest(
                chainId,
                fork,
                reward,
                objectMapper,
                referenceTestEnv,
                initialWorldState,
                transactions,
                rejections,
                new T8nExecutor.TracerManager() {
                  @Override
                  public OperationTracer getManagedTracer(final int txIndex, final Hash txHash) {
                    return OperationTracer.NO_TRACING;
                  }

                  @Override
                  public void disposeTracer(final OperationTracer tracer) {
                    // No output streams to dispose of
                  }
                });
      }

      ObjectNode outputObject = objectMapper.createObjectNode();
      outputObject.set("alloc", result.allocObject());
      outputObject.set("body", result.bodyBytes());
      outputObject.set("result", result.resultObject());

      try {
        String response =
            objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(outputObject);
        req.response().setChunked(true);
        req.response().putHeader("Content-Type", "application/json").send(response);
      } catch (JsonProcessingException e) {
        req.response().setStatusCode(500).end(e.getMessage());
      }
    } catch (Throwable t) {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8);
      t.printStackTrace(ps);
      ObjectNode json = objectMapper.createObjectNode();
      json.put("error", t.getMessage());
      json.put("stacktrace", baos.toString(StandardCharsets.UTF_8).replaceAll("\\s", " "));

      t.printStackTrace(System.out);

      req.response().setStatusCode(500).end(json.toString());
    }
  }

  private void sendHelp(final HttpServerRequest req, final ObjectMapper objectMapper) {
    ObjectNode outputObject = objectMapper.createObjectNode();
    outputObject.set("version", TextNode.valueOf(new VersionProvider().getVersion()[0]));
    ArrayNode forks = objectMapper.createArrayNode();
    outputObject.set("forks", forks);
    for (var fork : EvmSpecVersion.values()) {
      forks.add(TextNode.valueOf(fork.getName()));
    }
    outputObject.set("error", TextNode.valueOf("Both 'state' and 'input' fields must be set"));

    try {
      req.response()
          .putHeader("Content-Type", "application/json")
          .end(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(outputObject));

    } catch (JsonProcessingException e) {
      req.response().setStatusCode(500).end(e.getMessage());
    }
  }
}