Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TEE Rust Macro System — Advanced Type System

Status

This document describes the advanced type system for #[tee_command]. Most features listed below are implemented; some remain planned.

What is already implemented

See tee-sdk-design.md for the baseline. Advanced types now fully implemented:

CategoryImplemented types
Input parametersbool, u32, i32, u64, (u32, u32), &[u8], &mut [u8], &mut u32, &mut i32, &mut u64, T: TeeParam
Return / outputu32, u64, bool, Vec<u8>, T: TeeParam
SerializationPluggable codec via #[tee_command(codec = MyCodec)]; any codec implementing consortium_cfg_common::codec::CodecFor<T>

What this document covers

  • i32, (u32, u32) value pairs ✅
  • &mut u32 / &mut u64 output parameters (inout slots) ✅
  • Arbitrary serializable types via TeeParam + pluggable codec system ✅

Architecture

┌────────────────────────────────────────────────────────────────┐
│  Developer writes one function signature                       │
│                                                                │
│  #[tee_command(codec = MyCodec)]  ← required if using TeeParam │
│  fn process(ctx: &mut Ctx, cfg: MyConfig) -> Result<()>        │
└────────────────────┬───────────────────────────────────────────┘
                     │  macro expands to
                     │
          ┌──────────┴───────────┐
          │                      │
          ▼                      ▼
  process_dispatched()      call_process()        [cfg(feature="ca")]
  (TA side)                 (CA side)
  unpack params             pack params
  → call process()          → invoke_command()

The original function is always emitted untouched, so internal TA-to-TA calls pay zero marshalling overhead.

The codec attribute is required when any parameter or return type implements TeeParam. It specifies which codec to use for serialization/deserialization of those types.


Parameter Classification

The macro classifies each parameter by its Rust type and maps it to a TEE parameter slot. A TEE function has at most 4 parameter slots.

Context Parameter

With #[tee_command(ctx)], the first parameter with type &mut SomePath (where SomePath is not a primitive or slice) is treated as the TA context. Explicit selectors such as ctx = name or ctx = 0 are accepted for compatibility and emit one warning; if the selected context is not the first parameter, the warning asks the user to move it first. The context is forwarded directly to the inner function and never packed into a TEE slot.


Planned Types

Additional Primitive Values (ParamValue slot)

Rust typeSlot kindEncodingStatus
u32ValueInputa = value, b = 0✅ done
boolValueInputa = value as u32 (0 or 1), b = 0✅ done
u64ValueInput/Returna = low 32, b = high 32✅ done
i32ValueInputa = value as u32, b = 0✅ done
(u32, u32)ValueInputa = .0, b = .1✅ done

Output Value Parameters (ValueInout slot)

A &mut T where T is a primitive scalar is treated as an output parameter. The TA writes a result back through the pointer; the CA reads it after invoke_command returns.

Rust typeSlot kindTA behaviourCA behaviourStatus
&mut u32ValueInoutreads a, writes back via set_areads a into *ptr after call✅ done
&mut i32ValueInoutreads a as i32, writes back castreads a as i32 into *ptr after call✅ done
&mut u64ValueInoutreconstructs from a/b, writes backreconstructs from a/b after call✅ done

Example

#![allow(unused)]
fn main() {
#[tee_command(ctx)]
fn compute(ctx: &mut Ctx, input: u32, result: &mut u64) -> Result<()> {
    *result = (input as u64) * 0xDEAD_BEEF;
    Ok(())
}

// CA side (generated):
// #[cfg(feature = "ca")]
// fn call_compute(input: u32, result: &mut u64) -> consortium_tee::TeeResult<()> {
//     let _p0 = ParamValue::new(input, 0, ParamType::ValueInput);
//     let _p1 = ParamValue::new(0u32, 0u32, ParamType::ValueInout);
//     let mut _params = Parameters::new(_p0, _p1, ParamNone, ParamNone);
//     invoke_command(Command::Compute, &mut _params)?;
//     *result = ((_params.param_1().as_value().b() as u64) << 32)
//              | (_params.param_1().as_value().a() as u64);
//     Ok(())
// }
}

Serialized Types (TeeParamParamMemref slot)

Any type implementing TeeParam can be passed through a memref slot. The macro serializes it using a pluggable codec specified via the #[tee_command(codec = ...)] attribute.

Derive TeeParam on your type to opt in:

#![allow(unused)]
fn main() {
#[derive(TeeParam, Serialize, Deserialize)]
struct MyConfig {
    threshold: u32,
    flags: u64,
    mode: OperationMode,
    label: heapless::String<32>,
}
}

The codec type must implement consortium_cfg_common::codec::CodecFor<T> for your serialization format (e.g., PostcardCodec for postcard).

Rust typeSlot kindTA behaviourCA behaviourStatus
T: TeeParamMemrefInputdeserialize from buffer with codecserialize to buffer, pass as memref✅ done
&mut T: TeeParamMemrefInoutdeserialize, pass &mut, re-serializeserialize, pass as memref, deserialize back✅ done

The TeeParam Trait

#![allow(unused)]
fn main() {
pub trait TeeParam {
    /// Maximum serialized size in bytes.
    /// Used to pre-size the inout buffer on the CA side.
    const MAX_SIZE: usize;
}
}

TeeParam is a codec-agnostic marker trait. Derive it on any type you want to pass through a memref slot. The actual serialization is handled by the codec you specify in #[tee_command(codec = ...)].

MAX_SIZE solves the inout buffer sizing problem: since the TA may return a value larger than what the CA sent, the CA must pre-allocate a buffer sized for the response. Defining it on the type means the size contract lives next to the type definition.

If #[derive(TeeParam)] is used without a manual impl, the macro uses a conservative default of 1024 bytes.


Slot Limit Enforcement

TEE permits at most 4 parameter slots per call (the context never counts as a slot). This is already enforced for current types via compile_error!. The enforcement will extend to cover all planned types.

If you exceed 4 slots, restructure parameters into a serialized struct instead:

#![allow(unused)]
fn main() {
// Too many slots
#[tee_command(ctx)]
fn bad(ctx: &mut Ctx, a: u32, b: u64, c: u32, d: &[u8], e: &mut [u8]) -> Result<()> { ... }
//                   [0]    [1]    [2]   [3]          [4]  ← 5 slots, compile error

// Pack logically related scalars into a struct
#[derive(TeeParam, Serialize, Deserialize)]
struct Params { a: u32, b: u64, c: u32 }

#[tee_command(ctx)]
fn good(ctx: &mut Ctx, params: Params, input: &[u8], output: &mut [u8]) -> Result<()> { ... }
//                     [0]             [1]            [2]   ← 3 slots
}

Type Classification Priority (full order)

When the macro inspects a parameter type, it will test these rules in order:

1. &[u8]              → MemrefInput
2. &mut [u8]          → MemrefInout
3. &mut u32/i32/u64   → ValueInout output
4. u32                → ValueInput (a)
5. i32                → ValueInput (a, bit-cast)
6. bool               → ValueInput (a, 0/1)
7. u64                → ValueInput (a=low, b=high)
8. (u32, u32)         → ValueInput (a=.0, b=.1)
9. &mut T (Path)      → MemrefInout (TeeParam, codec round-trip)
10. T (Path)          → MemrefInput (TeeParam, codec deserialize)
11. anything else     → compile error

All rules are now implemented. Rules 9–10 require #[tee_command(codec = MyCodec)] to be specified.


Codec Selection and Requirements

Pluggable Codec System

The #[tee_command(codec = MyCodec)] attribute lets you choose how TeeParam types are serialized. Your codec type must implement consortium_cfg_common::codec::CodecFor<T> for each TeeParam type you use.

Example: Using PostcardCodec

#![allow(unused)]
fn main() {
use consortium_cfg_common::codec::PostcardCodec;

#[derive(TeeParam, Serialize, Deserialize)]
struct Config {
    threshold: u32,
    enabled: bool,
}

#[tee_command(codec = PostcardCodec)]
fn process(ctx: &mut Ctx, cfg: Config) -> Result<(), TeeError> {
    // ...
}
}

Considerations for Codec Choice

PropertypostcardbincodeCustom
no_std supportpartial
Output sizeminimalsmallvaries
Allocation-free mode
Schema evolutionmanualmanualcustom

Schema Evolution Warning

Most binary codecs (including postcard and bincode) are not self-describing. If you change a TeeParam type (add/remove/reorder fields), both CA and TA must be recompiled together to stay in sync.


Codec Implementation Reference

To implement a custom codec for your type T:

#![allow(unused)]
fn main() {
impl consortium_cfg_common::codec::CodecFor<MyType> for MyCodec {
    type Error = MyError;
    type Decoded<'buf> = MyType;

    fn encode(msg: &MyType, buf: &mut [u8]) -> Result<usize, Self::Error> {
        // Serialize msg into buf, return number of bytes written
    }

    fn decode(buf: &[u8]) -> Result<Self::Decoded, Self::Error> {
        // Deserialize from buf
    }
}
}