Skip to content
This repository has been archived by the owner on Jun 1, 2022. It is now read-only.

Codec Framework

Adrian Hope-Bailie edited this page Oct 30, 2017 · 1 revision

Overview

ilp-core provides a framework to encode and decode Interledger packets for transmission from one node to another during an Interledger transaction.

The main Java implementation of a packet is the InterledgerPacket.java interface. In ilp-core, not all POJOs in the library are considered Interledger "packets". For example, an InterledgerAddress.java is used in many packet implementations, but is not something that is sent by itself "on the wire" to facilitate Interledger operations.

Conversely, the InterledgerPayment.java interface is considered to be a packet because it is sent by itself "on the wire". For example, an Interledger Payment packet might be sent from a Ledger to a Connector, and then potentially forwarded-on through many Connectors before reaching a destination Ledger.

ILP Core Codec Framework

In ilp-core, there is a codec framework that can assist with serializing and deserializing packet data to/from various binary encoding formats. Currently, Interledger standardizes on the ASN.1 OER encoding format, which allows us to encode a Java packet into a binary format that is concise, easy to transport over information networks, and has a well-known decoding.

The heart of the Codec framework is the CodecContext.java class, which allows you to register various codecs, depending on your use-case.

For example, the following code creates a CodecContext that will read/write instances of InterledgerPayment:

final CodecContext codecContext = new CodecContext()
    .register(OerUint8.class, new OerUint8Codec())
    .register(OerUint64.class, new OerUint64Codec())
    .register(OerLengthPrefix.class, new OerLengthPrefixCodec())
    .register(OerIA5String.class, new OerIA5StringCodec())
    .register(OerOctetString.class, new OerOctetStringCodec())

    // ILP
    .register(InterledgerAddress.class, new InterledgerAddressOerCodec())
    .register(InterledgerPacketType.class, new InterledgerPacketTypeOerCodec())
    .register(InterledgerPayment.class, new InterledgerPaymentOerCodec());

This code will register all of the various codecs required to read and write ASN.1 OER data. For example, in order to encode a payment packet, one will need to be able to encode an InterledgerAddress, which itself is encoded as an Octet String in OER via the OerOctetStringCoded.

Using this paradigm, each building block of a specific encoding can be registered with the framework to create complex encoding and decoding schemes.

Because the above code is somewhat complex, we have created CodecContextFactory, which is preferred over manually assembling your CodecContext because it will be updated automatically as new codecs are supported by the framework. For example, the above code reduces to the following:

final CodecContext codecContext = CodecContextFactory.interledger();

In the future, you might imagine different encoding formats being supported, such as:

final CodecContext codecContext = CodecContextFactory.interledgerProtobuf();

Writing Packets

With the above framework in-place, writing a packet to binary form (i.e., serializing a packet) becomes trivial. If you know the class-type of the object being written, you can use something like the following code:

final OutputStream outputStream = ... // Assemble an outputstream (e.g., ByteArrayOutputStream)
final InterledgerPayment interledgerPayment = ... // Assemble a payment
codecContext.write(InterledgerPayment.java, interledgerPayment, outputStream)

In cases where you don't know the type of the class at runtime, you can allow the framework to attempt to determine this for you using something like this code:

final OutputStream outputStream = ... // Assemble an outputstream (e.g., ByteArrayOutputStream)
final Object interledgerPacket = ... // Assemble some sort of Object that the framework can handle.
codecContext.write(interledgerPacket, outputStream)

In either case, be careful to only attempt to serialize a class that has been registered with the CodecContext. If you attempt to encode a class for which the CodecContext does not know how to handle, an Exception will be thrown.

After encoding to an OutputStream, you will have access to the binary encoding of the object that was written. You can use any type of OutputStream, but for example purposes consider a ByteArrayOutputStream:

final OutputStream os = new ByteArrayOutputStream();
final InterledgerPayment payment = ... // Assemble a payment
codecContext.write(payment, os)
final byte[] encodedBytes = os.toByteArray();

Reading Packets

Reading packets (i.e., deserializing them) is likewise trivial, but involves a little bit more effort on the part of a developer. While the Codec Framework is general enough to handle any type of Java Object for encoding/decoding purposes, we'll focus only on reading binary representation of Interledger packets in these examples.

Simple Reading

The simplest way to decode a binary payload into an Interledger packet object is to call CodecContext.read, like this:

final InputStream inputStream = ... // Assemble an InputStream
final InterledgerPacketType paymentPacketType = new PaymentPacketType();
final InterledgerPacket interledgerPacket = codecContext.read(paymentPacketType, inputStream)

This is elegant, but has two noticeable drawbacks. First, the developer must specify the PaymentPacketType in the method call, and second, the return type from the read method is a generic interface, of type InterledgerPacket (see above for more details about this interface).

Improved Simple Reading

Instead, rather than needing to know the type of the packet ahead of time, the above snippet can be simplified by allowing the Coded Framework to determine the packet type, like this:

final InputStream inputStream = ... // Assemble an InputStream
final InterledgerPacket interledgerPacket = codecContext.read(inputStream)

This is an improvement, but the returned interface is still somewhat generic. We know the returned object was decoded as an InterledgerPacket, but it's unclear which type of packet it really is.

Here, we have two alternatives, each valid during certain scenarios: Concrete Typed Reading and Generic Runtime Handling.

Concrete Typed Reading

If you know the expected Java type of the decoded packet, then the framework allows you to specify this during the read operation, like this:

final InputStream inputStream = ... // Assemble an InputStream
final InterledgerPayment interledgerPayment = codecContext.read(InterledgerPayment.class, inputStream)

Using this method, you can easily obtain a typed instance of your choosing, and all of the complexity of binary decoding is hidden away since it's not important to the goal of operating on, in this case, a concrete instance of InterledgerPayment.

Generic Runtime Handling

Sometimes, it's not readily apparent what kind of packet your InputStream is going to return. For example, in certain streaming examples, one could imagine a series of payment and quoting packets being returned in the same InputStream. Other use-cases abound, but to handle this type of scenario, the InterledgerPacket.Handler interface can be used.

At the simplest level, there is an abstract class called InterledgerPacket.Handler.AbstractHandler that can be used to both guide you as the developer, as well as ensure that every possible use-case of potential "packet" is handled in your code. Consider the following example, where a developer wants to take an OutputStream from the Codec framework, and return a String representing the account in question. Since the account information is accessed differently in each subclass of InterledgerPacket, the framework allows the developer to pass-in business logic at compile-time to map each instance to an appropriate String. Only at runtime will the handler decide which code-path to execute. This is illustrated by the following code:

final CodecContext context = CodecContextFactory.interledger();
final InterledgerPayment interledgerPayment = new InterledgerPayment.Builder()
    .destinationAccount(InterledgerAddressBuilder.builder().value("test3.marty.mcfly").build())
    .destinationAmount(100L)
    .data(new byte[]{})
    .build();

final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
context.write(InterledgerPayment.class, interledgerPayment, outputStream);
    
final InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
final String accountValue = context.readAndHandle(inputStream, new AbstractHandler<String>() {
  @Override
  protected String handle(InterledgerPayment interledgerPayment) {
    return interledgerPayment.getDestinationAccount().getValue();
  }

  @Override
  protected String handle(QuoteLiquidityRequest quoteLiquidityRequest) {
    return quoteLiquidityRequest.getDestinationAccount().getValue();
  }

  @Override
  protected String handle(QuoteLiquidityResponse quoteLiquidityResponse) {
    return quoteLiquidityResponse.getAppliesToPrefix().getValue();
  }
});

logger.info("The account was: {}", accountValue);

The illustration above allows a developer to pass-in business logic that yields a particular result. However, sometimes it is preferable to simply terminate handling of a packet inside of the handler. To accomodate this, the VoidHandler interface can be used instead:

final CodecContext context = CodecContextFactory.interledger();
final InterledgerPayment interledgerPayment = new InterledgerPayment.Builder()
    .destinationAccount(InterledgerAddressBuilder.builder().value("test3.marty.mcfly").build())
    .destinationAmount(100L)
    .data(new byte[]{})
    .build();

final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
context.write(InterledgerPayment.class, interledgerPayment, outputStream);
    
final InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
context.readAndHandle(inputStream, new AbstractVoidHandler() {
  @Override
  protected void handle(InterledgerPayment interledgerPayment) {
    logger.info("The payment account was: {}", interledgerPayment.getDestinationAccount());
  }

  @Override
  protected void handle(QuoteLiquidityRequest quoteLiquidityRequest) {
      logger.info("The quote account was: {}", interledgerPayment.getDestinationAccount());
  }

  @Override
  protected void handle(QuoteLiquidityResponse quoteLiquidityResponse) {
      logger.info("The quote account was: {}", interledgerPayment.getDestinationAccount());
  }
});

Notice that the readAndHandle method no longer returns any value, instead allowing the handler to terminate the operation once the packet is handled.