Quick start with Rust

This quick-start guide shows how to work with linkspace and Rust. It starts with turn a light on and expand with more and more requirements. For each requirement a possible Rust program is shown.

Linkspace is written in Rust, a memory safer, modern programming language. This quick-start guide is modelled after the python quick start guide. Python is more relaxed then Rust. In Rust all function call parameters must be used, every varialbe type must be specified, etc. And code is less readable without the support of a IDE. But Rust is compiled and not interpretated. That implies that the execution of Rust is extremely fast, as with e.g. C language. Python is interpretated, much slower in execution. For small test and example program this isn't an issue, but for larger application it is. And as with Python, in the Linkspace collection there are a set of example programs which illustrates the key Linkspace calls and the different options you can use.

Let's start to see if the linkspace is available on this computer. This is done by creating a toggle.

Turn on a light

The most simple simulation of a light turned on is a simple datapoint with the text content "light on". But with Rust you first have to create a cargo with in the cargo.toml file the dependency with a path to the linkspace library.

...
[dependencies}
linkspace = {path = "../../crates/linkspace"}
...

The first program with in stead of printing "Hello world" a point with text "light on" is created and printed.

use linkspace::prelude::*;
use linkspace::lk_datapoint;

fn main() -> LkResult {
    let point = lk_datapoint(b"light on")?;
    println!("Datapoint data: {}", point.data_str());
    println!("Datapoint hash: {}", point.hash());
    Ok(())
}
Datapoint data: toggle in datapoint
Datapoint hash: IE2o3dWcPOq-4ZRAWWN-QVmebpN_ADrHWIBI2kUKIsQ

First time Rust users, don't be surprised by oddities of Rust. The LkResult, the Ok(()), and in particular the ? all become clear in due time. LkResult is a Rust structure <T,LkError> for error handling and the reason why the function is closed with the Ok(()). More troublesome for initial Rust users are the ? marks. It is a Rust handy feature replacing the .unwrap() function to get the T from the Result<T,Error>. When programming you will gradually get used to them. Your programming environment (compiler) will provide the needed feedback. But even then, don't worry to use some trial and error. Let's focus back on Linkspace.

A datapoint data can hold up to maximum 216-512 Bytes, around 65.000 Bytes. And a datapoint get an unique number in the form of its hash. The hash is 32 Bytes = 256 bit. Notice that IPv4 has 32 bit identities, written e.g. as 198.168.1.0 and hold 4 Billion numbers. That is not enought for our world. Therefore Internet now evolves to IPv6 with 128 bit identities. Linkspace point identifiers with 256 bits=32 Bytes=43 ASCII characters are practically unlimited. And with those 32 Bytes identifiers the chances of getting the same hash twice by accident is measured in meaningless improbabilities; such as twice guessing the same atom in the observable universe - at the same moment in time since the big bang.

Ok, Linkspace works on this computer and in the Linkspace a datapoint, alias light, was turned on.

Turn on a remote light

Assume that the control program for lights is on one computer and the light itself is controlled by another computer. Below you see how the `toggle` point created above is serialized such that it can be transmitted or stored and at the receiving end, in another program or at another computer or latter the `toggle` point can be deserialized and reread.

use linkspace::prelude::*;
use linkspace::lk_datapoint;
use linkspace::point::{lk_deserialize, lk_serialize};

fn main() -> LkResult {
    let point = lk_datapoint(b"toggle in datapoint")?;
    println!("serialize datapoint.data: {}", point.data_str()?);
    println!("The hash of the to-be serialized datapoint: {}",point.hash());

    let serialized_point = lk_serialize(&point)?;
    std::fs::write("/tmp/serialized-point.bin", &serialized_point)?;

    let deserialized_point = std::fs::read("/tmp/serialized-point.bin")?;
    let (received_point, _) = lk_deserialize(&deserialized_point,IOMode::Transmit)?;

    println!("The hash of the de-serialized point: {}", received_point.hash());
    println!("Deserialized datapoint.data: {}", received_point.data_str()?);

    Ok(())
}
serialize datapoint.data: toggle in datapoint
The hash of the to-be serialized datapoint: IE2o3dWcPOq-4ZRAWWN-QVmebpN_ADrHWIBI2kUKIsQ
The hash of the de-serialized point: IE2o3dWcPOq-4ZRAWWN-QVmebpN_ADrHWIBI2kUKIsQ
Deserialize datapoint.data: toggle in datapoint

This program works on the same computer. Between two Internet connected computers the pbytes vector with flexible size needs to be processed with std::io, output and input buffers, and TCP/IP function calls are needed.

Turn on a specific light

Our messages should indicating which specific light bulb we want to turn on. But before we can do so, one need to understand valid/non-valid text. Linkspace uses bytes as characters. Rust print messages and HTTP links uses UTF-8 (modern ASCII) printable characters. Not all bytes are valid printable characters, i.e. valid text.

ASCII text versus Linkspace bytes

When you open your browser and use HTTP to visit http://192.168.1.5:8080/garage/above-door, it opens a stream to 192.168.1.5:8080 and sends a header starting with . The header field /garage/above-door is the 'path'.

Linkspace does a similar thing with paths like /garage/above-door, but it differs in an important way.

HTTP headers like GET ... HTTP/1.1, are part of the message defined by a protocol. The HTTP protocol says header must only contain valid text. Light names with the name: 💡, /, or \0 ( a value of all 0 bits ) are not valid.

A lot of programs as well as HTTP expect valid text only. They break on printing a hash if a field contains e.g. the number \0.

The solution is to use ABE ( ASCII/UTF-8 byte expressions ). When ABE has to print bytes it escapes the unprintable ASCII characters with \x00 as well as '\', '/', ':', ' ', and '[]'. ABE is not part of the linkspace protocol, but it is the normative way to interface between bytes as used in Linkspace and e.g. print statement where you need to output on a terminal or screen printable strings.

println!( [ byte for byte in point.hash() ] ," are 32 bytes ");
println!(point.hash(), "are 43 letters in base64 encoding " );

Path to a specific light

A datapoint only contains a header field with the 'hash' and the large 'data' field. Other header fields are the space field with the domain:group:/path field as well as the pubkey, signature, stamp and link fields. In the datapoint the domain, path, pubkey, stamp and link fields are empty or zero and the group field is the [#:pub] group.

Example of a datapoint
## IE2o3dWcPOq-4ZRAWWN-QVmebpN_ADrHWIBI2kUKIsQ ##
type	DataPoint
group	[#:pub]
domain
path
pubkey	[@:0]
stamp	0
links	0

# data 19 #
toggle in datapoint
## end ##

A linkpoint has more of the header fields filled, in particular the 16 char long domain field, the variable length path field, its (time) stamp and optionally links in the form of a tag with a hash of another point. This path we can use to turn a specific light.

let linkpoint_common =
      lk_linkpoint(&"example:[#:pub]:/garage-above-door", b"data in linkpoint_common",&[])?;
lks_save(&linkpoint_common)?;

let path = lkpath(&[b"garage", b"above-door"]);
let point = lk_linkpoint(&path.as_path(), b"linkpoint_common", &[]);
println!(point.&path_list[]);
lks_save(&linkpoint)?;

let link = [Link { tag: ab(b"tag"), ptr: hash_datapoint1 }];
let linkpoint_with_link = lk_linkpoint(&space, b"data in linkpoint 3", &link)?;
println!("linkpoint_with_link: {:?}", linkpoint_with_link.links().unwrap());
lks_save(&linkpoint_with_link).expect("panic message");
lks_process();

The first example shown creating a linkpoint_common using the `space` string with includes "domain:group:path", then data in byte format and the links, here empty. The second example shows the use of a path with the expectation that space or the domain and group were set earlier with lke_set(). The path itself need no special characters. That means they it can be garage, but also special characters not usable in ASCII text as \0, 💡, or /. In this example lkpath and path_list are used too. The third example shown how a link array [] where each link {} consists of a (16 byte) short tag and the hash of another point. Here a space is used. Such a space can be constructed as follows:

let space: Space = Space {
    domain: ab(b"hello"),
    group: lke_group(),
    // lke_group() is determined by environment variable: LKE_GROUP
    path: Default::default(),
};

The first linkpoint contains the follows header fields and data field, here 16 bytes long.

## c2WnDNv6d1WPutW3AkjCjRD84sw8ILtLDUfBZBhHPgw ##
type	LinkPoint
group	[#:pub]
domain	example
path	/garage-above-door
pubkey	[@:0]
stamp	1742294250427209
links	0

# data 16 #
data in linkpoint_common
## end ##

A field all linkspoints have is the 'stamp'. Its the current time in microseconds since UNIX epoch. It offers some ordering between points. In the vast majority of cases you can trust the stamp's validity, especially when it is ordering points signed by one pubkey. But it is neither unique, nor a proof of ordering.

Above three different linkpoints are created and, if one uses a LkSystem database, the linkpoints are saved and process in the database. Notice in the third save call the `?` is not used, but replaced not by the standard .unwrap(), but by a error handler .expect(). The result of the lk_save call is a Rust std::io::Result<bool> with Result<T, Error>. With the .unwarp() or the ? the T can be extracted from the result. In our case T is a boolean, but if an error occured the expect() is called.

Turn on multiple lights by you

You light up the room! - Sweet, but less so when it is everybody including online strangers.

Next, limit control of the light to only messages coming from you. We need to include security. This requires (public-)key cryptography. Key cryptography works by doing a lot of math on a hash of a message. A hash which supernets have by definition.

Key cryptography in linkspace is used in keypoints. These are signed linkpoints - with all the same fields - and include a public key (i.e. pubkey) and signature.

The pubkey field is a number just like the hash. To determine if a message was created by you, simply check with your signature if the pubkey matches yours.

use linkspace::prelude::*;
use linkspace::point::lk_keypoint_ref;
use linkspace::identity::{lki_encrypt, lki_generate};
use linkspace::work_env::lke_set;

fn main() -> LkResult {
    let your_key = lki_generate();
    println!("pubkey: {}", your_key.pubkey());

    /* saving an (encrypted) key for later */
    let enckey = lki_encrypt(&your_key,b"my password");
    println!("enckey: {:?}", &enckey);

    lke_set(&your_key);

    let path = lkpath( &[b"garage",b"above-door"]);
    let point = lk_keypoint_ref(&your_key, ab(b"toggle"), PUBLIC, &path.as_path(),
                                b"data in keypoint", &[], now())?;
    assert_eq!(point.pubkey().unwrap(), &your_key.pubkey());
    // still the same key, from the lke_set
    let next = lk_keypoint(&path.as_path(), b"data in random linkpoint", &[])?;
    assert_eq!(next.pubkey().unwrap(), &your_key.pubkey());

    // some random message
    let random_key = lki_generate();
    lke_set(&random_key);
    let random = lk_keypoint(&path.as_path(), b"data in random linkpoint", &[])?;
    assert_eq!(random.pubkey().unwrap(), &your_key.pubkey());

    // for latter use
    std::fs::write("/tmp/encrypted-key.txt", enckey)?;
    std::fs::write("/tmp/authorized.txt",point.pubkey().to_string() )?;

    Ok(())
}

resulting in:

pubkey: ehNk4z3tn4_eQ7Xvotn4esGhD0f7H-w2MsSV1P5mAis
enckey: "$lki$argon2d$v=19$m=19456,t=3,p=1$ehNk4z3tn4_eQ7Xvotn4esGhD0f7H-w2MsSV1P5mAis$vNrnVOuAATIq7QztuhuQ-qPQ0pDsvBBh2c1NroHyyJ0"

thread 'main' panicked at apps/exa-rust-tests/src/main.rs:28:5:
assertion `left == right` failed
  left: J2Z5YsdXOiMiAN_AUmaC390VvMjgvY980lSj0dQlPB4
 right: ehNk4z3tn4_eQ7Xvotn4esGhD0f7H-w2MsSV1P5mAis

The first call, lki_generate, generates a public/private (private of secret) key set. Next you need to encrypt immediately your private key with a password resulting in the encrypted key enckey. As shown below you would do this elsewhere and export the pubkey and enckey.

At this moment you have a private/public key. This lets you create keypoints. In the example above the first and second create keypoints have the same key, but the third keypoint not. The second assert_eq macro crashed on purpose as the two key are different. (P.s the ! means macro in Rust and -eq! is not NOT equal, but -eq! means assert-eq macro.)

After the first lines of the program below you write your public key to a text file and share that file with others. Once you created a keypoint with the saved your_key, in which case the key is signed with your signature, you can you can serialize the point and communicate that point to someone else. At the receiving end the point can be deserialized and checked with the public key. Preferrable the file with the public text arrived along another, secure channel.

In the case of your switch and lamp system, the switch generates the public key, shared it with the lamp and the lamp stores the received in a file with authorized switches. When receiving a keypoint with, in the data, the order to turn the lamp on/off, the lamp checks whether the keypoint has public key that is in the list of authorized switches.

use linkspace::prelude::*

let pubkey_file= std::fs::read_to_string("/tmp/authorizedy.txt")?;
let pubkey_read_from_pubkey_file : PubKey= pubkey_file.parse()?;

let enckey = std::fs::read_to_string("/tmp/encrypted-key.txt")?;
let pubkey_read_from_enckey_file = lki_pubkey(&enckey)?;

 // ... assert! of if statement: if pubkey_read_from_enckey_file == pubkey_read_from_pubkey_file ...

Keypoints are proof that the points: data, public key, and other fields, were combined into a unit on a device holding the private key. The machine controlling the light is set up to only react to these kinds of points. A device without that specific private key is unable to create a point that toggles the light. Next we consider what we need to support more people controlling the lights.

Turn on lights by more people

You call, and there is light. For you. For me?

To accept who can use the light, we use the pubkey of those who are authorized from the authorized.txt files.

Once accepted as the receiving keypoint was from an authorize source, we can also indicates to which domain/group they belong. two more relevant fields: the group and domain.

# A group is 32 bytes, the all zeros is the local or private group
# A domain is 16 bytes - left padded with \0
let lp = lk_linkpoint("toggle", domain="example-💡".encode(), group=[0] * 32)
println!("linkpint group {:?}", &lp.group);
println!("linkpoint domain (utf8) {:?}". &lp.domain.utf8()); // strip padding - read as utf8
println!("linkpoint str(lp.domain) {:?}", lp.domain.to_str()); # with padding - ascii byte encoding

If none is provided the public group is used, indicated with [#:pub]. The local group is [#:0]. The domain separates one app from another. Domains can be thought of as port numbers.

The domain, group together with the path they determine a point's "space" as in "domain:group:/path/example" Where any component can be left off to use the default.

The default domain/group is taken from the LKE_DOMAIN / LKE_GROUP environment variables.

Turn on lights by light access

For our lights we'll sketch out a system where an 'admin' must grant permission slips to other people. The computer controlling our lights can demand anyone first sends their permission followed by a toggle request.

// text below is for showing the logic in Rust terms, but it is not 100% Rust

let admin = lki_generate();

let request = lk_keypoint(path=["lamp","garage","above-door"], key=key);

let permission = lk_keypoint(key=admin, path=["permission", request.hash]);

let incoming_points = [permission,request];
let mut accepting = vec![];
for point in incoming_points {
    if point.path0 == b"permission" && point.pubkey == admin.pubkey {
       ptr = point.links[0].ptr;
       if ptr! in accepting {
           accepting[ptr] = True;
       } else {
           if point.path0 == b"lamps" {
               if (ptr.hash in accepting) && (accepting[ptr] == True) {
                   accepting[ptr] = False; // don't allow reuse
                   path = "/".join(point.path_str[1:]);
                   print("toggle-light {}", path);
               }
           }
       }
    }
 }

Two issues pop up. First, if the program stops it forgets its accepted requests. Secondly, if a permission arrives before a request it doesn't work.

Both can be solvable by saving points. The first by writing a new point after fulfilling the request, and the second by reading the points we've saved.

Saving points can be as simple as reading/appending to a file.

Eventually, walking through every message, front to back, becomes slower than keeping an index in a database. Feel free to pick the database that keeps a log for received orders, and an index by hash and (path,pubkey). However, in the next chapter the use of the database library LkSysten that is include in Linkspace is explained.

Turn on lights using a database

To exchange all kind of (digital) information you can create linkpoints and keypoints. These points can be exchanged between programs. Each program can store all points it creates and receives from others. But the fundamental design feature of linkspace is that as each point has a unique number and as a result let's store all points in a shared file/database. We start with three (switch_on_off/lamp/logging) programs that exchange points on one computer, later we'll explain how this can be done between programs running on different computers interconnected by TCP/IP.

The linkspace LkSystem

The linkspace database, or linkspace system LkSystem, is created by the lks_open call. It is possible to use an in memory solution (lks_open_inmen(str)), but the standard call opens a file structure. That file/directory structure can be access by the program threads, but also by cli (command line instruction `lk`). This enables you to monitor the linkspace while the program(s) is executing or afterwards scan through the stored points. See the quickstart-cli document.

The linkspace LkSystem itself consists of a database shared between threads and processes on one computer. Once a point is created it is stored in the database after calling the lks_save. Per thread there is a set of callbacks called whenever a matching packet is observed. For this to happen, the lks_process call updates the view of the thread on the database to include new points saved from other applications and to trigger the registered callbacks.

Scan/watch/tap and queries on the linkspace database

Linkspace v1.jpg

Start with the Switch program (a bin executable created by a Rust cargo call) to switch the lamp on-off. This is done with creating every time a point with a value 1 or 0. The next program, Lamp, will monitor such points to see if the lamp is turned on or off.

use linkspace::lk_linkpoint;
use linkspace::prelude::*;
use linkspace::system::lks_info;
use std::{thread, time};

pub fn main() -> LkResult {
    let space = "example::/garage/above-door";
    let f= lks_open("").ok();
    let lks_name = lks_info(f.as_ref()).name;
    println!("Begin output Switch On/Off (5 cycles / 1 sec).rs using: {}", lks_name);

    let timer = 2; //sec
    let sleep_duration = time::Duration::new(timer, 0);
    let mut cycles = 0;
    let max_cycles = 5;

    while cycles < max_cycles {
        cycles += 1;

        let lp_result = lk_linkpoint(&space, b"1", &[])?;
        lks_save(&lp_result)?;
        println!("on");
        thread::sleep(sleep_duration);

        let lp = lk_linkpoint(&space, b"0", &[])?;
        lks_save(&lp)?;
        println!("off");
        thread::sleep(sleep_duration);
    }
    println!("exit after {} cycles light on-off", cycles);
    Ok(())
}

The lamp process monitors by a lks_watch with the callback function as one of its parameters. Once an update of the database with a lks_process_while takes place, LkSystem checks if a certain query condition is fulfilled.

use linkspace::commons::pull::lkc_pull;
use linkspace::prelude::endian_types::U64;
use linkspace::prelude::*;
use linkspace::system::cb::on_point;
use linkspace::system::lks_watch;

pub fn main() -> LkResult {
    let space = "example::/garage/above-door";
    lks_open("")?;
    println!("Begin output Lamp.rs");

    let lights_q = lkq_space(&space, &":watch above-door", &())?;

    fn toggle(point: &dyn Point) -> ShouldBreak {
        println!(
            "Garage lamp above door is:{}",
            point.data_str().unwrap_or("Invalid Data")
        );
        false
    }

    lks_watch(lights_q, on_point(toggle))?;

    loop {
        let calls = lks_process_while(Some(b"above-door"), U64::MAX)?;

        // NOTE: process_while returned positive once ready and negative when more points are expected
        if calls > 0 {
            // become never true as fn toggle also return false.
            return  Ok(());
        }
 bb    }
}

In the example the lks_process_while is used as in the case of this simple program the program/thread had nothing else to do and can just wait till the condition is fulfilled.

To list all switch/lamp events one can scan with a query for points of interest. With the lks_scan call the LkSystem returns those points that satisfy the query. This is shown in the logging process. It is just a call that reports the list of all lamp on/off points in the database.

// just a reminder: have you executed 'source ./activate'?
use linkspace::{abe::lka_eval2str, prelude::*};

pub fn main() -> LkResult {
    let space = "example::/garage/above-door";
    lks_open("")?;
    println!("Begin output Lamp.rs");

    fn log_report(point: &dyn Point) -> ShouldBreak {
        // let log_line = format!("At {} Garage lamp above door was: {}",
        //                  point.get_stamp(), point.data_str().unwrap_or("INVALID DATA"));
        // just of info, but more advanced, using lka_eval2str use this line in Python of shell apps.
        let log_line = lka_eval2str("At [stamp/us:str]  (delta= [stamp/us:delta]) lamp \
                                was set to [data]", &(point)).unwrap_or("INVALID DATA".into());
        println!("{}",log_line);
        false
    }

    let query_for_scan = lkq_space(&space, &":watch above-door", &[]);
    let amount = lks_scan(&query_for_scan?, &mut log_report)?;
    println!("there were {} points scanned (times lamp on/off)", -amount);
    Ok(())
}

Next to the lks_scan and lks_watch as used above, there is a third function lks_tap. This function is a combination of a scan first followed by a watch.

Summary

Here are some notes on what sets linkspace apart from similar projects.

  • Toggling light bulbs requires few dependencies
  • Not a framework - you call the library, not the other way around
  • A fast hash and security build-in from the start
  • Domains - the focus is on being a generic tool, not solve 1 use case
  • Groups - not everything is in a public group
  • Path - resembles organisation of data in well-known file directories
  • Spaces - no fixed dependency on TCP, IP, or any global network
  • And written in Rust, resulting in very fase execution

Created: 2025-04-03 Thu 10:50