Transaction Processor Tutorial Java

Overview

This tutorial covers the creation of a new Sawtooth transaction family in Java, based on the Sawtooth SDK. We will construct a transaction handler which implements a distributed version of the multi-player game tic- tac-toe.

Note

The SDK contains a fully-implemented version of tic-tac-toe. This tutorial is meant to demonstrate the relevant concepts, rather than to create a complete implementation. See the SDK for full implemenations in multiple languages.

A general description of tic-tac-toe, including the rules, can be found on Wikipedia at:

A full implementation of the tic-tac-toe transaction family can be found in

/project/sawtooth-core/sdk/examples/xo_java/.

Prerequisites

This tutorial assumes that you have gone through Installing and Running Sawtooth and are familiar with the concepts introduced there.

You should be familiar with the concepts introduced in the Installing and Running Sawtooth guide and have a working Sawtooth environment prior to completing this tutorial.

The Transaction Processor

There are two top-level components of a transaction processor: a processor class and a handler class. The SDK provides a general-purpose processor class. The handler class is application-dependent and contains the business logic for a particular family of transactions. Multiple handlers can be connected to an instance of the processor class.

Handlers get called in two ways:

  1. An apply method
  2. Various “metadata” methods

The metadata is used to connect the handler to the processor, and we’ll discuss it at the end of this tutorial. The bulk of the handler, however, is made up of apply and its helper functions, so that’s where we’ll start.

The apply Method

apply gets called with two arguments, transactionRequest and stateStore. transactionRequest holds the command that is to be executed (e.g. taking a space or creating a game), while stateStore stores information about the current state of the game (e.g. the board layout and whose turn it is).

The transaction contains payload bytes that are opaque to the validator core, and transaction family specific. When implementing a transaction handler the binary serialization protocol is up to the implementer.

Without yet getting into the details of how this information is encoded, we can start to think about what apply needs to do. apply needs to

  1. unpack the command data from the transaction,
  2. retrieve the game data from the state store,
  3. play the game, and
  4. save the updated game data.

Accordingly, a top-down approach to apply might look like this:

public void apply(TpProcessRequest transactionRequest, State stateStore) {
  TransactionData transactionData = getUnpackedTransaction(transactionRequest);

  GameData stateData = getStateData(stateStore, transactionData.gameName);

  GameData updatedGameData = playXo(transactionData, stateData);

  storeGameData(transactionData.gameName, updatedGameData, stateStore);
}

Note that the third step is the only one that actually concerns tic-tac-toe; the other three steps all concern the coordination of data.

Data

Note

Transactions and Batches! contains a detailed description of how transactions are structured and used. Please read this document before proceeding, if you have not reviewed it.

So how do we get data out of the transaction? The transaction consists of a header and a payload. The header contains the “signer”, which is used to identify the current player. The payload will contain an encoding of the game name, the action (‘create’ a game, ‘take’ a space), and the space (which will be an empty string if the action isn’t ‘take’). So our getUnpackedTransaction function will look like this:

private TransactionData getUnpackedTransaction(TpProcessRequest transactionRequest)
    throws InvalidTransactionException {
  String signer = transactionRequest.getHeader().getSignerPubkey();
  ArrayList<String> payload
      = decodeData(transactionRequest.getPayload().toStringUtf8());

  if (payload.size() > 3) {
    throw new InvalidTransactionException("Invalid payload serialization");
  }
  while (payload.size() < 3) {
    payload.add("");
  }
  return new TransactionData(payload.get(0), payload.get(1), payload.get(2), signer);
}

Before we say how exactly the transaction payload will be decoded, let’s look at getStateData. Now, as far as the handler is concerned, it doesn’t matter how the game data is stored. The only thing that matters is that given a game name, the state store is able to give back the correct game data. (In our full XO implementation, the game data is stored in a Merkle-radix tree.)

private GameData getStateData(String gameName, State stateStore)
    throws InternalError {
  String address = makeGameAddress(gameName);
  String stateEntry = stateStore.get(address);
  if (stateEntry.length() == 0) {
    return new GameData("", "", "", "", "");
  } else {
    try {
      ArrayList<String> data = decodeData(stateEntry, gameName);
      while (data.size() < 5) {
        data.add("");
      }
      return new GameData(
        data.get(0), data.get(1), data.get(2), data.get(3), data.get(4));
    } catch (Error e) {
      throw new InternalError("Failed to deserialize game data");
    }
  }
}

By convention, we’ll store game data at an address obtained from hashing the game name prepended with some constant:

private String makeGameAddress(String gameName) {
  String hashedName = Utils.hash512(gameName.getBytes("UTF-8"));
  return xoNameSpace + hashedName.substring(0, 64);
}

Finally, we’ll store the game data. To do this, we simply need to encode the updated state of the game and store it back at the address from which it came.

private void storeGameData(String gameName, GameData gameData, State stateStore) {
  String address = makeGameAddress(gameName);

  String encodedGameData = encodeData(gameData)
  ByteString gameByteString = ByteString.copyFromUtf8(encodedGameData);

  Map.Entry<String, ByteString> entry
      = new AbstractMap.SimpleEntry<>(address, gameByteString);

  Collection<Map.Entry<String, ByteString>> addressValues
      = Collections.singletonList(entry);

  Collection<String> addresses = stateStore.set(addressValues);

  if (addresses.size() < 1) {
    throw new InternalError("State Error");
  }
}

So, how should we encode and decode the data? We have many options in binary encoding schemes; the binary data stored in the validator state is up to the implementer of the handler. In this case, we’ll encode the data as a simple UTF-8 comma-separated value string, but we could use something more sophisticated, BSON.

private ArrayList<String> decodeData(String payload) {
  return new ArrayList<>(Arrays.asList(payload.split(",")))
}

private String encodeData(GameData gameData) {
  return String.format(
      "%s,%s,%s,%s,%s",
      gameData.gameName, gameData.board, gameData.state,
      gameData.playerOne, gameData.playerTwo);
}

Implementing Game Play

All that’s left to do is describe how to play tic-tac-toe. The details here are fairly straighforward, and the playXo``function could certainly be implemented in different ways. To see our implementation, go to ``/project/sawtooth-core/sdk/examples/xo_java. We choose to represent the board as a string of length 9, with each character in the string representing a space taken by X, a space taken by O, or a free space. Updating the board configuration and the current state of the game proceeds straightforwardly.

The XoHandler Class

And that’s all there is to apply! All that’s left to do is set up the XoHandler class and its metadata. The metadata is used to register the transaction processor with a validator by sending it information about what kinds of transactions it can handle.

public class XoHandler implements TransactionHandler {

  private String xoNameSpace;

  public XoHandler() {
    try {
      this.xoNameSpace = Utils.hash512(
        this.transactionFamilyName().getBytes("UTF-8")).substring(0, 6);
    } catch (UnsupportedEncodingException usee) {
      usee.printStackTrace();
      this.xoNameSpace = "";
    }
  }

  @Override
  public String transactionFamilyName() {
    return "xo";
  }

  @Override
  public String getEncoding() {
    return "csv-utf8";
  }

  @Override
  public String getVersion() {
    return "1.0";
  }

  @Override
  public Collection<String> getNameSpaces() {
    ArrayList<String> namespaces = new ArrayList<>();
    namespaces.add(this.xoNameSpace);
    return namespaces;
  }
}