190 lines
7.0 KiB
Markdown
190 lines
7.0 KiB
Markdown
|
|
# 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.
|