Time-Warp-NT Layer

time-warp is developed to provide a reliable networking layer with different levels of abstractions. Another important objective of time-warp is to provide an easy way to write and run tests for distributed systems using emulation mode, which should be flexible enough to support various scenarios (tunable network delays, disconnects, other real-time conditions).

time-warp is split up into two main parts:

  1. Mockable interfaces.
  2. Network functionality.

Mockable

Mockable interfaces allow to abstract from language-specific details of implementation of the basic functions.

They are split into several categories. For instance, Mockable Delay contains delay operation, while Mockable Fork keeps elementary functions to manipulate threads.

This innovation allows to launch the same code both in production and testing environment, where the latter allows to emulate time, threads, networking, etc.

Production implements all those interfaces with references to respective prototypes of the functions.

Networking

This layer is written on top of network-transport and provides network capabilities for the application layer. It is split up into two sub-layers: lower and upper.

Lower Layer

This sub-layer is a direct wrapper over network-transport package, and it provides a convenient interface which allows to initiate lightweight connection and send/receive data on it. Please read Network Transport Layer guide for more info.

It supports two types of connections, unidirectional and bidirectional.

Unidirectional Connections

Unidirectional connections allow to send a stream of bytes without waiting for peer’s response.

The function withOutChannel executes given action, providing it with one-shot lightweight connection.

Upon unidirectional connection initialization, node sends U:

+------------------+
|       UNI        |
+------------------+

|   'U' :: Word8   |

Word8 represents 8-bit unsigned integer value.

Bidirectional Сonnections

Bidirectional connections allow both nodes to send and receive bytes to each other.

The function withInOutChannel establishes connection, executes given action with given handle to send and receive bytes on connection, and automatically closes connection on action’s end. Its usage requires a handshake, which contains the following steps.

First, the initiator sends a connection request, which has the following structure:

+------------------+-----------------+
|     `BI_SYN`     |      Nonce      |
+------------------+-----------------+

|   'S' :: Word8   |   Word64 (BE)   |

where Nonce is randomly generated.

Then the peer sends acknowledgement, with the following structure:

+------------------+-----------------+--------------+
|     `BI_ACK`     |      Nonce      |   PeerData   |
+------------------+-----------------+--------------+

|   'A' :: Word8   |   Word64 (BE)   |   Generic    |

where Nonce is the same nonce which came from request.

If the initiator receives the acknowledgement with correct nonce, a conversation is started.

The opposite case could take place if the node have never sent any request on that nonce (peer made a protocol error). It could also be that the node did send the BI_SYN, but its handler for that conversation had already finished. That’s normal, and the node should ignore this acknowledgement.

PeerData is some additional information that is sent from the peer and parsed by the initiator. time-warp gives you an ability to provide some binary data during handshake which then can be used by your application in different ways. The structure of this data is generic. Application Level section describes how Cardano SL uses PeerData.

Messaging

Before talking about upper layer, let’s describe messaging.

In order to specify different handlers for various message types, sent messages should implement Message interface, defining two methods:

  1. messageName, it returns unique message identifier, which is sent along with the message itself and allows receiver to select correct handler to process this message.
  2. formatMessage, it provides description of message, for debug purposes.

Please see Message instance for the Parcel data type as an example.

Upper Layer

This sub-layer enables message exchange. It provides conversation style of communication. This style uses capabilities of bidirectional connection and allows to send/receive messages (one or more). For a single conversation, types of incoming and outgoing messages are fixed. In this case, the initiator node sends the message name once, and then both the initiator and the peer send required messages.

Network events processing is initiated by node function. This function uses two important concepts: worker and listener.

Worker is some action which performs as the initiator of all communication, being supplied with SendActions type which provides function withConnectionTo. This function initiates conversation, executing given action with ConversationActions provided and closing conversation once action completes. In turn, ConversationActions provides send and recv functions to communicate with peer.

Listener is a handler for a message. Each listener remembers type of related message, and several listeners with non-overlapping message types could be defined.

Please see complete example for technical details.

Serialization

time-warp doesn’t rely on any predefined serialization strategy, but rather allows users to use their own.

To define custom serialization, a user should create special data type, the so-called packing type, and implement Serializable interface for it. This interface defines two methods:

  1. packMsg, represents the way how to pack the data to raw bytestring.
  2. unpackMsg, represents the way how to unpack the data.

Please see Serializable instance for the BinaryP data type as an example.