Here is a quick doc to explain our current thinking about the binary format.
Syntax descriptions are ASN.1 (X.680), all encodings are representations according to Octet Encoding Rules (OER; X.696).
First, let's define some basic types.
GenericTypes
DEFINITIONS
AUTOMATIC TAGS ::=
BEGIN
-- Small Integers
Int8 ::= INTEGER (-128..127)
UInt8 ::= INTEGER (0..255)
Int16 ::= INTEGER (-32768..32767)
UInt16 ::= INTEGER (0..65535)
Int32 ::= INTEGER (-2147483648..2147483647)
UInt32 ::= INTEGER (0..4294967295)
Int64 ::= INTEGER (-9223372036854775808..9223372036854775807)
UInt64 ::= INTEGER (0..18446744073709551615)
-- Large (Cryptographic) Integers
UInt128 ::= OCTET STRING (SIZE(16))
UInt160 ::= OCTET STRING (SIZE(20))
UInt192 ::= OCTET STRING (SIZE(24))
UInt224 ::= OCTET STRING (SIZE(28))
UInt256 ::= OCTET STRING (SIZE(32))
UInt384 ::= OCTET STRING (SIZE(48))
UInt512 ::= OCTET STRING (SIZE(64))
-- Variable-Length Integers
VarInt ::= INTEGER
VarUInt ::= INTEGER (0..MAX)
-- Variable-Length Data
VarBytes ::= OCTET STRING
END
Most of these are encoded in OER as you would expect. A Uint16 for instance is encoded as two bytes in big-endian order.
Variable-length values (Var*
) are encoded using a length-prefix defined in the
OER standard. A short, readable explanation can be found
here.
Next, let's define some Interledger-specific types.
InterledgerTypes
DEFINITIONS
AUTOMATIC TAGS ::=
BEGIN
IMPORTS
Int8,
VarUInt
FROM GenericTypes
;
-- Readable names for special characters that may appear in ILP addresses
hyphen IA5String ::= "-"
period IA5String ::= "."
underscore IA5String ::= "_"
tilde IA5String ::= "~"
-- A standard interledger address
Address ::= IA5String
(FROM
( hyphen
| period
| "0".."9"
| "A".."Z"
| underscore
| "a".."z"
| tilde )
)
-- --------------------------------------------------------------------------
-- Amounts are provided as base-10 floating point
Amount ::= SEQUENCE {
exponent Int8,
mantissa VarUInt
}
-- --------------------------------------------------------------------------
-- GeneralizedTime is a standard ASN.1 time format based on ISO 8601. It was
-- tempting to use the Unix epoch, however unfortunately, standard POSIX
-- time is discontinuous. If we used POSIX time for expiries for example, a
-- transfer might expire and then un-expire when a leap second occurs. Since
-- Interledger protocols depend on causality, we cannot use POSIX time and
-- since we do not wish to create our own time standard, we end up using ISO
-- 8601.
Timestamp ::= GeneralizedTime
END
The Address
definition is just a very long-winded way to restrict the
character set in pure ASN.1. An Interledger address looks like this:
ilpdemo.red.alice
But we recommend it should always be printed with a prefix of ilp:
, e.g.
ilp:ilpdemo.red.alice
. This allows us to distinguish ILP addresses from
domains and countless other period-delimited identifiers. Please keep in mind
that despite appearances, ILP addresses are a much closer analog to IP addresses
than domain names. They are designed to assist the routing algorithm, but they
are not meant to be particularly short, or human-readable. That said, being able
to dissect ILP addresses visually without a WHOIS tool will help ILP operators
debug problems faster.
Finally, we're ready to tackle the heart of the Interledger Protocol: The ILP header. This header specifies the destination and amount of a payment and enables us to route and forward it closer to the destination.
InterledgerProtocolHeader
DEFINITIONS
AUTOMATIC TAGS ::=
BEGIN
IMPORTS
Address,
Amount
FROM InterledgerTypes
;
InterledgerProtocolHeader ::= SEQUENCE {
-- Destination ILP Address
destinationAccount Address,
-- Amount which must be received at the destination
destinationAmount Amount,
-- Enable ASN.1 Extensibility
...
}
END
The destination
ILP address and the amount
together specify the desired
outcome that we are asking the connector to help us achieve. Note that the
currency is implied - each account is associated with a ledger and each ledger
is associated with a currency or other asset type.
Higher-level protocols might inform the sender about the currency code/symbol of the recipient, but for the purposes of the Interledger layer, we do not need to know which currency it is.
The ...
indicates that we would like to enable ASN.1's extensibility feature.
We will see later what effect this has on the actual binary output.
Here is an example ILP header:
00 11 69 6C 70 64 65 6D 6F 2E 72 65 64 2E 61 6C
69 63 65 00 01 0A
Let's break it down:
00 # extensibility byte
11 # destinationAccount length (17 characters)
69 6C 70 64 65 6D 6F 2E # destinationAccount (as ASCII)
72 65 64 2E 61 6C 69 63 # lit. "ilpdemo.red.alice"
65
00 # amount exponent (0)
01 # amount mantissa length (1 byte)
0A # amount mantissa (10)
Amounts are always provided as base-10 floating point numbers. This example uses an exponent of zero and a mantissa of 10. (In case of dollars this could be $10.)
The destinationAccount
is given as a length-prefixed ASCII string. The length
prefix is itself variable-length as defined in OER. It is encoded as a single
byte in this case since the length is less than 128. However, the encoding can
express lengths up to 2^1017 which is hundreds of orders of magnitude greater
than the available memory on Earth. More information about the length
determinant is available
here
or in section 8.6 of X.696.
The character set of ILP addresses is limited to alphanumeric characters
(a-zA-Z0-9
), hyphen (-
), underscore (_
), tilde (~
) and period (.
).
The period character has special meaning by convention as the primary address
delimiter.
Finally, the first byte in the header is the extensibility byte. It allows extensibility in two ways: optional fields and ASN.1 extensibility.
We can add up to seven optional sections in the header (lower seven bits). Older implementations will ignore the additional sections and we can enforce this though appropriate test vectors.
In addition, if the highest bit is set, compliant implementations will look for a separate extensibility bitmask of variable size which allows further extensibility if needed.
Note that we don't expect to ever have to use the extensibility feature due to the ability to additional headers which we will discuss in the next section.
Interledger headers are not transmitted by themselves. Instead they are encapsulated in a Interledger Packet. The Interledger Packet can contain an infinite number of headers, though we will likely recommend a limit for the overall size of the packet, which ledgers would enforce.
InterledgerProtocolPacket
DEFINITIONS
AUTOMATIC TAGS ::=
BEGIN
IMPORTS
UInt16,
VarBytes
FROM GenericTypes
InterledgerProtocolHeader
FROM InterledgerProtocolHeader
SourceRoutingHeader
FROM SourceRoutingHeader
UserDataHeader
FROM UserDataHeader
CustomHeader
FROM CustomHeader
;
HEADER ::= CLASS
{
&typeId UInt16,
&Type
}
WITH SYNTAX {&typeId &Type}
HeaderSet HEADER ::=
{
{1 InterledgerProtocolHeader} |
{128 UserDataHeader} |
{144 SourceRoutingHeader} |
{16383 CustomHeader} |
{32767 CustomHeader} |
{49151 CustomHeader} |
{65535 CustomHeader}
}
InterledgerPacket ::= SEQUENCE {
header Header
}
Header ::= SEQUENCE {
headerType HEADER.&typeId ({HeaderSet}),
header HEADER.&Type ({HeaderSet}{@headerType}),
ext SEQUENCE {
nextHeader Header OPTIONAL
}
}
END
This schema uses some advanced ASN.1 syntax to implement the header type, so we will use an example to break it down.
00 01 # header type (1 = InterledgerProtocolHeader)
16 # header length (22 bytes)
00 11 69 6C 70 64 65 6D # header data
6F 2E 72 65 64 2E 61 6C
69 63 65 00 01 0A
00 # continue byte - 0x00 means this was the
# last header
We immediately see that this packet contains a header of type 1, which maps to the InterledgerProtocolHeader we discussed in the previous section. Next, we learn the header length - in this case 22 bytes. Next, the actual header data is provided. The example data here comes from the example from the previous section.
Finally, we see a single zero byte. This means that no further headers follow.
The other possible value for this byte is 0x80
, which means another header
follows.
Let's see what that looks like:
00 01 # header type (1 = InterledgerProtocolHeader)
16 # header length (22 bytes)
00 11 69 6C 70 64 65 6D # header data
6F 2E 72 65 64 2E 61 6C
69 63 65 00 01 0A
80 # continue byte - 0x80 means keep going
FF FF # header type (65535 = CustomHeader)
2F # header length (48 bytes)
25 68 74 74 70 73 3A 2F # header data
2F 72 69 70 70 6C 65 2E
63 6F 6D 2F 70 72 6F 74
6F 63 6F 6C 73 2F 72 70
73 70 2F 31 2E 30 08 01
23 45 67 89 AB CD EF
00 # continue byte - end of packet
We can see that outer envelope of the new header follows the previous one, but
we don't know what this header means yet, because we haven't encountered its
type id before (0xFFFF
= 65535).
When an Interledger implementation encounters an unknown header type, it must act according to the value of the two most significant bits:
Bits | Meaning |
---|---|
00 | Drop the payment, reply with an error, issue a refund if possible |
01 | Drop the payment, reply with an error, do not issue a refund |
10 | Drop the payment quietly, do not reply |
11 | Ignore the header, process the payment as if the header wasn't there |
In this case the first two bits are 11
, which means we could safely ignore
this unknown header.
But let's find out what a type 65535 header is in the next section.
CustomHeader is a special header type which allows vendor-specific extensibility. Here is the format:
CustomHeader
DEFINITIONS
AUTOMATIC TAGS ::=
BEGIN
IMPORTS
VarBytes
FROM GenericTypes
;
CustomHeader ::= SEQUENCE {
typeUri UTF8String,
value VarBytes
}
END
Once again, it's easier to understand this by example, so let's extract the example from the previous section and take a look:
25 # typeUri length (37 character)
68 74 74 70 73 3A 2F 2F # typeUri (UTF8)
72 69 70 70 6C 65 2E 63 # lit. "https://ripple.com/protocols/rpsp/1.0"
6F 6D 2F 70 72 6F 74 6F
63 6F 6C 73 2F 72 70 73
70 2F 31 2E 30
08 # value length (8 bytes)
01 23 45 67 89 AB CD EF # value data
Essentially we are looking at a simple key-value encoding. The key in this case refers to the header format and is a URI to avoid collisions between different vendors. The value is the actual header encoded according to the vendor's specification.
Adding custom headers is the preferred way for third parties to extend the protocol. Official, standard headers will be assigned a type ID and added natively to the protocol.
There are two header types mentioned above which we didn't explain, UserDataHeader and SourceRoutingHeader. These formats are still in development and not yet available for review.