Files
Shade/docs/cross-platform.md

190 lines
7.0 KiB
Markdown
Raw Normal View History

# Cross-platform parity — adding & running vectors
Shade keeps its TypeScript and Kotlin implementations in lock-step via a
**single source of truth**: `test-vectors/*.json`. Both runners load the
same files and verify their native code produces byte-identical output.
This document covers:
1. How the parity gate works (CI)
2. How to run vectors locally
3. How to add a new vector
## How the gate works
```
┌─────────────────────────────────┐
│ scripts/generate-vectors.ts │
│ (TS reference implementation) │
└────────────────┬────────────────┘
│ writes
┌─────────────────────────────────┐
│ test-vectors/*.json │
│ { version: 2, vectors: [...] }│
└─────┬──────────────────┬────────┘
│ │
│ loaded by │ loaded by
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ packages/shade-core/ │ │ android/shade-android/ │
│ tests/cross-platform- │ │ src/test/kotlin/.../ │
│ vectors.test.ts │ │ CrossPlatformVectorTest │
│ (bun) │ │ (gradle JUnit4) │
└───────────────────────────┘ └───────────────────────────┘
│ │
└─────────┬────────┘
both must pass before merge
(.gitea/workflows/cross-vectors.yml)
```
The CI workflow has **two independent jobs**`ts-vectors` and
`kotlin-vectors`. Either failing blocks the merge. The TS job also runs
`bun run vectors:gen` and fails if the result diverges from the committed
files: vector commits must come from the generator, never hand edits.
Vector files have a `version` integer at the top. Bump
`VECTOR_FILE_VERSION` in `scripts/generate-vectors.ts` whenever the
**schema** of any vector file changes (not just the values). Both test
suites assert the version matches their hard-coded expectation.
## Running vectors locally
### TypeScript
```bash
bun run test:vectors
# under the hood:
# bun test packages/shade-core/tests/cross-platform-vectors.test.ts
```
### Kotlin (JVM, no Android SDK required)
```bash
cd android
./gradlew :shade-android:test
```
Requires JDK 17. The wrapper downloads Gradle 8.10.2 on first run. Tink
1.15.0 (JVM JAR) is pulled from Maven Central.
### Regenerating vectors
When the protocol changes (new wire field, new label, new derivation step)
the TS reference is the source of truth. Edit `generate-vectors.ts`, then:
```bash
bun run vectors:gen
git diff test-vectors/ # eyeball the change
bun run test:vectors # confirm TS still agrees
cd android && ./gradlew :shade-android:test # confirm Kotlin still agrees
```
If Kotlin disagrees, **fix Kotlin** — TS is canonical. If both agree but
the diff is unintentional (e.g. you added a field by accident), revert
the generator change.
## Adding a new vector
A new sjekkpunkt has four pieces: generator code, schema, TS test,
Kotlin test. All four must land in the same PR; otherwise the gate
trips on the missing half.
### Step 1 — Add a generator function
In `scripts/generate-vectors.ts`, add a function that:
- Takes deterministic inputs (no randomness — fix every byte)
- Computes the value via the TS reference primitives
- Returns a `Vector[]` with a `description` per case + all inputs and outputs
in hex
Example skeleton:
```ts
async function generateMyVectors(): Promise<Vector[]> {
const input = new Uint8Array(32).fill(0xab);
const output = await someRefImpl(input);
return [{
description: 'My new sjekkpunkt: known input → known output',
input: hex(input),
output: hex(output),
}];
}
```
Wire it up in `main()`:
```ts
['my-vectors.json', { vectors: await generateMyVectors() }],
```
Run `bun run vectors:gen` → you should see `✓ my-vectors.json` and a new
file appears under `test-vectors/`.
### Step 2 — Add a TS test
In `packages/shade-core/tests/cross-platform-vectors.test.ts`:
```ts
test('My vectors match', async () => {
const { vectors } = loadVectors('my-vectors.json');
for (const v of vectors) {
const actual = await someRefImpl(fromHex(v.input));
expect(hex(actual)).toBe(v.output);
}
});
```
`loadVectors` already asserts the version field matches. If you're
introducing a schema-breaking change, bump `EXPECTED_VECTOR_VERSION` and
`VECTOR_FILE_VERSION` together.
### Step 3 — Add the Kotlin equivalent
In
`android/shade-android/src/test/kotlin/no/zyon/shade/CrossPlatformVectorTest.kt`:
```kotlin
@Test
fun myVectorsMatch() {
val vectors = loadVectors("my-vectors.json")
for (i in 0 until vectors.length()) {
val v = vectors.getJSONObject(i)
val actual = someKotlinImpl(fromHex(v.getString("input")))
assertEquals(v.getString("output"), hex(actual))
}
}
```
If the Kotlin port doesn't yet have `someKotlinImpl`, that's the implementation
work the new vector is gating — write it and re-run the test until it passes.
### Step 4 — Verify the gate trips on divergence
Sanity check: temporarily flip a byte in your Kotlin port and run
`./gradlew :shade-android:test`. The test should fail within 60 seconds
(see `docs/V3.5.md` §Akseptansekriterier). Revert.
## Why a separate generator (vs. golden fixtures)?
Golden test fixtures rot — when the protocol changes, every test file
that pinned a literal hex string needs updating, and it's easy to
"update" Kotlin to match a stale TS-generated value. By centralising
vector generation in one TS script, **the protocol changes in one
place** (the reference impl + `generate-vectors.ts`), the file
regenerates with one command, and any platform that drifts gets caught
by the next CI run.
## Schema versioning
`{ "version": 2, "vectors": [...] }` is the file format. Bump the int
when the **shape** of any vector changes (e.g. you add a field consumers
must read). Both runners hard-code their expected version and refuse to
parse mismatched files — this catches the case where a new vector field
was added in TS but the Kotlin loader silently ignored it.
Schema changes go in the same PR as the bump + the matching loader
update on both sides.