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 (TeeParam — ParamMemref 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.

That pre-sizing has a consequence: an in/out buffer arrives at the TA zero-padded to MAX_SIZE, and a Memref’s single size field cannot say how much of it the CA’s encoding filled. Decoding the padding happens to work for postcard, which ignores trailing bytes, but not for codecs that reject them (protobuf treats a zero byte as an invalid tag). A &mut T slot therefore carries a 4-byte little-endian length ahead of the payload in both directions, and its buffer is consortium_tee::inout::capacity(MAX_SIZE) bytes. Input and output Memrefs need no frame: the CA sends exactly the encoded bytes in, and the TA’s set_output reports exactly the bytes written out.

On the TA, MAX_SIZE is also enforced on the way in: a serialized input whose Memref is larger than MAX_SIZE (or a &mut T frame larger than capacity(MAX_SIZE), or one declaring a longer payload) is BadParameters, checked before the TA copies the buffer out of shared memory.

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 support✅partial✅
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
    }
}
}