Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

- Register the additive Latch platform-context TLVs (16–22), including RV64
CPU state, retained blackbox records, mission, time synchronization,
provisioning, supervisor, and environment evidence.
- Add a golden vector covering the new context TLVs.

## 1.0.0

- LEP v1 wire format, framing, crypto device path, TLV registry 1–15
Expand Down
10 changes: 10 additions & 0 deletions implementations/go/lep/lep.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ const (
KnownFlags = FlagAuthenticated | FlagEncrypted | FlagAEAD | FlagTruncated | FlagCompressed
)

const (
TLVCPU64 uint16 = 16
TLVBlackbox uint16 = 17
TLVMission uint16 = 18
TLVTimeSync uint16 = 19
TLVProvisioning uint16 = 20
TLVSupervisor uint16 = 21
TLVEnvironment uint16 = 22
)

type ErrorKind string

const (
Expand Down
48 changes: 48 additions & 0 deletions implementations/go/lep/lep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,54 @@ func TestGoldenValid(t *testing.T) {
}
}

func TestGoldenLatchPlatformContexts(t *testing.T) {
raw := loadHex(t, "valid/latch-platform-contexts.hex")
h, err := lep.Validate(raw)
if err != nil {
t.Fatal(err)
}
if h.Architecture != 5 || h.Sequence != 29 || h.EventID != 30 {
t.Fatalf("unexpected header: %+v", h)
}
tlvs, err := lep.ParseTLVs(raw[lep.HeaderSize : lep.HeaderSize+int(h.PayloadLength)])
if err != nil {
t.Fatal(err)
}
want := []uint16{
lep.TLVCPU64, lep.TLVBlackbox, lep.TLVMission, lep.TLVTimeSync,
lep.TLVProvisioning, lep.TLVSupervisor, lep.TLVEnvironment,
}
if len(tlvs) != len(want) {
t.Fatalf("got %d TLVs, want %d", len(tlvs), len(want))
}
for index, typ := range want {
if tlvs[index].Type != typ {
t.Fatalf("TLV %d: got %d, want %d", index, tlvs[index].Type, typ)
}
}
if got := tlvs[0].Value; len(got) != 4 || got[0] != 1 || got[1] != 2 || got[2] != 5 || got[3] != 8 {
t.Fatalf("unexpected CPU64 unavailable descriptor: %x", got)
}
}

func TestGoldenLatchCPU64Complete(t *testing.T) {
raw := loadHex(t, "valid/latch-cpu64-complete.hex")
h, err := lep.Validate(raw)
if err != nil {
t.Fatal(err)
}
tlvs, err := lep.ParseTLVs(raw[lep.HeaderSize : lep.HeaderSize+int(h.PayloadLength)])
if err != nil {
t.Fatal(err)
}
if len(tlvs) != 1 || tlvs[0].Type != lep.TLVCPU64 || len(tlvs[0].Value) != 292 {
t.Fatalf("unexpected CPU64 TLV: %+v", tlvs)
}
if got := tlvs[0].Value[:4]; got[0] != 1 || got[1] != 1 || got[2] != 5 || got[3] != 8 {
t.Fatalf("unexpected CPU64 complete descriptor: %x", got)
}
}

func TestRoundTrip(t *testing.T) {
payload, _ := hex.DecodeString("01000200aabb")
raw, err := lep.Encode(lep.Header{Version: 1, Type: 2, Sequence: 7, EventID: 9}, payload)
Expand Down
68 changes: 67 additions & 1 deletion registry/tlv-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ type u16 LE | length u16 LE | value[length]
| 13 | MEMORY | yes |
| 14 | STACK | no |
| 15 | HEAP | no |
| 0x0010–0x0013 | build/project/release/hash extensions | — |
| 16 | CPU64 | no |
| 17 | BLACKBOX | yes |
| 18 | MISSION | no |
| 19 | TIME_SYNC | no |
| 20 | PROVISIONING | no |
| 21 | SUPERVISOR | no |
| 22 | ENVIRONMENT | no |
| 0x0020–0x0021 | attachment meta/chunk | yes (chunk) |
| 0x0030 | probe waveform (reserved) | — |
| 0x8000–0x8FFF | vendor | — |
Expand Down Expand Up @@ -145,3 +151,63 @@ flags: SAFE, HASH, VOLATILE, SENSITIVE.
## 15 HEAP (20 bytes)

`free_bytes | minimum_free | largest_block | allocation_failures | pool_exhaustions`

## 16 CPU64

`encoding u8 | flags u8 | architecture u8 | word_size u8`

Encoding is `1`, architecture is `5` (`RISCV64`) and word size is `8`.
Exactly one flag is set: `COMPLETE` (`0x01`) or `UNAVAILABLE` (`0x02`). A
complete value is 292 bytes and appends `x0..x31`, `mstatus`, `mcause`,
`mtval`, and `mepc` as 36 little-endian `u64` values. An unavailable value is
exactly four bytes; consumers MUST NOT infer upper words from TLV 4 (`CPU`).

## 17 BLACKBOX (27 bytes, repeatable)

`encoding u8 | timestamp_ms u32 | kind u16 | source_id u16 | flags u16 |
value[4] i32`

Encoding is `1`. A producer emits records oldest to newest within the bounded
export window and omits records marked sensitive.

## 18 MISSION (46 bytes plus optional strings)

`encoding u8 | mission_hash u32 | dive_hash u32 | node_hash u32 |
vehicle_mode_hash u32 | phase u32 | depth_cm i32 | elapsed_ms u32 |
incident_hi u64 | incident_lo u64 | incident_active u8`

Encoding is `1`. If strings are retained, zero or more suffix fields follow as
`field_id u8 | length u8 | UTF-8 bytes`: 1 `mission_id`, 2 `dive_id`, 3
`node_id`, 4 `vehicle_mode`.

## 19 TIME_SYNC (22 bytes)

`encoding u8 | source u8 | utc_ms_at_sync u64 | monotonic_ms_at_sync u32 |
uncertainty_ms u32 | generation u32`

Encoding is `1`. Source values are 1 RTC, 2 GNSS, 3 NTP, 4 PTP, and 5 HOST.

## 20 PROVISIONING (18 bytes)

`encoding u8 | state u8 | key_id u32 | pending_key_id u32 | generation u32 |
monotonic_counter u32`

Encoding is `1`. State values are 0 unprovisioned, 1 active, 2 rotating, 3
revoked, 4 decommissioned, and 5 decommissioning.

## 21 SUPERVISOR (17 bytes)

`encoding u8 | active_alarms u32 | previous_alarms u32 | transitions u32 |
last_change_ms u32`

Encoding is `1`. Alarm bit definitions are product policy; consumers preserve
unknown bits.

## 22 ENVIRONMENT (29 bytes)

`encoding u8 | timestamp_ms u32 | pressure_pa u32 | depth_cm i32 |
internal_temperature_c i16 | humidity_permyriad u16 | vibration_mg_rms u16 |
flags u16 | sample_count u32 | leak_events u32`

Encoding is `1`. Environment flags are bit 0 leak detected, bit 1 water
ingress, bit 2 pressure-sensor fault, and bit 3 vibration limit.
15 changes: 11 additions & 4 deletions spec/lep-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,25 @@ Assigned types: [`registry/tlv-types.md`](../registry/tlv-types.md) and [`identi
| 1 | `0x02` | `ENCRYPTED` | Payload is ciphertext |
| 2 | `0x04` | `AEAD` | 28-byte AEAD metadata present |
| 3 | `0x08` | `TRUNCATED` | Sender omitted optional fields due to capacity |
| 4–7 | | reserved | MUST be zero on send; unknown set bits MUST be rejected |
| 4 | `0x10` | `COMPRESSED` | Payload is compressed; see §5.2 |
| 5–7 | | reserved | MUST be zero on send; unknown set bits MUST be rejected |

### 5.1 Flag combination rules

1. `ENCRYPTED` and `AEAD` MUST both be set or both clear.
2. If `AEAD` is set, `AUTHENTICATED` MUST also be set.
3. `TRUNCATED` MAY combine with any valid security combination.
4. Receivers MUST reject unknown flag bits (v1: any bit outside `0x0F`).
4. `COMPRESSED` MAY combine with any valid security combination. A receiver
that does not implement the selected compression codec MUST reject the
envelope before interpreting its payload.
5. Receivers MUST reject unknown flag bits (v1: any bit outside `0x1F`).

### 5.2 Informative — historical compression flag collision
### 5.2 Compressed payloads

Some Relay builds treated bit 3 as `COMPRESSED` instead of `TRUNCATED`. That collides with Latch (the primary producer). **Canonical v1 bit 3 is `TRUNCATED`.** Compression, when standardized, uses a **different** mechanism (see [`compression.md`](compression.md) and [`compatibility.md`](compatibility.md)).
`COMPRESSED` is bit 4; `TRUNCATED` remains bit 3. The compressed payload is
opaque until decompressed successfully, so receivers MUST NOT parse TLVs or
make routing decisions from it first. Compression selection and bounds are
defined in [`compression.md`](compression.md).

---

Expand Down
10 changes: 10 additions & 0 deletions test-vectors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
"path": "valid/lep-v1-basic.hex",
"expect": "valid"
},
{
"id": "valid-latch-platform-contexts",
"path": "valid/latch-platform-contexts.hex",
"expect": "valid"
},
{
"id": "valid-latch-cpu64-complete",
"path": "valid/latch-cpu64-complete.hex",
"expect": "valid"
},
{
"id": "invalid-magic",
"path": "invalid/bad-magic.hex",
Expand Down
1 change: 1 addition & 0 deletions test-vectors/valid/latch-cpu64-complete.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4c535450010105001f0000002000000028010000258918b1100024010101050800000000000000000100000000000000020000000000000003000000000000000400000000000000050000000000000006000000000000000700000000000000080000000000000009000000000000000a000000000000000b000000000000000c000000000000000d000000000000000e000000000000000f0000000000000010000000000000001100000000000000120000000000000013000000000000001400000000000000150000000000000016000000000000001700000000000000180000000000000019000000000000001a000000000000001b000000000000001c000000000000001d000000000000001e000000000000001f000000000000002000000000000000210000000000000022000000000000002300000000000000e2cdc85a
1 change: 1 addition & 0 deletions test-vectors/valid/latch-platform-contexts.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4c535450010205001d0000001e000000bf000000a68f4cfd100004000102050811001b00010403020105060708000001000000feffffff030000000400000012002e00011111111122222222333333334444444405000000faffffff0700000008000000000000000900000000000000011300160001020a000000000000000b0000000c0000000d0000001400120001010e000000000000000f0000001000000015001100011100000012000000130000001400000016001d00011500000016000000e9ffffffe8ff19001a0007001b0000001c0000009433e936
Loading