Rung 1: Basics: values, loops, functions

let and mut, integer types, if, for, while, functions and Vec. Everything in Gestalt is bytes and counts.

Everything Gestalt stores is bytes, and everything it counts is a whole number. So the first things you need are few: names for values, numbers of a fixed size, loops, and functions. By the end of this rung you will have written the first function of Gestalt's codec.

The idea #

Names and let #

let seq = 0;        // a name for a value; it cannot change
let mut total = 0;  // `mut`: this one can
total = total + 5;

A name is immutable unless you write mut. You mark change on purpose. That suits a system whose records only ever grow: nothing is quietly overwritten.

Numbers have a size #

TypeHoldsUsed in Gestalt for
u80 to 255, one byteevery byte of every entry
u320 to about 4.3 billionsmall counts, parts of encodings
u640 to about 18 quintillionsequence numbers, lengths, milliseconds, amounts
usizethe machine's wordindexes and lengths of things in memory
i64, i128negative and positivesigned amounts; derive computes in i128

Write numbers as 255, 0xff, 0b1111_1111 or 1_000_000. Convert with as: b as u32.

A number that leaves its range is an overflow. Rust refuses to build when it can see one, and panics at run time in a debug build. Gestalt never lets one pass silently: its arithmetic is checked, and an overflow becomes a hole (rung 3).

Bits #

A byte is 8 bits. Four operators take bytes apart and put them together:

let b: u8 = 0x4e;
let high = b >> 4;            // 0x4: the first hex digit
let low = b & 0x0f;           // 0xe: the second hex digit
let n: u32 = (0x12 << 8) | 0x34;  // 0x1234: append a byte on the right

if, for and while #

let mut zeros = 0;
for b in bytes {              // each item, in order
    if b == 0 {
        zeros += 1;
    }
}

for i in 1..seqs.len() {      // 1, 2, ... up to len - 1 (the end is left out)
    // seqs[i] and seqs[i - 1]
}

while n > 0 {                 // until the condition is false
    n >>= 8;
}

if is an expression, so it can give a value: let size = if n < 24 { 1 } else { 2 };

Functions #

fn sum_cents(amounts: Vec<u64>) -> u64 {
    let mut total = 0;
    for a in amounts {
        total += a;
    }
    total   // the last expression, with no semicolon, is the result
}

Use return to leave early once you know the answer.

Vec and String #

Vec<u8> is a list of bytes that can grow: Vec::new(), vec![1, 2, 3], .push(x), .len(), v[i], .extend_from_slice(&[a, b]).

String is text, always valid UTF-8: String::new(), .push('a'), format!("{:02x}", b).

In this rung every function takes its Vec by value: it consumes it, and the caller can't use it again. Rung 2 shows why that is wasteful and how borrowing fixes it.

Why Gestalt cares #

  • Everything is bytes. An entry, a signature and a digest are all bytes: Vec<u8> or [u8; 32]. The record format is CBOR, hashed with SHA-256.
  • Everything counted is a whole number. Sequence numbers start at 0. Times are milliseconds since 1970. Amounts are in the smallest unit. The kernel has no floats at all, and the determinism gate refuses them (rung 6), because two machines must always compute the same bytes.
  • There is one right way to write a number. CBOR could write 5 in one byte or in nine. Gestalt allows only the shortest form; otherwise the same entry could be written two ways and get two hashes. Exercise 7 is that rule.

In Gestalt's code #

The codec writes the head of every CBOR item with this function. It is exercise 7 for all major types at once. major << 5 puts the major type in the top 3 bits, and match with ranges is a tidier if / else if (rung 3).

From engine/kernel/codec/src/cbor.rs:281:

fn head(out: &mut Vec<u8>, major: u8, n: u64) {
    let m = major << 5;
    match n {
        0..=23 => out.push(m | n as u8),
        24..=0xff => out.extend_from_slice(&[m | 24, n as u8]),
        0x100..=0xffff => {
            out.push(m | 25);
            out.extend_from_slice(&(n as u16).to_be_bytes());
        }
        0x1_0000..=0xffff_ffff => {
            out.push(m | 26);
            out.extend_from_slice(&(n as u32).to_be_bytes());
        }
        _ => {
            out.push(m | 27);
            out.extend_from_slice(&n.to_be_bytes());
        }
    }
}

A digest has a text form: a kind letter and 52 base32 letters. Here is a for, a while and the bit operators working together. Each byte goes into an accumulator; whenever 5 or more bits are waiting, one letter comes out.

From engine/kernel/codec/src/hash.rs:193:

pub fn to_text(&self, kind: char) -> String {
    let mut s = String::with_capacity(1 + TEXT_CHARS);
    s.push(kind);
    let (mut acc, mut bits) = (0u32, 0u32);
    for b in self.0 {
        acc = (acc << 8) | u32::from(b);
        bits += 8;
        while bits >= 5 {
            bits -= 5;
            s.push(BASE32[((acc >> bits) & 31) as usize] as char);
        }
    }
    // ... the last few bits

And here are real bytes. The spec's first line vector (spec/vectors/line.json) starts a7 00 01 01 58 de:

ByteMajor typeSays
a75, a map7 entries follow
000, unsignedkey 0 (kind)
010, unsignedvalue 1: a signed key event
010, unsignedkey 1 (bytes)
58 de2, byteslow bits 24, so the length is the next byte: 0xde = 222 bytes follow

Exercise #

cd ~/Documents/gestalt-learn
./ladder 1

Open rungs/01-basics/src/lib.rs. Seven functions say todo!(). Fill them in from top to bottom and run ./ladder 1 after each one. A passing test says ok. The last one, cbor_uint_head, is the codec's own rule.

Then the two drills. Each one fails to build until you fix it:

./ladder drill 1 immutable
./ladder drill 1 overflow

Stuck? rungs/01-basics/solution/ has one way to do it. Try first. ./ladder solution 1 runs the tests against it.

Check yourself #

  • Why is a name immutable unless you write mut?
  • What does the CBOR byte 0x1a say?
  • Why does Gestalt refuse a number that is not in its shortest form?
  • Why are amounts whole numbers in the smallest unit, never floats?

The cards for this rung ask these and more.

Sign in to mark your progress

Sign in or create a learner to mark the lesson and the exercise, and to review this rung's cards.

The cards (15)

Try each one before you open its answer. Review them on a schedule from the Review page.

  1. What does let give you by default, and how do you allow a value to change?

    Answer

    An immutable name. Write let mut for one that changes. You mark change on purpose.

  2. u8, u64, usize: which is for what?

    Answer

    u8: one byte, 0 to 255. u64: sequence numbers, lengths, milliseconds, amounts. usize: indexes and lengths of things in memory (the machine's word).

  3. For a byte b, what are b >> 4 and b & 0x0f?

    Answer

    The high 4 bits and the low 4 bits: its first and second hex digit. For 0x4e, they are 0x4 and 0xe.

  4. How do you append a byte b to a big-endian number n?

    Answer

    n = (n << 8) | b as u64: shift left one byte, then put b in the low 8 bits. The codec's reader does exactly this.

    See engine/kernel/codec/src/cbor.rs:383

  5. What does a Rust function return if it has no return?

    Answer

    Its last expression, written without a semicolon.

  6. Which values does for i in 1..5 visit? And 1..=5?

    Answer

    1..5: 1, 2, 3, 4 (the end is left out). 1..=5: 1 to 5.

  7. Why does Gestalt's kernel use no floats?

    Answer

    Floats round, and 0.1 + 0.2 is not 0.3. Two nodes must compute the same bytes, so amounts are whole numbers in the smallest unit. The determinism gate refuses floats in the kernel.

    See engine/README.md

  8. What happens when a u8 holding 255 gets 1 added?

    Answer

    Rust refuses to build if it can see it; a debug build panics at run time; a release build wraps to 0 unless the code checks. Gestalt checks: in derive an overflow becomes a hole, Reason::Overflow.

    See engine/kernel/derive/src/num.rs:58

  9. In CBOR's first byte, what do the top 3 bits and the low 5 bits hold?

    Answer

    Top 3: the major type (0 unsigned, 1 negative, 2 bytes, 3 text, 4 array, 5 map, 7 true/false/null). Low 5: the value itself if 0 to 23; 24, 25, 26 or 27 mean 1, 2, 4 or 8 bytes follow.

    See engine/kernel/codec/src/cbor.rs:281

  10. Decode the CBOR bytes 19 01 00.

    Answer

    0x19 is major type 0 with low bits 25: two bytes follow, big-endian. 0x0100 = 256.

  11. Why does Gestalt accept only the shortest CBOR form of each number?

    Answer

    So every value has exactly one encoding. Otherwise one entry could be written two ways and get two hashes. The decoder refuses a longer form as non-shortest.

    See engine/kernel/codec/src/cbor.rs:383

  12. The spec's line vector starts a7 00 01. What do these three bytes say?

    Answer

    a7: a map with 7 entries. 00: key 0 (kind). 01: value 1, a signed key event.

    See spec/vectors/line.json

  13. A record's sequence numbers: where do they start, and what is the rule?

    Answer

    At 0, and each line is exactly one more than the last. prev is null exactly at seq 0.

    See engine/kernel/chain/src/line.rs:11

  14. How many base32 letters does a 32-byte digest take in its text form, and why?

    Answer

    52: 256 bits at 5 bits a letter is 51.2, rounded up. A kind letter goes in front.

    See engine/kernel/codec/src/hash.rs:193

  15. Vec<u8> or String: what is the difference?

    Answer

    Vec<u8> holds any bytes. String is always valid UTF-8 text. CBOR keeps them apart too: major type 2 is bytes, 3 is text.