Skip to content

Commit 26d5dc8

Browse files
committed
Merge branch 'feature/golden-vector-matrix'
2 parents 619aa79 + 832e9a7 commit 26d5dc8

22 files changed

Lines changed: 446 additions & 21 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Golden vectors
2+
3+
# Rebuilds official libcsp v2.1 (commit 48f7fb0) from source, regenerates every
4+
# committed CAN capture under tests/golden_vectors/, and fails if the fresh
5+
# capture differs from what is committed. This catches an upstream libcsp
6+
# change (or a moved v2.1 tag) even when no local change triggers the run.
7+
on:
8+
schedule:
9+
- cron: "0 3 * * *" # nightly, 03:00 UTC
10+
push:
11+
branches: [main]
12+
paths:
13+
- "tests/golden_vectors/**"
14+
- "Dockerfile"
15+
- "docker-compose.yml"
16+
pull_request:
17+
paths:
18+
- "tests/golden_vectors/**"
19+
- "Dockerfile"
20+
- "docker-compose.yml"
21+
workflow_dispatch: {}
22+
23+
jobs:
24+
regenerate-and-diff:
25+
runs-on: ubuntu-latest
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Build the libcsp golden-vector image
32+
run: docker compose build
33+
34+
- name: Start the golden-vector container
35+
run: docker compose up -d
36+
37+
- name: Regenerate every committed vector from a fresh libcsp build
38+
run: docker compose exec -T libcsp-vectors /app/tests/golden_vectors/scripts/regenerate-vectors.sh
39+
40+
- name: Stop the golden-vector container
41+
if: always()
42+
run: docker compose down
43+
44+
- name: Fail if regenerated vectors differ from committed fixtures
45+
run: |
46+
if ! git diff --exit-code -- tests/golden_vectors/*.txt; then
47+
echo "::error::Freshly captured CAN traffic from libcsp v2.1 (48f7fb0) no longer matches the committed golden vectors."
48+
echo "::error::Either upstream libcsp changed on-wire behavior, or a committed fixture is stale. Investigate before merging."
49+
exit 1
50+
fi

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ still pre-release.
1616
- Deterministic discrete-event simulation engine with virtual time.
1717
- libcsp-compatible CSP v2 single-frame codec, validated against
1818
repository-owned golden vectors generated from official libcsp `v2.1`.
19-
- Dockerized golden-vector generation workflow (`vcan0` + `candump`).
19+
- Dockerized golden-vector generation workflow (`vcan0` + `candump`), with a
20+
committed vector matrix covering priority, address truncation, payload
21+
bounds, CRC32/HMAC flags, and reversed source/destination, plus a nightly
22+
CI job that rebuilds libcsp and fails on drift from the committed fixtures.
2023
- In-memory transport adapter for CI/tests and a SocketCAN adapter for
2124
Linux/HIL.
2225
- Pydantic schemas for TOML setup config and YAML scenario scripts.

tests/golden_vectors/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@ The container workflow uses `vcan0` consistently.
4848
/app/tests/golden_vectors/bin/csp_client -c vcan0 -p -d 2
4949
```
5050

51+
The helper's full option set (`-p -h` for usage) covers the committed
52+
vector matrix:
53+
54+
```text
55+
-a <source-address> CSP source/interface address (default: 1)
56+
-d <destination-address> CSP destination address (default: 2)
57+
-r <priority> CSP priority 0-3: 0=critical 1=high 2=norm 3=low
58+
-l <payload-length> payload bytes, 0-4; incrementing pattern from 0x55
59+
-o <flag[,flag...]> comma-separated CSP options: crc32, rdp, hmac
60+
```
61+
62+
`-o rdp` cannot be captured this way: RDP is a stateful, connection-oriented
63+
protocol that blocks on a SYN handshake with no peer on the bus to answer
64+
it. The RDP flag bit is covered by a synthetic pack/decode round trip in
65+
`tests/test_csp_v2.py` instead of a captured vector. `crc32`/`hmac` append
66+
their own trailer to the payload, so use `-l 0` with them to stay within
67+
the v1 single-frame payload limit (a 1-byte payload plus a 4-byte trailer
68+
would push libcsp into multi-frame transmission, which is out of v1 scope).
69+
5170
5. `candump -n 1` exits after the first received frame. Commit the resulting
5271
fixture files under `tests/golden_vectors/`.
5372

@@ -60,6 +79,40 @@ The container workflow uses `vcan0` consistently.
6079
6. Commit a sibling metadata file next to each dump, for example
6180
`ping.meta.toml` for `ping.txt`.
6281

82+
To regenerate every committed vector at once (for example, to check for
83+
upstream libcsp drift before merging a codec change), run the same script CI
84+
uses instead of repeating steps 3-5 by hand:
85+
86+
```sh
87+
docker compose exec -T libcsp-vectors /app/tests/golden_vectors/scripts/regenerate-vectors.sh
88+
```
89+
90+
Then inspect `git diff -- tests/golden_vectors/*.txt`. An empty diff means
91+
libcsp's on-wire behavior still matches every committed fixture.
92+
93+
## Committed vector matrix
94+
95+
| Vector | Covers |
96+
| --- | --- |
97+
| `ping_node_2` (`ping.txt`) | Baseline: `src=1 dst=2 prio=NORM flags=none payload=1B` |
98+
| `priority_critical` | `prio=0` (CRITICAL), the low end of the 2-bit priority field |
99+
| `priority_low` | `prio=3` (LOW), the high end of the 2-bit priority field |
100+
| `high_address` | `src=65 dst=66`: the CAN-ID `sender` field is only 6 bits (libcsp uses `interface_address & 0x3F`), so this is the only committed vector where `sender != source` -- every other vector uses addresses below 64, where they are trivially equal |
101+
| `payload_empty` | 0-byte application payload, the lower bound of the v1 single-frame payload range |
102+
| `payload_max` | 4-byte application payload, the upper bound of the v1 single-frame payload range |
103+
| `flag_crc32` | `CSP_FLAG_CRC32` set |
104+
| `flag_hmac` | `CSP_FLAG_HMAC` set |
105+
| `reverse_direction` | `src=2 dst=1`: the same node pair as the baseline, addressed the other way |
106+
107+
`CSP_FLAG_RDP` has no committed vector (see the generation-workflow note
108+
above); it is covered by a synthetic pack/decode round trip instead.
109+
110+
Regenerating every vector and diffing against what is committed is required
111+
before merging any change to `tests/golden_vectors/src/csp_client.c` or
112+
`src/cubesat_testbed/protocol/csp_v2.py`, and also runs nightly in CI (see
113+
`.github/workflows/golden-vectors.yml`) so an upstream libcsp change (or a
114+
moved `v2.1` tag) is caught even without a local change triggering it.
115+
63116
## Fixture contract
64117

65118
Every traffic dump must have a sibling `*.meta.toml` file. For example:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name = "flag_crc32"
2+
capture = "flag_crc32.txt"
3+
libcsp_repo = "https://github.com/libcsp/libcsp"
4+
libcsp_tag = "v2.1"
5+
libcsp_commit = "48f7fb0"
6+
command = "/app/tests/golden_vectors/bin/csp_client -c vcan0 -p -d 2 -o crc32 -l 0"
7+
capture_command = "candump -n 1 vcan0 > /app/tests/golden_vectors/flag_crc32.txt"
8+
interface = "vcan0"
9+
meaning = "Single-frame CSP v2 ping-port request from node 1 to node 2 with the CSP_FLAG_CRC32 header bit set and a zero-byte application payload (libcsp appends its own 4-byte CRC32 trailer to the payload; using a 1-byte application payload here would push the packet over the v1 single-frame limit and into multi-frame libcsp transmission, which is out of v1 scope). Covers the CRC32 flag bit; the testbed's v1 codec treats CSP flags as an opaque bitmask and does not itself compute or verify the CRC."
10+
protocol = "CSP v2 over classic CAN 2.0B extended frame"
11+
source_address = 1
12+
destination_address = 2
13+
expected_flags = 1
14+
expected_extended_can_id = "0x10004083"
15+
expected_payload_hex = "00 04 14 81 00 00 00 00"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vcan0 10004083 [8] 00 04 14 81 00 00 00 00
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name = "flag_hmac"
2+
capture = "flag_hmac.txt"
3+
libcsp_repo = "https://github.com/libcsp/libcsp"
4+
libcsp_tag = "v2.1"
5+
libcsp_commit = "48f7fb0"
6+
command = "/app/tests/golden_vectors/bin/csp_client -c vcan0 -p -d 2 -o hmac -l 0"
7+
capture_command = "candump -n 1 vcan0 > /app/tests/golden_vectors/flag_hmac.txt"
8+
interface = "vcan0"
9+
meaning = "Single-frame CSP v2 ping-port request from node 1 to node 2 with the CSP_FLAG_HMAC header bit set and a zero-byte application payload (libcsp appends its own 4-byte HMAC trailer to the payload; using a 1-byte application payload here would push the packet over the v1 single-frame limit and into multi-frame libcsp transmission, which is out of v1 scope). Covers the HMAC flag bit; the testbed's v1 codec treats CSP flags as an opaque bitmask and does not itself compute or verify the HMAC."
10+
protocol = "CSP v2 over classic CAN 2.0B extended frame"
11+
source_address = 1
12+
destination_address = 2
13+
expected_flags = 8
14+
expected_extended_can_id = "0x10004083"
15+
expected_payload_hex = "00 04 14 88 FB DB 1D 1B"

tests/golden_vectors/flag_hmac.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vcan0 10004083 [8] 00 04 14 88 FB DB 1D 1B
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name = "high_address"
2+
capture = "high_address.txt"
3+
libcsp_repo = "https://github.com/libcsp/libcsp"
4+
libcsp_tag = "v2.1"
5+
libcsp_commit = "48f7fb0"
6+
command = "/app/tests/golden_vectors/bin/csp_client -c vcan0 -p -a 65 -d 66"
7+
capture_command = "candump -n 1 vcan0 > /app/tests/golden_vectors/high_address.txt"
8+
interface = "vcan0"
9+
meaning = "Single-frame CSP v2 ping-port request from node 65 to node 66. The CSP v2 CAN ID's 'sender' field is only 6 bits wide (libcsp uses the low 6 bits of the outgoing interface address), so at source=65 it diverges from the full 14-bit CSP source address (sender = 65 & 0x3F = 1, not 65): the only committed vector where these two fields are not numerically identical, since every other vector uses source/destination addresses below 64."
10+
protocol = "CSP v2 over classic CAN 2.0B extended frame"
11+
source_address = 65
12+
destination_address = 66
13+
expected_sender = 1
14+
expected_extended_can_id = "0x10084083"
15+
expected_payload_hex = "01 04 14 80 55"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vcan0 10084083 [5] 01 04 14 80 55
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name = "payload_empty"
2+
capture = "payload_empty.txt"
3+
libcsp_repo = "https://github.com/libcsp/libcsp"
4+
libcsp_tag = "v2.1"
5+
libcsp_commit = "48f7fb0"
6+
command = "/app/tests/golden_vectors/bin/csp_client -c vcan0 -p -d 2 -l 0"
7+
capture_command = "candump -n 1 vcan0 > /app/tests/golden_vectors/payload_empty.txt"
8+
interface = "vcan0"
9+
meaning = "Single-frame CSP v2 ping-port request from node 1 to node 2 with a zero-byte application payload: only the 4-byte CSP v2 header extension is carried, covering the lower bound of the v1 single-frame payload range."
10+
protocol = "CSP v2 over classic CAN 2.0B extended frame"
11+
source_address = 1
12+
destination_address = 2
13+
expected_extended_can_id = "0x10004083"
14+
expected_payload_hex = "00 04 14 80"

0 commit comments

Comments
 (0)