feat(system): bind cubefs+cubecode+cubecrypt into one running system (cubesys)
Integrates Packages 3-5 over a single shared CubeStore, the literal CUBELinux premise (data addressed by coordinate, not path). Adds the cubesys crate (lib + cube CLI + cube-demo) proving two end-to-end properties: a cubefs path IS a runnable code cell at the same coordinate, and a sealed record reopens and runs on the same store. Two latent cross-crate bugs surfaced and fixed while integrating: - cubecoords: refresh_flags() now preserves out-of-band flag bits (8..=15), so cubecrypt's HEADER_FLAG_ENCRYPTED survives refresh. - cubestore: record codec now serializes raw flag bits (TLV tag 12) so the encrypted bit survives the store round-trip. All gates green (./check, incl. cubefs --features mount).
This commit is contained in:
@@ -6,6 +6,7 @@ members = [
|
|||||||
"cubefs",
|
"cubefs",
|
||||||
"cubecode",
|
"cubecode",
|
||||||
"cubecrypt",
|
"cubecrypt",
|
||||||
|
"cubesys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -194,6 +194,13 @@ impl HeaderFlags {
|
|||||||
HeaderFlags(bits)
|
HeaderFlags(bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Alias for [`from_bits`] used by record decoders that reconstruct a
|
||||||
|
/// header's raw flag field.
|
||||||
|
#[inline]
|
||||||
|
pub const fn flags_from_bits(bits: u16) -> Self {
|
||||||
|
HeaderFlags(bits)
|
||||||
|
}
|
||||||
|
|
||||||
/// The raw bitmask.
|
/// The raw bitmask.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn bits(&self) -> u16 {
|
pub const fn bits(&self) -> u16 {
|
||||||
@@ -256,6 +263,12 @@ impl CubeHeader {
|
|||||||
|
|
||||||
/// Recompute the flag bits from which fields are present. Call after
|
/// Recompute the flag bits from which fields are present. Call after
|
||||||
/// mutating fields so `flags` stays consistent with the structure.
|
/// mutating fields so `flags` stays consistent with the structure.
|
||||||
|
///
|
||||||
|
/// Known field flags occupy bits 0–7 (title..associations). Any
|
||||||
|
/// out-of-band/spare bits set directly on `flags` (e.g.
|
||||||
|
/// `cubecrypt::HEADER_FLAG_ENCRYPTED` at bit 12) are preserved, because
|
||||||
|
/// they are not derived from structured fields and would otherwise be
|
||||||
|
/// clobbered on every refresh.
|
||||||
pub fn refresh_flags(&mut self) {
|
pub fn refresh_flags(&mut self) {
|
||||||
let mut f = 0u16;
|
let mut f = 0u16;
|
||||||
if self.title.is_some() {
|
if self.title.is_some() {
|
||||||
@@ -279,6 +292,10 @@ impl CubeHeader {
|
|||||||
if !self.linked_records.is_empty() {
|
if !self.linked_records.is_empty() {
|
||||||
f |= HeaderFlags::HAS_ASSOCIATIONS;
|
f |= HeaderFlags::HAS_ASSOCIATIONS;
|
||||||
}
|
}
|
||||||
|
// Preserve spare/out-of-band flag bits (bits 8..=15) that are not
|
||||||
|
// derived from structured fields.
|
||||||
|
const DERIVED_BITS: u16 = 0x00FF;
|
||||||
|
f |= self.flags.bits() & !DERIVED_BITS;
|
||||||
self.flags = HeaderFlags::from_bits(f);
|
self.flags = HeaderFlags::from_bits(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,22 +38,22 @@ pub use env::{CubeEnv, EnvError, Selector, HEADER_FLAG_ENCRYPTED};
|
|||||||
pub use transform::{CryptoError, Key, KeySlot, TransformId};
|
pub use transform::{CryptoError, Key, KeySlot, TransformId};
|
||||||
|
|
||||||
use cubecoords::{CubeHeader, Czyx};
|
use cubecoords::{CubeHeader, Czyx};
|
||||||
use cubestore::{CubeStore, HashBackend};
|
use cubestore::{CubeBackend, CubeStore};
|
||||||
|
|
||||||
/// An append-only access log living in a Null-cube range. Each [`AccessLog`]
|
/// An append-only access log living in a Null-cube range. Each [`AccessLog`]
|
||||||
/// targets one Null-cube head coordinate and appends fixed-size entries
|
/// targets one Null-cube head coordinate and appends fixed-size entries
|
||||||
/// (record coord + op byte + 8-byte timestamp). It is "tamper-evident" in the
|
/// (record coord + op byte + 8-byte timestamp). It is "tamper-evident" in the
|
||||||
/// weak sense that the log itself can be stored encrypted via another
|
/// weak sense that the log itself can be stored encrypted via another
|
||||||
/// [`CubeEnv`]; this type only provides the structure and append/read.
|
/// [`CubeEnv`]; this type only provides the structure and append/read.
|
||||||
pub struct AccessLog {
|
pub struct AccessLog<B: CubeBackend> {
|
||||||
store: CubeStore<HashBackend>,
|
store: CubeStore<B>,
|
||||||
head: Czyx,
|
head: Czyx,
|
||||||
next: u8,
|
next: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessLog {
|
impl<B: CubeBackend> AccessLog<B> {
|
||||||
/// Bind a log to a Null-cube head coordinate (entries append along X).
|
/// Bind a log to a Null-cube head coordinate (entries append along X).
|
||||||
pub fn new(store: CubeStore<HashBackend>, head: Czyx) -> Self {
|
pub fn new(store: CubeStore<B>, head: Czyx) -> Self {
|
||||||
AccessLog {
|
AccessLog {
|
||||||
store,
|
store,
|
||||||
head,
|
head,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ pub trait CubeBackend {
|
|||||||
/// Decision: packs to `u32` (not a 4-tuple key) so the map layout matches the
|
/// Decision: packs to `u32` (not a 4-tuple key) so the map layout matches the
|
||||||
/// PDF's `HashMap<u32, Vec<u8>>` example exactly and stays cheap. A production
|
/// PDF's `HashMap<u32, Vec<u8>>` example exactly and stays cheap. A production
|
||||||
/// backend would replace this with the on-disk store.
|
/// backend would replace this with the on-disk store.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct HashBackend(pub HashMap<u32, Vec<u8>>);
|
pub struct HashBackend(pub HashMap<u32, Vec<u8>>);
|
||||||
|
|
||||||
impl HashBackend {
|
impl HashBackend {
|
||||||
@@ -106,6 +107,7 @@ impl CubeBackend for HashBackend {
|
|||||||
/// The header is length-prefixed so the body boundary is recoverable without
|
/// The header is length-prefixed so the body boundary is recoverable without
|
||||||
/// a fixed schema — this is the "evolve toward explicit C/Z/Y/X-mapped flag
|
/// a fixed schema — this is the "evolve toward explicit C/Z/Y/X-mapped flag
|
||||||
/// bytes" step the PDF mentions, done inline.
|
/// bytes" step the PDF mentions, done inline.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct CubeStore<B: CubeBackend> {
|
pub struct CubeStore<B: CubeBackend> {
|
||||||
backend: B,
|
backend: B,
|
||||||
}
|
}
|
||||||
@@ -180,6 +182,13 @@ mod record_codec {
|
|||||||
if let Some(a) = h.last_remote_access {
|
if let Some(a) = h.last_remote_access {
|
||||||
put_u64(&mut out, 11, a);
|
put_u64(&mut out, 11, a);
|
||||||
}
|
}
|
||||||
|
// Tag 12: raw flag bits. Serializes out-of-band/spare bits (e.g.
|
||||||
|
// `cubecrypt::HEADER_FLAG_ENCRYPTED`) that are not derived from
|
||||||
|
// structured fields, so they survive an encode/decode round-trip.
|
||||||
|
if h.flags.bits() != 0 {
|
||||||
|
out.push(12);
|
||||||
|
out.extend_from_slice(&h.flags.bits().to_le_bytes());
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +266,14 @@ mod record_codec {
|
|||||||
h.last_remote_access = Some(v);
|
h.last_remote_access = Some(v);
|
||||||
b = rest;
|
b = rest;
|
||||||
}
|
}
|
||||||
|
12 => {
|
||||||
|
if b.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let raw = u16::from_le_bytes([b[0], b[1]]);
|
||||||
|
h.flags = cubecoords::HeaderFlags::flags_from_bits(raw);
|
||||||
|
b = &b[2..];
|
||||||
|
}
|
||||||
_ => return None, // unknown tag -> reject (strict at Package 1)
|
_ => return None, // unknown tag -> reject (strict at Package 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "cubesys"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "CUBELinux-2 system integration: one CubeStore shared by cubefs, cubecode and cubecrypt (PDF Packages 3-5 bound into a single running system)."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cubecoords = { path = "../cubecoords" }
|
||||||
|
cubestore = { path = "../cubestore" }
|
||||||
|
cubefs = { path = "../cubefs" }
|
||||||
|
cubecode = { path = "../cubecode" }
|
||||||
|
cubecrypt = { path = "../cubecrypt" }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cube"
|
||||||
|
path = "src/bin/cube.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cube-demo"
|
||||||
|
path = "src/bin/cube_demo.rs"
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# CUBELinux-2 system integration (`cubesys`)
|
||||||
|
|
||||||
|
This crate is the composition layer the PDF implies but does not name as a
|
||||||
|
"Package 6". It binds Packages 3-5 (cubefs, cubecode, cubecrypt) into one
|
||||||
|
running system over a **single shared `CubeStore`**, which is the literal
|
||||||
|
CUBELinux premise: *data addressed by coordinate, not path.*
|
||||||
|
|
||||||
|
## The one invariant
|
||||||
|
|
||||||
|
> The cube is the single source of truth. cubefs is a *view* of it, cubecode
|
||||||
|
> is a *kind* of record in it, cubecrypt is a *transform* applied to records
|
||||||
|
> in it. None of them own storage.
|
||||||
|
|
||||||
|
That is what makes the integration real instead of three crates side by side:
|
||||||
|
every layer operates on the same backend, so a write through one is visible to
|
||||||
|
the others at the same coordinate.
|
||||||
|
|
||||||
|
## What binds to what
|
||||||
|
|
||||||
|
| Layer | Role in the system | Shares via |
|
||||||
|
|--------------|------------------------------------------------------|----------------------------------|
|
||||||
|
| `cubefs` | POSIX namespace; a path `C/Z/Y/X` is a coordinate | `CubeStore`, `path_to_czyx` |
|
||||||
|
| `cubecode` | code cells stored *as records*; VM runs them | `CubeStore`, `CodeCell` |
|
||||||
|
| `cubecrypt` | seals records under a `CubeEnv`; key in Null cubes | `CubeStore`, `CubeEnv` |
|
||||||
|
|
||||||
|
Two primitives in this crate do the binding:
|
||||||
|
|
||||||
|
* `path_to_czyx(path)` — the single bijection (re-exported from `cubefs`) so
|
||||||
|
path and coordinate never disagree.
|
||||||
|
* `load_code_cell(store, path)` — reads the record at a path's coordinate and
|
||||||
|
decodes it as cubevm bytecode. A cubefs file **is** a runnable VM cell.
|
||||||
|
|
||||||
|
## End-to-end properties proved by the tests
|
||||||
|
|
||||||
|
1. **A path is a function.** Writing bytecode at `/c012/z001/y001/x021` makes
|
||||||
|
cubefs list it as a file *and* the VM run it at `Czyx::new(12,1,1,21)` —
|
||||||
|
same coordinate, two views. (Note: an axis value of `0` is Null control
|
||||||
|
space and is rejected as a path component, so every user path maps to a
|
||||||
|
coordinate with all axes in `1..=255`; keys and metadata live in Null
|
||||||
|
space and are addressed directly by `C.Z.Y.X`.)
|
||||||
|
2. **Seal then run is the same record.** `CubeEnv::put_encrypted` at a
|
||||||
|
coordinate leaves the cube carrying an encrypted body (encrypted flag set);
|
||||||
|
the raw body no longer decodes as bytecode. To *execute* a sealed record the
|
||||||
|
system decrypts it back into a plaintext record at the same coordinate, then
|
||||||
|
runs the VM (the VM runs code located by coordinate; reading the envelope
|
||||||
|
directly would fail decode). `open` + `CodeCell::from_record` + `Vm::run`
|
||||||
|
recovers and executes it on the *same* store.
|
||||||
|
|
||||||
|
## Bugs found and fixed while integrating (real, cross-crate)
|
||||||
|
|
||||||
|
Building the integration turned up two latent cross-crate bugs that the
|
||||||
|
per-package test suites never exercised:
|
||||||
|
|
||||||
|
* **`CubeHeader::refresh_flags()` dropped out-of-band flag bits.** It recomputed
|
||||||
|
`flags` only from structured fields (bits 0–7), so `cubecrypt`'s
|
||||||
|
`HEADER_FLAG_ENCRYPTED` (bit 12), which is set out-of-band, was wiped on every
|
||||||
|
refresh. `put_encrypted` calls `refresh_flags` before storing, so an encrypted
|
||||||
|
record lost its flag. Fixed by preserving spare bits (8..=15) in
|
||||||
|
`refresh_flags`. (`cubecoords`)
|
||||||
|
* **The record codec never serialized `flags`.** `record_codec::encode_header`
|
||||||
|
wrote only structured fields, and `decode_header` rebuilt `flags` from them —
|
||||||
|
so even a correctly-set encrypted bit was lost on the store round-trip. Added
|
||||||
|
tag `12` (raw flag bits) to the TLV codec so out-of-band bits survive.
|
||||||
|
(`cubestore`)
|
||||||
|
|
||||||
|
Both are now guarded by `cubesys` integration tests.
|
||||||
|
|
||||||
|
## Why this, and not a "Package 6"
|
||||||
|
|
||||||
|
The PDF arc ends at cubecrypt; the only named next package is `cubeai`, which
|
||||||
|
the directive explicitly excluded (hardware cannot run it). Integration is the
|
||||||
|
honest completion: it turns five isolated crates into the coherent
|
||||||
|
coordinate-addressed system the design describes, without inventing a package
|
||||||
|
the spec never defined.
|
||||||
|
|
||||||
|
## Binaries
|
||||||
|
|
||||||
|
* `cube-demo` — self-contained tour (write two linked cells, run; seal a
|
||||||
|
record, reopen + run). Prints evidence at each step.
|
||||||
|
* `cube` — CLI: `write`, `run`, `ls`, `stat`, `seal`, `open` over one store,
|
||||||
|
interactively (`repl`), from a file (`script`), or as the demo (`demo`).
|
||||||
|
|
||||||
|
## Building / testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /home/CUBELinux/CUBELinux-2
|
||||||
|
./check quick # fmt + clippy -D warnings + tests (the gate)
|
||||||
|
./check mount # also builds cubefs --features mount (FUSE adapter)
|
||||||
|
cargo run -p cubesys --bin cube-demo
|
||||||
|
```
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
//! `cube` — the CUBELinux-2 system CLI.
|
||||||
|
//!
|
||||||
|
//! One process holds ONE in-memory `CubeStore` shared by cubefs, cubecode and
|
||||||
|
//! cubecrypt. Commands are issued as a script (file) or interactively (REPL);
|
||||||
|
//! every command operates on that shared store, so `write` then `run` then
|
||||||
|
//! `seal` then `open` all see the same cube.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! cube # print this help
|
||||||
|
//! cube demo # run the built-in integration demo
|
||||||
|
//! cube repl # read commands from stdin, one per line
|
||||||
|
//! cube script <file> # read commands from <file>, one per line
|
||||||
|
//!
|
||||||
|
//! Commands (operate on the shared cube):
|
||||||
|
//! write <path> <bytes...> # store cubevm bytecode (hex/dec) at a path
|
||||||
|
//! run <path> # load the code cell at <path> and run the VM
|
||||||
|
//! ls <dir> # list a cubefs directory
|
||||||
|
//! stat <path> # getattr via cubefs
|
||||||
|
//! seal <path> <K.C.Z.Y.X> <tf> # encrypt the record at <path> (tf: none|gcm|chacha|xts)
|
||||||
|
//! open <path> <K.C.Z.Y.X> <tf> # decrypt + decode + run the sealed record
|
||||||
|
//!
|
||||||
|
//! Coordinates are written `C.Z.Y.X` (decimal). Key cells live in Null space,
|
||||||
|
//! so they are given directly as coordinates, not as cubefs paths.
|
||||||
|
|
||||||
|
use std::io::BufRead;
|
||||||
|
|
||||||
|
use cubecode::{CodeCell, Kind, Op, Vm};
|
||||||
|
use cubecoords::CubeHeader;
|
||||||
|
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId};
|
||||||
|
use cubestore::{CubeStore, HashBackend};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
match args.get(1).map(|s| s.as_str()) {
|
||||||
|
None => print_help(),
|
||||||
|
Some("demo") => cubesys::demo::run(),
|
||||||
|
Some("repl") => {
|
||||||
|
let mut store = CubeStore::new(HashBackend::new());
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let lock = stdin.lock();
|
||||||
|
for line in lock.lines() {
|
||||||
|
let line = match line {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
if line.trim().is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match run_line(&mut store, &line) {
|
||||||
|
Ok(out) => println!("{out}"),
|
||||||
|
Err(e) => eprintln!("error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("script") => {
|
||||||
|
let file = args.get(2).expect("script needs <file>");
|
||||||
|
let text = std::fs::read_to_string(file).expect("cannot read script file");
|
||||||
|
let mut store = CubeStore::new(HashBackend::new());
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.trim().is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match run_line(&mut store, line) {
|
||||||
|
Ok(out) => println!("{out}"),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
eprintln!("unknown subcommand: {other}\n");
|
||||||
|
print_help();
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"cube - CUBELinux-2 system CLI (cubefs + cubecode + cubecrypt over one store)\n\
|
||||||
|
\n\
|
||||||
|
Usage:\n \
|
||||||
|
cube show this help\n \
|
||||||
|
cube demo run the built-in integration demo\n \
|
||||||
|
cube repl read commands from stdin (one per line)\n \
|
||||||
|
cube script <file> run commands from a file\n\
|
||||||
|
\nCommands:\n \
|
||||||
|
prog <path> <ops...> write CUBEVM bytecode from op names\n \
|
||||||
|
write <path> <bytes...> store cubevm bytecode at a path\n \
|
||||||
|
run <path> run the code cell at <path>\n \
|
||||||
|
ls <dir> list a cubefs directory\n \
|
||||||
|
stat <path> getattr via cubefs\n \
|
||||||
|
seal <path> <K.Z.Y.X> <tf> encrypt a record (tf: none|gcm|chacha|xts)\n \
|
||||||
|
open <path> <K.Z.Y.X> <tf> decrypt + decode + run a sealed record\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute one command line against the shared store.
|
||||||
|
fn run_line(store: &mut CubeStore<HashBackend>, line: &str) -> Result<String, String> {
|
||||||
|
let mut it = line.split_whitespace();
|
||||||
|
let cmd = it.next().ok_or_else(|| "empty line".to_string())?;
|
||||||
|
match cmd {
|
||||||
|
"prog" => {
|
||||||
|
// prog <path> <ops...> — write CUBEVM bytecode assembled from
|
||||||
|
// op names (see `parse_op`). Example:
|
||||||
|
// prog /c005/z001/y001/x007 const 7 halt
|
||||||
|
let path = it.next().ok_or_else(|| "prog needs <path>".to_string())?;
|
||||||
|
let mut ops: Vec<Op> = Vec::new();
|
||||||
|
while let Some(tok) = it.next() {
|
||||||
|
let arg = if takes_arg(tok) {
|
||||||
|
it.next()
|
||||||
|
.and_then(|a| a.parse::<u8>().ok())
|
||||||
|
.ok_or_else(|| format!("prog: {tok} needs a u8 argument"))?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
ops.push(make_op(tok, arg)?);
|
||||||
|
}
|
||||||
|
if ops.is_empty() {
|
||||||
|
return Err("prog: no ops given".to_string());
|
||||||
|
}
|
||||||
|
let name = path.rsplit('/').next().unwrap_or(path);
|
||||||
|
let coord = cubesys::store_code_cell(store, path, Kind::Fn, name, &[], &ops)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(format!(
|
||||||
|
"wrote program {path} -> coord {} ({} ops)",
|
||||||
|
coord.pack_u32(),
|
||||||
|
ops.len()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"write" => {
|
||||||
|
let path = it.next().ok_or_else(|| "write needs <path>".to_string())?;
|
||||||
|
let bytes: Vec<u8> = it
|
||||||
|
.map(parse_byte)
|
||||||
|
.collect::<Option<_>>()
|
||||||
|
.ok_or_else(|| "write: every byte must be hex/dec 0..255".to_string())?;
|
||||||
|
let code =
|
||||||
|
cubecode::decode(&bytes).map_err(|e| format!("bytecode decode error: {e:?}"))?;
|
||||||
|
let name = path.rsplit('/').next().unwrap_or(path);
|
||||||
|
let coord = cubesys::store_code_cell(store, path, Kind::Fn, name, &[], &code)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(format!("wrote {path} -> coord {}", coord.pack_u32()))
|
||||||
|
}
|
||||||
|
"run" => {
|
||||||
|
let path = it.next().ok_or_else(|| "run needs <path>".to_string())?;
|
||||||
|
let cell = cubesys::load_code_cell(store, path).map_err(|e| e.to_string())?;
|
||||||
|
let mut vm = Vm::new(store.clone());
|
||||||
|
let res = vm.run(cell.label);
|
||||||
|
let mut out = format!("run {path} => {res:?}");
|
||||||
|
if !vm.output().is_empty() {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"\n trace: {}",
|
||||||
|
String::from_utf8_lossy(vm.output()).trim_end()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
"ls" => {
|
||||||
|
let dir = it.next().ok_or_else(|| "ls needs <dir>".to_string())?;
|
||||||
|
let fs = cubefs::CubeFs::new(store.clone());
|
||||||
|
let entries = fs.readdir(dir).map_err(|e| format!("ls {dir}: {e:?}"))?;
|
||||||
|
if entries.is_empty() {
|
||||||
|
Ok(format!("ls {dir} -> (empty)"))
|
||||||
|
} else {
|
||||||
|
let names: Vec<String> = entries.into_iter().map(|(n, _)| n).collect();
|
||||||
|
Ok(format!("ls {dir} -> {}", names.join(" ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"stat" => {
|
||||||
|
let path = it.next().ok_or_else(|| "stat needs <path>".to_string())?;
|
||||||
|
let fs = cubefs::CubeFs::new(store.clone());
|
||||||
|
let a = fs
|
||||||
|
.getattr(path)
|
||||||
|
.map_err(|e| format!("stat {path}: {e:?}"))?;
|
||||||
|
Ok(format!(
|
||||||
|
"stat {path} -> ino={} kind={:?} size={} mode={:o}",
|
||||||
|
a.ino, a.kind, a.size, a.mode
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"seal" | "open" => {
|
||||||
|
let path = it.next().ok_or_else(|| format!("{cmd} needs <path>"))?;
|
||||||
|
let keyc = it
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("{cmd} needs <K.Z.Y.X> key cell"))?;
|
||||||
|
let tf = it
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("{cmd} needs <transform>"))?;
|
||||||
|
let coord = cubesys::path_to_czyx(path).map_err(|e| e.to_string())?;
|
||||||
|
let kc =
|
||||||
|
parse_coord(keyc).ok_or_else(|| "bad key-cell coord (use C.Z.Y.X)".to_string())?;
|
||||||
|
let transform = parse_transform(tf)
|
||||||
|
.ok_or_else(|| "unknown transform (none|gcm|chacha|xts)".to_string())?;
|
||||||
|
|
||||||
|
// Ensure key material exists at the Null-cube key cell.
|
||||||
|
if store.get_record(&kc).is_none() {
|
||||||
|
store.put_record(kc, &CubeHeader::new(), b"demo-key-material-32-bytes-long!!");
|
||||||
|
}
|
||||||
|
let env = CubeEnv::new(
|
||||||
|
vec![KeySlot {
|
||||||
|
key_cell: kc,
|
||||||
|
transform,
|
||||||
|
salt: vec![],
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
if cmd == "seal" {
|
||||||
|
let (h, body) = store
|
||||||
|
.get_record(&coord)
|
||||||
|
.ok_or_else(|| format!("seal: no record at {path}"))?;
|
||||||
|
env.put_encrypted(store, coord, Selector::Slot(0), &body, h)
|
||||||
|
.map_err(|e| format!("seal: {e:?}"))?;
|
||||||
|
Ok(format!("sealed {path} under key {} ({tf})", kc.pack_u32()))
|
||||||
|
} else {
|
||||||
|
let (_, envelope) = store
|
||||||
|
.get_record(&coord)
|
||||||
|
.ok_or_else(|| format!("open: no record at {path}"))?;
|
||||||
|
let pt = env
|
||||||
|
.open(store, Selector::Slot(0), &envelope)
|
||||||
|
.map_err(|e| format!("open: {e:?}"))?;
|
||||||
|
let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt)
|
||||||
|
.ok_or_else(|| "open: decrypted body is not valid bytecode".to_string())?;
|
||||||
|
// The VM runs code located by coordinate, so to execute a sealed
|
||||||
|
// record we decrypt it back into a plaintext record, then run.
|
||||||
|
store.put_record(coord, &CubeHeader::new(), &pt);
|
||||||
|
let mut vm = Vm::new(store.clone());
|
||||||
|
let res = vm.run(cell.label);
|
||||||
|
Ok(format!(
|
||||||
|
"open+run {path} (key {}) => {res:?}",
|
||||||
|
kc.pack_u32()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => Err(format!("unknown command: {other}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a byte token: decimal (`42`) or hex (`0x2a`).
|
||||||
|
fn parse_byte(t: &str) -> Option<u8> {
|
||||||
|
if let Ok(v) = t.parse::<u8>() {
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
u8::from_str_radix(t.trim_start_matches("0x"), 16).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a coordinate `C.Z.Y.X` (decimal, allows 0 for Null space).
|
||||||
|
fn parse_coord(s: &str) -> Option<cubecoords::Czyx> {
|
||||||
|
let parts: Vec<&str> = s.split('.').collect();
|
||||||
|
if parts.len() != 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let nums: Option<Vec<u8>> = parts.iter().map(|p| p.parse::<u8>().ok()).collect();
|
||||||
|
let nums = nums?;
|
||||||
|
Some(cubecoords::Czyx::new(nums[0], nums[1], nums[2], nums[3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a cubevm op name (case-insensitive) into an [`Op`]. `arg` is the
|
||||||
|
/// operand byte for ops that take one (const/load/store/jmp/jz/jnz/call/ret/
|
||||||
|
/// syscall); it is ignored for argument-less ops.
|
||||||
|
fn make_op(t: &str, arg: u8) -> Result<Op, String> {
|
||||||
|
Ok(match t.to_ascii_lowercase().as_str() {
|
||||||
|
"nop" => Op::Nop,
|
||||||
|
"halt" => Op::Halt,
|
||||||
|
"const" => Op::Const(arg),
|
||||||
|
"load" => Op::Load(arg),
|
||||||
|
"store" => Op::Store(arg),
|
||||||
|
"add" => Op::Add,
|
||||||
|
"sub" => Op::Sub,
|
||||||
|
"mul" => Op::Mul,
|
||||||
|
"div" => Op::Div,
|
||||||
|
"mod" => Op::Mod,
|
||||||
|
"and" => Op::And,
|
||||||
|
"or" => Op::Or,
|
||||||
|
"xor" => Op::Xor,
|
||||||
|
"shl" => Op::Shl,
|
||||||
|
"shr" => Op::Shr,
|
||||||
|
"eq" => Op::Eq,
|
||||||
|
"ne" => Op::Ne,
|
||||||
|
"lt" => Op::Lt,
|
||||||
|
"gt" => Op::Gt,
|
||||||
|
"le" => Op::Le,
|
||||||
|
"ge" => Op::Ge,
|
||||||
|
"jmp" => Op::Jmp(arg),
|
||||||
|
"jz" => Op::Jz(arg),
|
||||||
|
"jnz" => Op::Jnz(arg),
|
||||||
|
"call" => Op::CallLink(arg),
|
||||||
|
"ret" => Op::Ret,
|
||||||
|
"syscall" => Op::Syscall(arg),
|
||||||
|
other => return Err(format!("prog: unknown op {other}")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True for ops that consume the next token as a u8 operand.
|
||||||
|
fn takes_arg(t: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
t.to_ascii_lowercase().as_str(),
|
||||||
|
"const" | "load" | "store" | "jmp" | "jz" | "jnz" | "call" | "syscall"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_transform(s: &str) -> Option<TransformId> {
|
||||||
|
match s {
|
||||||
|
"none" => Some(TransformId::None),
|
||||||
|
"gcm" => Some(TransformId::Aes256Gcm),
|
||||||
|
"chacha" => Some(TransformId::ChaCha20Poly1305),
|
||||||
|
"xts" => Some(TransformId::Aes256Xts),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! `cube-demo` binary: runs the shared integration demo (see `cubesys::demo`).
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
cubesys::demo::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
//! CUBELinux-2 system integration — Packages 3-5 bound into one running
|
||||||
|
//! system over a single [`CubeStore`] (PDF Packages 3-5).
|
||||||
|
//!
|
||||||
|
//! The five crates in CUBELinux-2 are deliberately separable. This crate is
|
||||||
|
//! the composition layer the PDF implies but never names as a "Package 6":
|
||||||
|
//! it proves the packages interoperate over a *shared* store, exactly as the
|
||||||
|
//! original design intends ("data addressed by coordinate not path").
|
||||||
|
//!
|
||||||
|
//! The binding it enforces:
|
||||||
|
//!
|
||||||
|
//! * **cubefs** lays a POSIX namespace over the cube. Files live at
|
||||||
|
//! `C/Z/Y/X` paths; each file/record owns a [`Czyx`] coordinate.
|
||||||
|
//! * **cubecode** stores code cells *as records* in the very same cube — so a
|
||||||
|
//! file path and a code cell address are the same coordinate. A function
|
||||||
|
//! written through cubefs at `/c012/z000/y000/x021` IS the cell the VM runs
|
||||||
|
//! at `Czyx::new(12,0,0,21)`.
|
||||||
|
//! * **cubecrypt** seals records under a [`CubeEnv`]: the key material lives
|
||||||
|
//! in Null cubes of the *same* store, so the same coordinate opens to
|
||||||
|
//! different plaintext under a different environment.
|
||||||
|
//!
|
||||||
|
//! Two primitives tie them together:
|
||||||
|
//!
|
||||||
|
//! * [`load_code_cell`] — given a cubefs path, read that record and decode it
|
||||||
|
//! into a [`cubecode::CodeCell`]. This is the path→code bridge: a user
|
||||||
|
//! drops a `.fn` file, the VM runs it, no coordinate bookkeeping required.
|
||||||
|
//! * [`path_to_czyx`] — the single bijection (re-exported from `cubefs`) so
|
||||||
|
//! every layer agrees on where a path lands in the cube.
|
||||||
|
//!
|
||||||
|
//! The big design decision (documented in `docs/integration.md`): the cube is
|
||||||
|
//! the single source of truth. cubefs is a *view* of the cube, cubecode is a
|
||||||
|
//! *kind* of record in it, and cubecrypt is a *transform* applied to records
|
||||||
|
//! in it. None of them own storage; the store does. That is the whole point of
|
||||||
|
//! "coordinate-addressed" — and it is what this crate makes runnable.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use cubecode::{CodeCell, Kind};
|
||||||
|
use cubecoords::{CubeHeader, Czyx};
|
||||||
|
use cubefs::path::parse_path;
|
||||||
|
use cubestore::{CubeBackend, CubeStore, HashBackend};
|
||||||
|
|
||||||
|
/// The error type for system-level operations that span packages.
|
||||||
|
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub enum SysError {
|
||||||
|
/// The path did not parse as a cube path.
|
||||||
|
BadPath(String),
|
||||||
|
/// No record exists at the resolved coordinate (plain or encrypted).
|
||||||
|
MissingRecord(Czyx),
|
||||||
|
/// The record's body is not valid cubevm bytecode.
|
||||||
|
NotCode(Czyx),
|
||||||
|
/// The cubecrypt envelope failed to open (wrong env / tampered / bad
|
||||||
|
/// magic). Carries a human reason.
|
||||||
|
Crypto(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SysError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SysError::BadPath(s) => write!(f, "bad path: {s}"),
|
||||||
|
SysError::MissingRecord(c) => {
|
||||||
|
write!(f, "no record at coord {}", c.pack_u32())
|
||||||
|
}
|
||||||
|
SysError::NotCode(c) => {
|
||||||
|
write!(f, "record at coord {} is not valid bytecode", c.pack_u32())
|
||||||
|
}
|
||||||
|
SysError::Crypto(s) => write!(f, "crypto error: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a POSIX path to its [`Czyx`] coordinate using the cubefs bijection.
|
||||||
|
///
|
||||||
|
/// This is the canonical entry point every layer should use so path and
|
||||||
|
/// coordinate never disagree. `c000` and friends are rejected (Null space is
|
||||||
|
/// not a path), so a returned coordinate is always a user-addressable record
|
||||||
|
/// or directory prefix.
|
||||||
|
pub fn path_to_czyx(path: &str) -> Result<Czyx, SysError> {
|
||||||
|
let parsed = parse_path(path).map_err(|e| SysError::BadPath(e.to_string()))?;
|
||||||
|
parsed
|
||||||
|
.czyx()
|
||||||
|
.ok_or_else(|| SysError::BadPath(format!("{path:?} names a directory, not a record")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load (and decode) the code cell stored at a cubefs file path.
|
||||||
|
///
|
||||||
|
/// This is the bridge that makes "a file is a function" literally true: it
|
||||||
|
/// reads the record at the path's coordinate through the *same* store cubefs
|
||||||
|
/// uses, then decodes the body as cubevm bytecode via [`cubecode`]. Returns the
|
||||||
|
/// typed [`CodeCell`] so callers get the kind, name, links and decoded ops.
|
||||||
|
pub fn load_code_cell<B: CubeBackend>(
|
||||||
|
store: &CubeStore<B>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<CodeCell, SysError> {
|
||||||
|
let coord = path_to_czyx(path)?;
|
||||||
|
let (header, body) = store
|
||||||
|
.get_record(&coord)
|
||||||
|
.ok_or(SysError::MissingRecord(coord))?;
|
||||||
|
// Prefer a code cell if the record carries a code doc_type; otherwise try
|
||||||
|
// to decode whatever is there (the cube is code-as-data, so a raw record
|
||||||
|
// body may still be valid bytecode).
|
||||||
|
if let Some(cell) = CodeCell::from_record(coord, &header, &body) {
|
||||||
|
Ok(cell)
|
||||||
|
} else {
|
||||||
|
Err(SysError::NotCode(coord))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a [`CodeCell`] from parts and write it back as a record at `path`,
|
||||||
|
/// so code written by the VM (or tooling) is visible to cubefs at that path.
|
||||||
|
pub fn store_code_cell<B: CubeBackend>(
|
||||||
|
store: &mut CubeStore<B>,
|
||||||
|
path: &str,
|
||||||
|
kind: Kind,
|
||||||
|
name: &str,
|
||||||
|
links: &[Czyx],
|
||||||
|
code: &[cubecode::Op],
|
||||||
|
) -> Result<Czyx, SysError> {
|
||||||
|
let coord = path_to_czyx(path)?;
|
||||||
|
let mut h = CubeHeader::new();
|
||||||
|
h.title = Some(name.to_string());
|
||||||
|
h.doc_type = Some(kind.as_str().to_string());
|
||||||
|
h.linked_records = links.to_vec();
|
||||||
|
if h.doc_type.as_deref() == Some("fn") {
|
||||||
|
h.size_bytes = Some(cubecode::encode(code).len() as u64);
|
||||||
|
}
|
||||||
|
h.refresh_flags();
|
||||||
|
store.put_record(coord, &h, &cubecode::encode(code));
|
||||||
|
Ok(coord)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: an in-memory system store, the default for demos/tests.
|
||||||
|
pub fn memory_store() -> CubeStore<HashBackend> {
|
||||||
|
CubeStore::new(HashBackend::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cubecode::Op;
|
||||||
|
use cubecoords::Czyx;
|
||||||
|
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId, HEADER_FLAG_ENCRYPTED};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_is_the_code_cell_coordinate() {
|
||||||
|
// The headline property: a cubefs path and a code cell resolve to the
|
||||||
|
// SAME coordinate, so the VM runs what cubefs lists as a file.
|
||||||
|
// Note: axis value 0 is Null control space and is NOT a valid path
|
||||||
|
// component, so every axis here is non-zero.
|
||||||
|
let coord = path_to_czyx("/c012/z001/y001/x021").unwrap();
|
||||||
|
assert_eq!(coord, Czyx::new(12, 1, 1, 21));
|
||||||
|
|
||||||
|
let mut store = memory_store();
|
||||||
|
let path = "/c012/z001/y001/x021";
|
||||||
|
store_code_cell(
|
||||||
|
&mut store,
|
||||||
|
path,
|
||||||
|
Kind::Fn,
|
||||||
|
"add",
|
||||||
|
&[],
|
||||||
|
&[Op::Const(2), Op::Const(3), Op::Add, Op::Halt],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// cubefs sees it as a file at that path...
|
||||||
|
let fs = cubefs::CubeFs::new(store.clone());
|
||||||
|
assert_eq!(fs.kind_of(path).unwrap(), cubefs::Kind::File);
|
||||||
|
|
||||||
|
// ...and cubecode decodes the very same record as a runnable cell.
|
||||||
|
let cell = load_code_cell(&store, path).unwrap();
|
||||||
|
assert_eq!(cell.kind(), Kind::Fn);
|
||||||
|
assert_eq!(cell.name(), Some("add"));
|
||||||
|
assert_eq!(
|
||||||
|
cell.code,
|
||||||
|
vec![Op::Const(2), Op::Const(3), Op::Add, Op::Halt]
|
||||||
|
);
|
||||||
|
|
||||||
|
// run it through the VM on the shared store
|
||||||
|
let mut vm = cubecode::Vm::new(store.clone());
|
||||||
|
assert_eq!(vm.run(coord), cubecode::RunResult::Halted { top: Some(5) });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_then_run_is_the_same_record() {
|
||||||
|
// Encrypt a record via cubecrypt, confirm the same coordinate now
|
||||||
|
// carries an encrypted body, then decrypt and run it.
|
||||||
|
let mut store = memory_store();
|
||||||
|
let env = CubeEnv::new(
|
||||||
|
vec![KeySlot {
|
||||||
|
key_cell: Czyx::new(0, 1, 0, 1), // Null cube 1
|
||||||
|
transform: TransformId::Aes256Gcm,
|
||||||
|
salt: vec![],
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
store.put_record(
|
||||||
|
Czyx::new(0, 1, 0, 1),
|
||||||
|
&CubeHeader::new(),
|
||||||
|
b"environment-root-key-32bytes!!",
|
||||||
|
);
|
||||||
|
|
||||||
|
let coord = path_to_czyx("/c005/z001/y001/x007").unwrap();
|
||||||
|
// plaintext bytecode first
|
||||||
|
let code = vec![Op::Const(7), Op::Halt];
|
||||||
|
store.put_record(coord, &plaintext_header(), &cubecode::encode(&code));
|
||||||
|
assert!(load_code_cell(&store, "/c005/z001/y001/x007").is_ok());
|
||||||
|
|
||||||
|
// now seal the SAME coordinate under the env
|
||||||
|
env.put_encrypted(
|
||||||
|
&mut store,
|
||||||
|
coord,
|
||||||
|
Selector::Slot(0),
|
||||||
|
&cubecode::encode(&code),
|
||||||
|
CubeHeader::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (h, _body) = store.get_record(&coord).unwrap();
|
||||||
|
assert!(h.flags.has(HEADER_FLAG_ENCRYPTED));
|
||||||
|
// the raw record body is no longer plaintext bytecode
|
||||||
|
assert!(load_code_cell(&store, "/c005/z001/y001/x007").is_err());
|
||||||
|
|
||||||
|
// decrypt + decode + run on the shared store
|
||||||
|
let envelope = store.get_record(&coord).unwrap().1;
|
||||||
|
let pt = env.open(&store, Selector::Slot(0), &envelope).unwrap();
|
||||||
|
let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt).unwrap();
|
||||||
|
assert_eq!(cell.code, code);
|
||||||
|
// The VM runs code located by coordinate, so to *execute* a sealed
|
||||||
|
// record we decrypt it back into a plaintext record at the same coord,
|
||||||
|
// then run. (Reading the encrypted envelope directly would fail decode.)
|
||||||
|
store.put_record(coord, &CubeHeader::new(), &pt);
|
||||||
|
let mut vm = cubecode::Vm::new(store.clone());
|
||||||
|
assert_eq!(vm.run(coord), cubecode::RunResult::Halted { top: Some(7) });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plaintext_header() -> CubeHeader {
|
||||||
|
let mut h = CubeHeader::new();
|
||||||
|
h.doc_type = Some("fn".into());
|
||||||
|
h.refresh_flags();
|
||||||
|
h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The integration demo: build ONE in-memory [`CubeStore`] and run cubefs,
|
||||||
|
/// cubecode and cubecrypt over it, printing evidence at each step.
|
||||||
|
///
|
||||||
|
/// See `docs/integration.md`. Shared by the `cube` and `cube-demo` binaries.
|
||||||
|
pub mod demo {
|
||||||
|
use cubecode::{Kind, Op, Vm};
|
||||||
|
use cubecoords::Czyx;
|
||||||
|
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId};
|
||||||
|
use cubestore::{CubeStore, HashBackend};
|
||||||
|
|
||||||
|
/// Run the integration tour.
|
||||||
|
pub fn run() {
|
||||||
|
println!("=== CUBELinux-2 integration demo ===\n");
|
||||||
|
|
||||||
|
let mut store = CubeStore::new(HashBackend::new());
|
||||||
|
|
||||||
|
// ---- 1. write code cells as cubefs paths, run through the VM ----------
|
||||||
|
println!("[1] cubefs + cubecode: a path IS a code cell");
|
||||||
|
let double = "/c002/z001/y001/x001";
|
||||||
|
let entry = "/c001/z001/y001/x001";
|
||||||
|
|
||||||
|
let double_code = vec![Op::Store(0), Op::Load(0), Op::Const(2), Op::Mul, Op::Ret];
|
||||||
|
let entry_code = vec![Op::Const(21), Op::CallLink(0), Op::Halt];
|
||||||
|
|
||||||
|
let double_coord =
|
||||||
|
super::store_code_cell(&mut store, double, Kind::Fn, "double", &[], &double_code)
|
||||||
|
.expect("store double");
|
||||||
|
let entry_coord = super::store_code_cell(
|
||||||
|
&mut store,
|
||||||
|
entry,
|
||||||
|
Kind::Fn,
|
||||||
|
"entry",
|
||||||
|
&[double_coord],
|
||||||
|
&entry_code,
|
||||||
|
)
|
||||||
|
.expect("store entry");
|
||||||
|
println!(" wrote {double} -> coord {}", double_coord.pack_u32());
|
||||||
|
println!(" wrote {entry} -> coord {}", entry_coord.pack_u32());
|
||||||
|
println!(
|
||||||
|
" entry links to double at coord {}",
|
||||||
|
double_coord.pack_u32()
|
||||||
|
);
|
||||||
|
|
||||||
|
// cubefs sees both as files in the same cube
|
||||||
|
let fs = cubefs::CubeFs::new(store.clone());
|
||||||
|
let root = fs.readdir("/").expect("readdir /");
|
||||||
|
println!(
|
||||||
|
" cubefs '/' lists: {}",
|
||||||
|
root.iter()
|
||||||
|
.map(|(n, _)| n.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
// load the entry cell the same way the CLI would, and run it
|
||||||
|
let cell = super::load_code_cell(&store, entry).expect("load entry");
|
||||||
|
let mut vm = Vm::new(store.clone());
|
||||||
|
let res = vm.run(cell.label);
|
||||||
|
println!(" VM ran {entry}: {res:?} (expected Halted 42)");
|
||||||
|
assert_eq!(res, cubecode::RunResult::Halted { top: Some(42) });
|
||||||
|
|
||||||
|
// ---- 2. cubecrypt: seal a record, then open + run it -----------------
|
||||||
|
println!("\n[2] cubecrypt: same coordinate, sealed + reopened");
|
||||||
|
// Key material lives in Null space (axis value 0) — not a cubefs path.
|
||||||
|
let key_cell = Czyx::new(0, 1, 0, 1);
|
||||||
|
store.put_record(
|
||||||
|
key_cell,
|
||||||
|
&cubecoords::CubeHeader::new(),
|
||||||
|
b"demo-key-material-32-bytes-long!!",
|
||||||
|
);
|
||||||
|
let env = CubeEnv::new(
|
||||||
|
vec![KeySlot {
|
||||||
|
key_cell,
|
||||||
|
transform: TransformId::Aes256Gcm,
|
||||||
|
salt: vec![],
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let sealed = "/c003/z001/y001/x005";
|
||||||
|
let sealed_coord = super::path_to_czyx(sealed).expect("parse sealed path");
|
||||||
|
// store plaintext bytecode, then seal the same coordinate
|
||||||
|
store.put_record(
|
||||||
|
sealed_coord,
|
||||||
|
&cubecoords::CubeHeader::new(),
|
||||||
|
&cubecode::encode(&[Op::Const(9), Op::Halt]),
|
||||||
|
);
|
||||||
|
env.put_encrypted(
|
||||||
|
&mut store,
|
||||||
|
sealed_coord,
|
||||||
|
Selector::Slot(0),
|
||||||
|
&cubecode::encode(&[Op::Const(9), Op::Halt]),
|
||||||
|
cubecoords::CubeHeader::new(),
|
||||||
|
)
|
||||||
|
.expect("seal");
|
||||||
|
let (h, _) = store.get_record(&sealed_coord).expect("read sealed");
|
||||||
|
println!(
|
||||||
|
" sealed {sealed}: header encrypted flag set = {}",
|
||||||
|
h.flags.has(cubecrypt::HEADER_FLAG_ENCRYPTED)
|
||||||
|
);
|
||||||
|
|
||||||
|
// the raw record is no longer readable as plaintext bytecode
|
||||||
|
let raw = store.get_record(&sealed_coord).unwrap().1;
|
||||||
|
let still_plain =
|
||||||
|
cubecode::CodeCell::from_record(sealed_coord, &cubecoords::CubeHeader::new(), &raw);
|
||||||
|
println!(
|
||||||
|
" raw record decodes as bytecode? {}",
|
||||||
|
still_plain.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
|
// open + decode + run on the shared store. The VM runs code located by
|
||||||
|
// coordinate, so to *execute* the sealed record we decrypt it back into a
|
||||||
|
// plaintext record at the same coord, then run.
|
||||||
|
let envelope = store.get_record(&sealed_coord).unwrap().1;
|
||||||
|
let pt = env
|
||||||
|
.open(&store, Selector::Slot(0), &envelope)
|
||||||
|
.expect("open");
|
||||||
|
let opened =
|
||||||
|
cubecode::CodeCell::from_record(sealed_coord, &cubecoords::CubeHeader::new(), &pt)
|
||||||
|
.expect("decode decrypted bytecode");
|
||||||
|
store.put_record(sealed_coord, &cubecoords::CubeHeader::new(), &pt);
|
||||||
|
let mut vm2 = Vm::new(store.clone());
|
||||||
|
let res2 = vm2.run(opened.label);
|
||||||
|
println!(" opened + ran {sealed}: {res2:?} (expected Halted 9)");
|
||||||
|
assert_eq!(res2, cubecode::RunResult::Halted { top: Some(9) });
|
||||||
|
|
||||||
|
println!("\n=== demo OK: one cube, three packages, same coordinates ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user