Skip to content

Java Binding User Guide

Java Binding App is an application that includes the Exonum framework and Java services runtime environment.

Creating Project

The easiest way to create a Java service project is to use a template project generator. After installing Maven 3, run the command:

mvn archetype:generate \
    -DinteractiveMode=false \
    -DarchetypeGroupId=com.exonum.binding \
    -DarchetypeArtifactId=exonum-java-binding-service-archetype \
    -DarchetypeVersion=0.5.0 \
    -DgroupId=com.example.myservice \
    -DartifactId=my-service \
    -Dversion=1.0.0

You can also use the interactive mode:

mvn archetype:generate \
    -DarchetypeGroupId=com.exonum.binding \
    -DarchetypeArtifactId=exonum-java-binding-service-archetype \
    -DarchetypeVersion=0.5.0

The build definition files for other build systems (e.g., Gradle) can be created similarly to the template. For more information see an example.

Service Development

The service abstraction serves to extend the framework and implement the business logic of an application. The service defines the schema of the stored data that constitute the service state; transaction processing rules that can make changes to the stored data; handles events occurring in the ledger; and defines an API for external clients that allows interacting with the service from outside of the system. See more information on the software model of services in the corresponding section.

In Java, the abstraction of a service is represented by Service interface. Implementations can use the abstract class AbstractService.

Schema Description

Exonum provides several collection types to persist service data. The main types are sets, lists and maps. Data organization inside the collections is arranged in two ways – ordinary collections and Merkelized collections; the latter allow providing cryptographic evidence of the authenticity of data to the clients of the system (for example, that an element is stored in the collection under a certain key). The blockchain state is influenced only by the Merkelized collections.

For the detailed description of all Exonum collection types see the corresponding documentation section. In Java, implementations of collections are located in a separate package. Said package documentation describes their use.

Note

SparseListIndex is not yet supported in Java. Let us know if it may be useful for you!

Collections work with a database view – either Snapshot, which is read-only and represents the database state as of the latest committed block; or Fork, which is mutable and allows performing modifying operations. The database view is provided by the framework: Snapshot can be requested at any time, while Fork – only when the transaction is executed. The lifetime of these objects is limited by the scope of the method to which they are passed to.

Exonum stores elements in collections as byte arrays. Therefore, serializers for values stored in collections must be provided. See Serialization for details.

Example of ProofMapIndex Creation

void updateBalance(Fork fork) {
  var name = "balanceById";
  var balanceById = ProofMapIndexProxy.newInstance(name, fork,
      StandardSerializers.hash(),
      StandardSerializers.longs());
  balanceById.put(id, newBalance);
}

A set of named collections constitute a service scheme. For convenient access to service collections you can implement a factory of service collections.

The state of the service in the blockchain is determined by the list of root hashes of its Merkelized collections. Said root hashes, when aggregated with root hashes of other Merkelized collections in the blockchain, form a single blockchain state hash, which is included in each committed block. When using AbstractService, the root hash list must be defined in the schema class that implements Schema interface; when implementing Service directly – in the service itself.

Serialization

As Exonum storage accepts data in the form of byte arrays, storing user data requires serialization. Java Binding provides a set of built-in serializers that you can find in the StandardSerializers utility class. The list of serializers covers the most often-used entities and includes:

  • Standard types: boolean, float, double, byte[] and String. Integers with various encoding types, see StandardSerializers Java documentation and the table below.
  • Exonum types: PrivateKey, PublicKey and HashCode.
  • Any Protobuf messages using StandardSerializers#protobuf.

Besides the available built-in serializers, users can still implement their own serializers for storing their data in a custom format instead of using the built-in ones.

Integer Encoding Types Comparison Table
Type Description The most efficient range
fixed32 Always four bytes If values are often greater than 2^28
uint32 Unsigned int that uses variable-length encoding If values are in range [0; 2^21-1]
sint32 Signed int that uses variable-length encoding If values are in range [-2^20; 2^20-1]
fixed64 Always eight bytes If values are often greater than 2^56
uint64 Unsigned int that uses variable-length encoding If values are in range [0; 2^49-1]
sint64 Signed int that uses variable-length encoding If values are in range [-2^48; 2^48-1]

Transactions Description

Exonum transactions allow you to perform modifying atomic operations with the storage. Transactions are executed sequentially, in the order determined by the consensus of the nodes in the network.

For more details about transactions in Exonum – their properties and processing rules – see the corresponding section of our documentation.

Messages

Transactions are transmitted by external service clients to the framework as Exonum messages. A transaction message contains a header with the identifying information, such as an ID of the service this transaction belongs to and a transaction ID within that service; a payload containing transaction parameters; a public key of the author and a signature that authenticates them.

The transaction payload in the message can be serialized using an arbitrary algorithm supported by both the service client and the service itself.

If the service itself needs to create a transaction on a particular node, it can use the Node#submitTransaction method. This method will create and sign a transaction message using the service key of that particular node (meaning that the node will be the author of the transaction), and submit it to the network. Invoking this method on each node unconditionally will produce N transactions that have the same payloads, but different authors’ public keys and signatures, where N is the number of nodes in the network.

Ed25519 is a standard cryptographic system for digital signing of Exonum messages. It is available through the CryptoFunctions#ed25519 method.

Transaction Lifecycle

The lifecycle of a Java service transaction is the same as in any other Exonum service:

  1. A service client creates a transaction message, including IDs of the service and this transaction, serialized transaction parameters as a payload, and signs the message with the author’s key pair.
  2. The client transmits the message to one of the Exonum nodes in the network. The transaction is identified by the hash of the corresponding message.
  3. The node verifies the correctness of the message: its header, including the service ID, and its cryptographic signature against the author’s public key included into it.
  4. The node verifies that the transaction payload can be correctly decoded by the service into an executable transaction.
  5. If all checks pass, the node that received the message adds it to its local transaction pool and broadcasts the message to all the other nodes in the network.
  6. Other nodes, having received the transaction message, perform all the previous verification steps, and, if they pass, add the message to the local transaction pool.
  7. When majority of validator nodes agree to include this transaction into the next block, they take the message from the transaction pool and convert it into an executable transaction, and execute it.
  8. When all transactions in the block are executed, all changes are atomically applied to the database state and a new block is committed.

The transaction messages are preserved in the database regardless of the execution result, and can be later accessed via Blockchain class. For a more detailed description of transaction processing, see the Transaction Lifecycle section.

Transaction Execution

When the framework receives a transaction message, it must transform it into an executable transaction to process. As every service has several transaction types each with its own parameters, it must provide a TransactionConverter for this purpose (see also Service#convertToTransaction). When the framework requests a service to convert a transaction, its message is guaranteed to have a correct cryptographic signature.

An executable transaction is an instance of a class implementing the Transaction interface and defining transaction business logic. The interface implementations must define an execution rule for the transaction in execute method.

The Transaction#execute method describes the operations that are applied to the current storage state when the transaction is executed. Exonum passes an execution context as an argument, which provides a Fork – a view that allows performing modifying operations; and some information about the corresponding transaction message: its SHA-256 hash that uniquely identifies it, and the author’s public key. A service schema object can be used to access data collections of this service.

Also, Transaction#execute method may throw TransactionExecutionException which contains a transaction error report. This feature allows users to notify Exonum about an error in a transaction execution whenever one occurs. It may check the preconditions before executing a transaction and either accepts it or throws an exception that is further transformed into an Exonum core TransactionResult enum containing an error code and a message with error data. If transaction execution fails, the changes made by the transaction are rolled back, while the error data is stored in the database for further user reference. Light clients also provide access to information on the transaction execution result (which may be either success or failure) to their users.

An implementation of the Transaction#execute method must be a pure function, i.e. it must produce the same observable result on all the nodes of the system for the given transaction. An observable result is the one that affects the blockchain state hash: a modification of a collection that affects the service state hash, or an execution exception.

Blockchain Events

A service can also handle a block commit event that occurs each time the framework commits a new block. The framework delivers this event to implementations of Service#afterCommit(BlockCommittedEvent) callback in each deployed service. Each node in the network processes that event independently from other nodes. The event includes a Snapshot, allowing a read-only access to the database state exactly after the commit of the corresponding block.

As services can read the database state in the handler, they may detect any changes in it, e.g., that a certain transaction is executed; or some condition is met. Services may also create and submit new transactions using Node#submitTransaction. Using this callback to notify other systems is another common use case, but the implementations must pay attention to not perform any blocking operations such as synchronous I/O in this handler, as it is invoked synchronously in the same thread that handles transactions. Blocking that thread will delay transaction processing on the node.

Core Schema API

Users can access information stored in the blockchain by the framework using methods of Blockchain class. This API can be used both in transaction code and in read requests. The following functionality is available:

  • getHeight: long The height of the latest committed block in the blockchain.
  • getBlockHashes: ListIndex<HashCode> The list of all block hashes, indexed by the block height.
  • getBlockTransactions: ProofListIndexProxy<HashCode> The proof list of transaction hashes committed in the block with the given height or ID.
  • getTxMessages: MapIndex<HashCode, TransactionMessage> The map of transaction messages identified by their SHA-256 hashes. Both committed and in-pool (not yet processed) transactions are returned.
  • getTxResults: ProofMapIndexProxy<HashCode, TransactionResult> The map of transaction execution results identified by the corresponding transaction SHA-256 hashes.
  • getTxLocations: MapIndex<HashCode, TransactionLocation> The map of transaction positions inside the blockchain identified by the corresponding transaction SHA-256 hashes.
  • getBlocks: MapIndex<HashCode, Block> The map of block objects identified by the corresponding block hashes.
  • getLastBlock: Block The latest committed block.
  • getActualConfiguration: StoredConfiguration The configuration for the latest height of the blockchain, including services and their parameters.

External Service API

The external service API is used for the interaction between a service and external systems. A set of operations defined by a service usually includes read requests for the blockchain data with the provision of the corresponding cryptographic proof. Exonum provides an embedded web framework for implementing the REST-interface of the service.

Service#createPublicApiHandlers method is used to set the handlers for HTTP requests. These handlers are available at the common path corresponding to the service name. Thus, the /balance/:walletId handler for balance requests in the "cryptocurrency" service will be available at /api/cryptocurrency/balance/:walletId.

See documentation on the possibilities of Vert.x used as a web framework.

Dependencies Management

Exonum uses Guice to describe the dependencies of the service components (both system-specific ones, for example, Exonum time service, and external ones). Each service should define a module describing implementations of the framework interfaces – Service, TransactionConverter and implementations of other components, if any.

Minimalistic Example of Service Module

public class ServiceModule extends AbstractModule {

  @Override
  protected void configure() {
    // Define the Service implementation.
    bind(Service.class).to(CryptocurrencyService.class).in(Singleton.class);

    // Define the TransactionConverter implementation.
    bind(TransactionConverter.class).to(CryptocurrencyTransactionConverter.class);
  }
}

The fully-qualified name (FQN) of the module class must be specified/passed during configuration of an Exonum App that will run the service.

For more information on using Guice, see the project wiki.

Testing

Schema and Operations with Storage

To test the schema and operations with the storage, Exonum provides a database that stores the values in the RAM — MemoryDb. Before using it in integration tests, it is necessary to load a library with the implementation of native methods:

public class MySchemaIntegrationTest {
  static {
    LibraryLoader.load();
  }

  // Tests.
}

The plug-in for running tests should be configured to pass java.library.path system property to JVM:

-Djava.library.path=<path-to-java-bindings-library>

Surefire/Failsafe for Maven should be configured as follows:

<plugin>
    <!-- You can also configure a failsafe to run integration tests during
         'validate' phase of a Maven build to separate unit tests and ITs. -->
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            -Djava.library.path=${path-to-java-bindings-library}
        </argLine>
    </configuration>
</plugin>

Each test can create Snapshot and/or Fork, using MemoryDb, and also apply changes made to Fork to the database state:

MySchemaTest
package com.exonum.binding.qaservice;

import com.exonum.binding.proxy.Cleaner;
import com.exonum.binding.storage.database.Fork;
import com.exonum.binding.storage.database.MemoryDb;
import com.exonum.binding.storage.database.Snapshot;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class MySchemaTest {

  MemoryDb db;
  Cleaner cleaner;

  @Before
  public void setUp() {
    db = MemoryDb.newInstance();
    cleaner = new Cleaner();
  }

  @After
  public void tearDown() throws Exception {
    cleaner.close();
    db.close();
  }

  @Test
  public void testCreatingSnapshot() {
    Snapshot snapshot = db.createSnapshot(cleaner);
    // Use the Snapshot to test the schema.
  }

  @Test
  public void testMergingSomeChanges() {
    Fork fork = db.createFork(cleaner);
    // Make some changes to Fork.
    // …

    // Merge the changes made to the database Fork into the database.
    db.merge(fork);

    // Create a Snapshot of the database state. Must include the changes
    // made above.
    Snapshot snapshot = db.createSnapshot(cleaner);
    // Use the Snapshot to test the schema/transaction
    // with a certain database state.
    // …
  }
}

Warning

Java integration tests that use the shared native library currently work on MacOS only. However, the Java Binding App, which runs Java services, works fine on both MacOS and Linux.

Transactions

To test transactions execution, you can use MemoryDb, as in the previous section.

Read Requests

To test read requests for service data, you can use a fake that implements Node interface and uses MemoryDb to create Snapshot: NodeFake. The MemoryDb contents can be filled in by executing MemoryDb#merge(Fork) operation as in the section above.

API

To test API implemented with Vertx tools, use the tools described in the project documentation. You can use Vertx Web Client as a client or another HTTP client.

An example of API service tests can be found in ApiControllerTest.

How to Run a Service

Currently you have to build a native application to run a node with your Java service:

  • Install the system dependencies and build the application.
  • Follow the instructions in the application guide to configure and start an Exonum node with your service.

Built-In Services

Currently Java Binding includes the following built-in services:

  • Configuration Update Service. Although every node has its own configuration file, some settings should be changed for all nodes simultaneously. This service allows updating global configuration parameters of the network without stopping the nodes. The changes are agreed upon through the consensus mechanism.

See the Configuration Update Service article for more details.

  • Anchoring Service. The anchoring service writes the hash of the current Exonum blockchain state to the Bitcoin blockchain with a certain time interval. The anchored data is authenticated by a supermajority of validators using digital signature tools available in Bitcoin.

See the Anchoring Service article for more details.

  • Time Oracle. Time oracle allows user services to access the calendar time supplied by validator nodes to the blockchain.

See the Time Oracle article for more details.

To enable a particular service, include its name in ejb_app_services.toml configuration file in the EJB App's directory with the following content:

services = ["service-name"]

where possible values for service-name are:

  • configuration for Configuration Update Service.
  • btc-anchoring for Anchoring Service.
  • time for Time Oracle.

In case there is no ejb_app_services.toml file, only Configuration Service will be activated.

Common Library

Java Binding includes a library module that can be useful for Java client applications that interact with an Exonum service. The module does not have the dependency on Java Binding Core, but it contains Java classes obligatory for the core that can now as well be easily used in clients, if necessary. The library provides the ability to create transaction messages, check proofs, serialize/deserialize data and perform cryptographic operations. For using the library just include the dependency in your pom.xml:

    <dependency>
      <groupId>com.exonum.binding</groupId>
      <artifactId>exonum-java-binding-common</artifactId>
      <version>0.5.0</version>
    </dependency>

Known Limitations

  • Core collections necessary to form a complete cryptographic proof for user service data (collections and their elements) are available only in a "raw" form – without deserialization of the content, which makes their use somewhat difficult.
  • Custom Rust services can be added to the application only by modifying and rebuilding thereof.
  • The application supports only one Java service. Support of multiple Java services is coming in the near future.

See Also