Summary

This RFC introduces both the API and implementation of the bee-ternary crate, a general-purpose ternary manipulation crate written in Rust that may be used to handle ternary data in the IOTA ecosystem.

Motivation

Ternary has been a fundamental part of the IOTA technology and is used throughout, including for fundamental features like wallet addresses and transaction identification.

More information about ternary in IOTA can be found here.

Note that the IOTA foundation is seeking to reduce the prevalence of ternary in the IOTA protocol to better accomodate modern binary architectures. See Chrysalis for more information.

Manipulating ternary in an ergonomic manner on top of existing binary-driven code can require complexity and, up until now, comes either with a lot of boilerplate code, implementations that are difficult to verify as correct, or a lack of features.

bee-ternary seeks to become the canonical ternary crate in the Rust IOTA ecosystem by providing an API that is efficient, ergonomic, and featureful all at once.

bee-ternary will allow IOTA developers to more easily write code that works with ternary and allow the simplification of existing codebases through use of the API.

Broadly, there are 3 main benefits that bee-ternary aims to provide over use-specific implementations of ternary manipulation code.

  • Ergonomics: The API should be trivial to use correctly and should naturally guide developers towards correct and efficient solutions.

  • Features: The API should provide a fundamental set of core features that can be used together to cover most developer requirements. Examples of such features include multiple encoding schemes, binary/ternary conversion, (de)serialization, and tryte string de/encoding.

  • Performance: The API should allow 'acceptably' efficient manipulation of ternary data. This is difficult to ruggedly define, and implementation details may change later, but broadly this RFC intends to introduce an API that does not inherently inhibit high performance through poor design choices.

Detailed design

bee-ternary is designed to be extensible and is built on top of a handful of fundamental abstraction types. Below are listed the features of the API and a summary of the core API features.

Features

  • Efficient manipulation of ternary buffers (trits and trytes).
  • Multiple encoding schemes.
  • Extensible design that allows it to sit on top of existing data structures, avoiding unnecessary allocation and copying.
  • An array of utility functions to allow for easy manipulation of ternary data.
  • Zero-cost conversion between trit and tryte formats (i.e: no slower than the equivalent code would be if hand-written).

Key Types & Traits

The crate supports both balanced and unbalanced trit representations, supported through two types, Btrit and Utrit. Btrit is the more common representation and so is the implicit default for most operations in the crate.

Btrit and Utrit


#![allow(unused)]
fn main() {
enum Btrit { NegOne, Zero, PlusOne }
enum Utrit { Zero, One, Two }
}

Trits


#![allow(unused)]
fn main() {
struct Trits<T: RawEncoding> { ... }
}
  • Generic across different trit encoding schemes (see below).
  • Analogous to str or [T].
  • Unsized type, represents a buffer of trits of a specified encoding.
  • Most commonly used from behind a reference (as with &str and &[T]), representing a 'trit slice'.
  • Can be created from a variety of types such asTritBuf and [i8] with minimal overhead.
  • Sub-slices can be created from trit slices at a per-trit level.

TritBuf


#![allow(unused)]
fn main() {
struct TritBuf<T: RawEncodingBuf> { ... }
}
  • Generic across different trit encoding schemes (see below).
  • Analogous to String or Vec<T>.
  • Most common way to manipulate trits.
  • Allows pushing, popping, and collecting trits.
  • Implements Deref<Target=Trits> (in the same way that Vec<T> implements Deref<[T]>).

RawEncoding


#![allow(unused)]
fn main() {
trait RawEncoding { ... }
}
  • Represents a raw trit buffer of some encoding.
  • Common interface implemented by all trit encodings (T1B1, T3B1, T5B1, etc.).
  • Largely an implementation detail, not something you need to care about unless implementing your own encodings.
  • Minimal implementation requirements: safety and most utility functionality is provided by Trits instead.

RawEncodingBuf


#![allow(unused)]
fn main() {
trait RawEncodingBuf { ... }
}
  • Buffer counterpart of RawEncoding, always associated with a specific RawEncoding.
  • Distinct from RawEncoding to permit different buffer-like data structures in the future (linked lists, stack-allocated arrays, etc.).

T1B1/T2B1/T3B1/T4B1/T5B1


#![allow(unused)]
fn main() {
struct TXB1 { ... }
}
  • Types that implement RawEncoding.
  • Allow different encodings in Trits and TritBuf types.

T1B1Buf/T2B1Buf/T3B1Buf/T4B1Buf/T5B1Buf


#![allow(unused)]
fn main() {
struct TXB1Buf { ... }
}
  • Types that implement RawEncodingBuf.
  • Allow different encodings in TritBuf types.
  • Each type is associated with a RawEncoding type.

Tryte


#![allow(unused)]
fn main() {
enum Tryte {
    N = -13,
    O = -12,
    P = -11,
    Q = -10,
    R = -9,
    S = -8,
    T = -7,
    U = -6,
    V = -5,
    W = -4,
    X = -3,
    Y = -2,
    Z = -1,
    Nine = 0,
    A = 1,
    B = 2,
    C = 3,
    D = 4,
    E = 5,
    F = 6,
    G = 7,
    H = 8,
    I = 9,
    J = 10,
    K = 11,
    L = 12,
    M = 13,
}
}
  • Type that represents a ternary tryte.
  • Has the same representation as 3 T3B1 byte-aligned trits.

TryteBuf


#![allow(unused)]
fn main() {
struct TryteBuf { ... }
}
  • A growable linear buffer of Trytes.
  • Roughly analogous to Vec.
  • Has utility methods for converting to/from tryte strings.

API

The API makes up the body of this RFC. Due to its considerable length, this RFC simply refers to the documentation in question. You can find those docs in the bee repository.

Encodings

bee-ternary supports many different trit encodings. Notable encodings are explained in the crate documentation.

Common Patterns

When using the API, the most common types interacted with are Trits and TritBuf. These types are designed to play well with the rest of the Rust ecosystem. Here follows some examples of common patterns that you may wish to make use of.

Turning some i8s into a trit buffer


#![allow(unused)]
fn main() {
[-1, 0, 1, 1, -1, 0]
	.iter()
	.map(|x| Btrit::try_from(*x).unwrap())
	.collect::<TritBuf>()
}

Alternatively, for the T1B1 encoding only, you may directly reinterpret the i8 slice.


#![allow(unused)]
fn main() {
let xs = [-1, 0, 1, 1, -1, 0];

Trits::try_from_raw(&xs, xs.len())
	.unwrap()
}

If you are certain that the slice contains only valid trits then it is possible to unsafely reinterpret with amortised O(1) cost (i.e: it's basically free). However, scenarios in which this is necessary and sound are exceedingly uncommon. If you find yourself doing this, ask yourself first whether it is necessary.


#![allow(unused)]
fn main() {
let xs = [-1, 0, 1, 1, -1, 0];

unsafe { Trits::from_raw_unchecked(&xs, xs.len()) }
}

Turning a trit slice into a tryte string


#![allow(unused)]
fn main() {
trits
	.iter_trytes()
	.map(|trit| char::from(trit))
	.collect::<String>()
}

This becomes even more efficient easier with a T3B1 trit slice since it has the same underlying representation as a tryte slice.


#![allow(unused)]
fn main() {
trits
	.as_trytes()
	.iter()
	.map(|trit| char::from(*trit))
	.collect::<String>()
}

Turning a tryte string into a tryte buffer


#![allow(unused)]
fn main() {
tryte_str
	.chars()
	.map(Tryte::try_from)
	.collect::<Result<TryteBuf, _>>()
	.unwrap()
}

Since this is a common operation, there exists a shorthand.


#![allow(unused)]
fn main() {
TryteBuf::try_from_str(tryte_str).unwrap()
}

Turning a trit slice into a trit buffer


#![allow(unused)]
fn main() {
trits.to_buf()
}

Turning a trit slice into a trit buffer of a different encoding


#![allow(unused)]
fn main() {
trits.encode::<T5B1>()
}

Overwriting a sub-slice of a trit with copies of a trit


#![allow(unused)]
fn main() {
trits[start..end].fill(Btrit::Zero)
}

Copying trits from a source slice to a destination slice


#![allow(unused)]
fn main() {
tgt.copy_from(&src[start..end])
}

Drawbacks

This RFC does not have any particular drawbacks over an alternative approach. Ternary is currently essential to the function of much of the IOTA ecosystem and Bee requires a ternary API of some sort in order to effectively operate within it.

Rationale and alternatives

No suitable alternatives exist to this RFC. Rust does not have a mature ternary manipulation crate immediately available that suits our needs.

Unresolved questions

The API has now been in use in Bee for several months. Questions about additional API features exist, but the current API seems to have proved its suitability.