mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
test(marmot): run the reference implementation's own fixtures
Our Marmot tests agreed with nobody but themselves. They were written from the same reading of the spec as the code they test, so a parser that read a tag differently from every other client would pass all of them. MDK ships fixtures built for exactly this. `fixtures/encrypted-media/ imeta-v2.json` says so in its own description: "Shared by marmot-app, marmot-uniffi, and wn-cli tests so every layer agrees on validation verdicts and exact wire round-trips." We are another layer and were not using it. Same for the byte-level component vectors under `cgka-conformance-simulator/vectors/byte-fixtures/`, whose manifest marks 31 of its 41 artifacts `"status": "portable"`. Copied verbatim and wired to our codecs: - **10 imeta v2 cases** — 5 golden, 5 rejections. The rejections are the half that matters: a merely lenient parser passes every golden case and still cannot be interoperated with, because it accepts tags a conformant sender never emits and then renders media another client refuses. The fixture also distinguishes an absent hint from a present-but-empty one, which is a real wire distinction we now assert rather than assume. - **10 imeta v1 cases as NEGATIVE cases.** `0x8008` is frozen and "MUST NOT be reinterpreted as v2"; the two share enough field layout that a parser keying only on fields would read one as the other and derive a file key under the wrong scheme. Every v1 case now has to bounce off the v2 parser, including the ones v1 itself calls valid. - **3 nostr-routing byte fixtures.** These are the first tests we have that pin a component's wire bytes against another implementation instead of against our own encoder — a round trip proves we can read what we wrote, which is a different and much weaker claim. The invalid fixture is the sharp one: a decoder that deduplicated the relay list rather than refusing it would hold bytes no peer agrees with. All 8 tests pass unmodified, so this is coverage rather than a fix — but it is coverage that can now fail for a reason our own tests never could. The fixtures carry a README with their provenance and a refresh command, because a copied artifact drifts silently. It also records what is still missing: the 19 portable scenario vectors need a runner that drives our client through a scripted trace and projects state per `foundation/conformance.md`, and that is where the convergence and crash/restart coverage lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kCuA6tc4JQzHPCDd39GHq
This commit is contained in:
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.marmot.conformance
|
||||
|
||||
import com.vitorpamplona.quartz.TestResourceLoader
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaV2
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
/**
|
||||
* The reference implementation's own `imeta` fixtures, run against our parser.
|
||||
*
|
||||
* These are `mdk/fixtures/encrypted-media/imeta-{v1,v2}.json`, copied verbatim.
|
||||
* The file says what they are for: "Shared by marmot-app, marmot-uniffi, and
|
||||
* wn-cli tests so every layer agrees on validation verdicts and exact wire
|
||||
* round-trips." We are another layer, and until now we agreed with nobody but
|
||||
* ourselves — our own tests would happily bless a parser that read the tag
|
||||
* differently from every other client, because they were written from the same
|
||||
* reading of the spec as the parser.
|
||||
*
|
||||
* The interesting half is the rejections. A parser that is merely lenient
|
||||
* passes every golden case and still cannot be interoperated with: it accepts
|
||||
* tags a conformant sender never emits, so it silently renders media that
|
||||
* another client refuses, and the two disagree about what the group contains.
|
||||
*/
|
||||
class EncryptedMediaImetaVectorTest {
|
||||
private fun fixture(name: String) = Json.parseToJsonElement(TestResourceLoader().loadString("marmot/conformance/$name")).jsonObject
|
||||
|
||||
private val v2 by lazy { fixture("imeta-v2.json") }
|
||||
private val v1 by lazy { fixture("imeta-v1.json") }
|
||||
|
||||
private fun cases(fixture: kotlinx.serialization.json.JsonObject) = fixture["cases"]!!.jsonArray.map { it.jsonObject }
|
||||
|
||||
private fun tagOf(case: kotlinx.serialization.json.JsonObject) = case["tag"]!!.jsonArray.map { it.jsonPrimitive.content }.toTypedArray()
|
||||
|
||||
private fun nameOf(case: kotlinx.serialization.json.JsonObject) = case["name"]!!.jsonPrimitive.content
|
||||
|
||||
@Test
|
||||
fun everyGoldenV2CaseParsesToExactlyTheExpectedFields() {
|
||||
val golden = cases(v2).filter { it["valid"]!!.jsonPrimitive.content == "true" }
|
||||
assertTrue(golden.size >= 5, "expected the full golden set, got ${golden.size}")
|
||||
|
||||
for (case in golden) {
|
||||
val name = nameOf(case)
|
||||
val reference =
|
||||
try {
|
||||
EncryptedMediaV2.parseImetaTag(tagOf(case))
|
||||
} catch (e: Exception) {
|
||||
fail("golden case '$name' was rejected: ${e.message}")
|
||||
}
|
||||
val expected = case["expected"]!!.jsonObject
|
||||
|
||||
assertEquals(
|
||||
expected["locators"]!!.jsonArray.map {
|
||||
it.jsonObject["kind"]!!.jsonPrimitive.content to it.jsonObject["value"]!!.jsonPrimitive.content
|
||||
},
|
||||
reference.locators.map { it.kind to it.value },
|
||||
"$name locators (order is the producer's and is preserved)",
|
||||
)
|
||||
assertEquals(expected["ciphertext_sha256"]!!.jsonPrimitive.content, reference.ciphertextSha256.toHexKey(), "$name ciphertext_sha256")
|
||||
assertEquals(expected["plaintext_sha256"]!!.jsonPrimitive.content, reference.plaintextSha256.toHexKey(), "$name plaintext_sha256")
|
||||
assertEquals(expected["nonce_hex"]!!.jsonPrimitive.content, reference.nonce.toHexKey(), "$name nonce")
|
||||
assertEquals(expected["media_type"]!!.jsonPrimitive.content, reference.mediaType, "$name m")
|
||||
assertEquals(expected["file_name"]!!.jsonPrimitive.content, reference.filename, "$name filename")
|
||||
|
||||
// The fixture distinguishes absent (null) from present-but-empty
|
||||
// (""), and so must we: a hint that was written and left blank is
|
||||
// not the same wire state as one that was never written, and
|
||||
// collapsing them changes what a re-encode emits.
|
||||
assertEquals(optional(expected, "dim"), reference.dim, "$name dim")
|
||||
assertEquals(optional(expected, "thumbhash"), reference.thumbhash, "$name thumbhash")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun everyRejectionV2CaseIsRejected() {
|
||||
val rejections = cases(v2).filter { it["valid"]!!.jsonPrimitive.content == "false" }
|
||||
assertTrue(rejections.size >= 5, "expected the full rejection set, got ${rejections.size}")
|
||||
|
||||
for (case in rejections) {
|
||||
val name = nameOf(case)
|
||||
assertNull(
|
||||
EncryptedMediaV2.parseImetaTagOrNull(tagOf(case)),
|
||||
"'$name' must be rejected — the fixture expects '${case["error_contains"]?.jsonPrimitive?.content}'",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aV1TagIsNotReadableAsV2() {
|
||||
// "Component id `0x8008` remains the frozen v1 policy and MUST NOT be
|
||||
// reinterpreted as v2." The two share a field layout closely enough
|
||||
// that a parser keying only on the fields would happily read one as the
|
||||
// other — and then derive a file key under the wrong scheme. So every
|
||||
// v1 case, including the ones v1 itself calls valid, has to bounce off
|
||||
// the v2 parser.
|
||||
val v1Cases = cases(v1)
|
||||
assertTrue(v1Cases.isNotEmpty(), "expected the v1 fixture to carry cases")
|
||||
assertEquals("encrypted-media-v1", v1["media_version"]!!.jsonPrimitive.content)
|
||||
|
||||
for (case in v1Cases) {
|
||||
assertNull(
|
||||
EncryptedMediaV2.parseImetaTagOrNull(tagOf(case)),
|
||||
"v1 case '${nameOf(case)}' must not parse as ${EncryptedMediaPolicyV2.MEDIA_FORMAT}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A fixture field that is absent, JSON null, or a real string. */
|
||||
private fun optional(
|
||||
obj: kotlinx.serialization.json.JsonObject,
|
||||
key: String,
|
||||
): String? =
|
||||
obj[key]?.let { element ->
|
||||
val primitive = element.jsonPrimitive
|
||||
if (primitive.isString) primitive.content else null
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.marmot.conformance
|
||||
|
||||
import com.vitorpamplona.quartz.TestResourceLoader
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.AppComponentIds
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.NostrRoutingV1
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
/**
|
||||
* The reference implementation's byte-level fixtures for
|
||||
* `marmot.transport.nostr.routing.v1` (`0x8004`), run against our codec.
|
||||
*
|
||||
* These come from `mdk/crates/cgka-conformance-simulator/vectors/byte-fixtures/`,
|
||||
* copied verbatim, and they are the only tests we have that pin the WIRE BYTES
|
||||
* of a Marmot app component against another implementation rather than against
|
||||
* our own encoder. A round-trip test proves we can read what we wrote; these
|
||||
* prove we can read what MDK wrote, which is a different claim and the one that
|
||||
* matters for a group with members on both.
|
||||
*
|
||||
* The invalid fixture is the sharper one. Its note says implementations "MUST
|
||||
* reject this update during component validation" — a decoder that quietly
|
||||
* deduplicated the relay list instead would hold different canonical bytes than
|
||||
* the peer that sent them, and the two would disagree about the group's state
|
||||
* forever after.
|
||||
*/
|
||||
class NostrRoutingByteFixtureTest {
|
||||
private fun fixture(name: String) = Json.parseToJsonElement(TestResourceLoader().loadString("marmot/conformance/$name")).jsonObject
|
||||
|
||||
private fun bytesOf(fixture: kotlinx.serialization.json.JsonObject) = Hex.decode(fixture["bytes"]!!.jsonObject["hex"]!!.jsonPrimitive.content)
|
||||
|
||||
private fun expectedRelays(fixture: kotlinx.serialization.json.JsonObject) =
|
||||
fixture["expected"]!!
|
||||
.jsonObject["fields"]!!
|
||||
.jsonObject["relays"]!!
|
||||
.jsonArray
|
||||
.map { it.jsonPrimitive.content }
|
||||
|
||||
private fun expectedGroupId(fixture: kotlinx.serialization.json.JsonObject) =
|
||||
fixture["expected"]!!
|
||||
.jsonObject["fields"]!!
|
||||
.jsonObject["nostr_group_id_hex"]!!
|
||||
.jsonPrimitive.content
|
||||
|
||||
/** Every fixture names the component it belongs to; check we read the right ones. */
|
||||
private fun assertIsRoutingFixture(fixture: kotlinx.serialization.json.JsonObject) {
|
||||
assertEquals("1", fixture["fixture_version"]!!.jsonPrimitive.content)
|
||||
assertEquals(
|
||||
AppComponentIds.NOSTR_ROUTING_V1,
|
||||
fixture["component"]!!
|
||||
.jsonObject["id"]!!
|
||||
.jsonPrimitive.content
|
||||
.removePrefix("0x")
|
||||
.toInt(16),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theValidStateFixtureDecodesToItsDeclaredFields() {
|
||||
val fixture = fixture("nostr-routing-v1-valid-state.v1.json")
|
||||
assertIsRoutingFixture(fixture)
|
||||
assertTrue(fixture["expected"]!!.jsonObject["valid"]!!.jsonPrimitive.content == "true")
|
||||
|
||||
val decoded = NostrRoutingV1.decode(bytesOf(fixture))
|
||||
assertEquals(expectedGroupId(fixture), decoded.nostrGroupId.toHexKey())
|
||||
assertEquals(expectedRelays(fixture), decoded.relays)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun weReEncodeTheValidStateToTheSameBytes() {
|
||||
// Canonical encoding is a two-way claim: reading their bytes is half of
|
||||
// it, and writing bytes they would read is the other. A component whose
|
||||
// re-encode differs by one byte is state a conformant decoder rejects
|
||||
// outright, because it compares against its own serialization.
|
||||
val fixture = fixture("nostr-routing-v1-valid-state.v1.json")
|
||||
val bytes = bytesOf(fixture)
|
||||
assertContentEquals(bytes, NostrRoutingV1.decode(bytes).encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theValidUpdateFixtureDecodesWithTheSameCodecAsTheState() {
|
||||
// "The update is a full replacement state and begins with
|
||||
// nostr_group_id[32]; it is decoded by the same codec as the state
|
||||
// vector." An implementation that gave the update its own shape would
|
||||
// read a relay rotation as garbage.
|
||||
val fixture = fixture("nostr-routing-v1-valid-update.v1.json")
|
||||
assertIsRoutingFixture(fixture)
|
||||
|
||||
val bytes = bytesOf(fixture)
|
||||
val decoded = NostrRoutingV1.decode(bytes)
|
||||
assertEquals(expectedGroupId(fixture), decoded.nostrGroupId.toHexKey())
|
||||
assertEquals(expectedRelays(fixture), decoded.relays)
|
||||
assertContentEquals(bytes, decoded.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theDuplicateRelayFixtureIsRejected() {
|
||||
val fixture = fixture("nostr-routing-v1-invalid-duplicate-relay.v1.json")
|
||||
assertIsRoutingFixture(fixture)
|
||||
assertTrue(fixture["expected"]!!.jsonObject["valid"]!!.jsonPrimitive.content == "false")
|
||||
assertEquals(
|
||||
listOf("duplicate_relay"),
|
||||
fixture["expected"]!!.jsonObject["errors"]!!.jsonArray.map { it.jsonPrimitive.content },
|
||||
)
|
||||
|
||||
// Rejected, not repaired. Silently dropping the duplicate would leave
|
||||
// us holding bytes no peer agrees with.
|
||||
assertFailsWith<IllegalArgumentException>("a duplicate relay must be refused, not deduplicated") {
|
||||
NostrRoutingV1.decode(bytesOf(fixture))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theComponentDataWrapperCarriesTheSameStateBytes() {
|
||||
// `component_data_hex` is the OpenMLS ComponentData entry: the uint16
|
||||
// component id, then the state as a variable-length vector. Checking
|
||||
// the inner bytes against `hex` is what catches a framing mistake in
|
||||
// the dictionary layer rather than in the component codec.
|
||||
val fixture = fixture("nostr-routing-v1-valid-state.v1.json")
|
||||
val wrapper = Hex.decode(fixture["bytes"]!!.jsonObject["component_data_hex"]!!.jsonPrimitive.content)
|
||||
val state = bytesOf(fixture)
|
||||
|
||||
val id = ((wrapper[0].toInt() and 0xff) shl 8) or (wrapper[1].toInt() and 0xff)
|
||||
assertEquals(AppComponentIds.NOSTR_ROUTING_V1, id, "the wrapper names 0x8004")
|
||||
if (!wrapper.copyOfRange(wrapper.size - state.size, wrapper.size).contentEquals(state)) {
|
||||
fail("the ComponentData wrapper does not end with the state bytes it declares")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Marmot conformance fixtures (copied from MDK)
|
||||
|
||||
These files are **copied verbatim** from the reference implementation. They are
|
||||
not ours to edit: their whole value is that another implementation wrote them,
|
||||
so a local "fix" to make a test pass would delete the only thing they prove.
|
||||
|
||||
| File | Upstream path |
|
||||
|---|---|
|
||||
| `imeta-v1.json`, `imeta-v2.json` | `fixtures/encrypted-media/` |
|
||||
| `nostr-routing-v1-*.v1.json` | `crates/cgka-conformance-simulator/vectors/byte-fixtures/` |
|
||||
|
||||
Upstream is the MDK checkout the interop harness already vendors at
|
||||
`cli/tests/marmot/state/mdk`. To refresh:
|
||||
|
||||
```bash
|
||||
MDK=cli/tests/marmot/state/mdk
|
||||
cp $MDK/fixtures/encrypted-media/imeta-v{1,2}.json \
|
||||
quartz/src/commonTest/resources/marmot/conformance/
|
||||
cp $MDK/crates/cgka-conformance-simulator/vectors/byte-fixtures/nostr-routing-v1-*.v1.json \
|
||||
quartz/src/commonTest/resources/marmot/conformance/
|
||||
```
|
||||
|
||||
A refresh that makes a test fail is a signal, not a chore: either the wire
|
||||
format moved and we have not, or upstream tightened a rule we were lenient
|
||||
about.
|
||||
|
||||
## What is NOT here yet
|
||||
|
||||
`crates/cgka-conformance-simulator/vectors/manifest.v1.json` lists 41 artifacts,
|
||||
31 marked `"status": "portable"` — built for exactly this purpose. The 19
|
||||
scenario vectors (`three-client-message-exchange`, `publish-fail`,
|
||||
`convergence-*-selected`, `restart-delivery-faults`, `late-welcome-backfill`, …)
|
||||
need a runner that can drive our client through a scripted trace and project
|
||||
its state per `marmot/foundation/conformance.md` ("Canonical snapshot"). That
|
||||
is a separate piece of work, and it is where the convergence and crash/restart
|
||||
coverage lives.
|
||||
|
||||
`tests/agent_text_stream_vectors.rs` pins the key context encoding, the HKDF
|
||||
record-key derivation, the record AAD, the transcript hash and the broker
|
||||
control envelope as literal bytes. It is Rust source rather than a data file,
|
||||
so adopting it means transcribing the expected values.
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"schema": "mdk-encrypted-media-imeta-fixture/v1",
|
||||
"description": "Golden and rejection imeta fixtures for encrypted-media-v1. Shared by marmot-app, marmot-uniffi, and wn-cli tests so every layer agrees on validation verdicts and exact wire round-trips. `expected.dim`/`expected.thumbhash` distinguish absent (null) from present-empty (\"\").",
|
||||
"media_version": "encrypted-media-v1",
|
||||
"cases": [
|
||||
{
|
||||
"name": "golden-image-full",
|
||||
"source_epoch": 7,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png",
|
||||
"dim 800x600",
|
||||
"thumbhash 1QcSHQRnh493V4dIh4eXh1h4kJUI"
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v1",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce_hex": "333333333333333333333333",
|
||||
"media_type": "image/png",
|
||||
"file_name": "diagram.png",
|
||||
"dim": "800x600",
|
||||
"thumbhash": "1QcSHQRnh493V4dIh4eXh1h4kJUI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-document-minimal",
|
||||
"source_epoch": 42,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/4444444444444444444444444444444444444444444444444444444444444444.bin",
|
||||
"ciphertext_sha256 4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"plaintext_sha256 5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"nonce 666666666666666666666666",
|
||||
"m application/pdf",
|
||||
"filename brief.pdf"
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v1",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/4444444444444444444444444444444444444444444444444444444444444444.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"plaintext_sha256": "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"nonce_hex": "666666666666666666666666",
|
||||
"media_type": "application/pdf",
|
||||
"file_name": "brief.pdf",
|
||||
"dim": null,
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-present-empty-dim",
|
||||
"source_epoch": 7,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png",
|
||||
"dim "
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v1",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce_hex": "333333333333333333333333",
|
||||
"media_type": "image/png",
|
||||
"file_name": "diagram.png",
|
||||
"dim": "",
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rejects-missing-nonce",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "nonce",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"m image/png",
|
||||
"filename diagram.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-duplicate-media-type",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "exactly one m",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png",
|
||||
"m image/jpeg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-blurhash-field",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "thumbhash",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png",
|
||||
"blurhash LEHV6nWB2yk8pyo0adR*.7kCMdnj"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-blossom-locator-hash-mismatch",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "locator",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/7777777777777777777777777777777777777777777777777777777777777777.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-unknown-version",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "version",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v3",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-whitespace-only-filename",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "file name",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 333333333333333333333333",
|
||||
"m image/png",
|
||||
"filename "
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-short-nonce",
|
||||
"source_epoch": 7,
|
||||
"valid": false,
|
||||
"error_contains": "nonce",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v1",
|
||||
"locator blossom-v1 https://media.example/1111111111111111111111111111111111111111111111111111111111111111.bin",
|
||||
"ciphertext_sha256 1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"plaintext_sha256 2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"nonce 3333333333333333",
|
||||
"m image/png",
|
||||
"filename diagram.png"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"schema": "mdk-encrypted-media-imeta-fixture/v1",
|
||||
"description": "Golden and rejection imeta fixtures for encrypted-media-v2. Shared by marmot-app, marmot-uniffi, and wn-cli tests so every layer agrees on validation verdicts and exact wire round-trips. `expected.dim`/`expected.thumbhash` distinguish absent (null) from present-empty (\"\").",
|
||||
"media_version": "encrypted-media-v2",
|
||||
"cases": [
|
||||
{
|
||||
"name": "golden-image-full",
|
||||
"source_epoch": 9,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename photo.jpg",
|
||||
"dim 1920x1080",
|
||||
"thumbhash 1QcSHQRnh493V4dIh4eXh1h4kJUI"
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v2",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/abababababababababababababababababababababababababababababababab.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce_hex": "efefefefefefefefefefefef",
|
||||
"media_type": "image/jpeg",
|
||||
"file_name": "photo.jpg",
|
||||
"dim": "1920x1080",
|
||||
"thumbhash": "1QcSHQRnh493V4dIh4eXh1h4kJUI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-audio-minimal",
|
||||
"source_epoch": 3,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/1212121212121212121212121212121212121212121212121212121212121212.bin",
|
||||
"ciphertext_sha256 1212121212121212121212121212121212121212121212121212121212121212",
|
||||
"plaintext_sha256 3434343434343434343434343434343434343434343434343434343434343434",
|
||||
"nonce 565656565656565656565656",
|
||||
"m audio/ogg",
|
||||
"filename voice.ogg"
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v2",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/1212121212121212121212121212121212121212121212121212121212121212.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "1212121212121212121212121212121212121212121212121212121212121212",
|
||||
"plaintext_sha256": "3434343434343434343434343434343434343434343434343434343434343434",
|
||||
"nonce_hex": "565656565656565656565656",
|
||||
"media_type": "audio/ogg",
|
||||
"file_name": "voice.ogg",
|
||||
"dim": null,
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-multi-locator",
|
||||
"source_epoch": 12,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"locator mirror-v9 https://mirror.example/blob",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m video/mp4",
|
||||
"filename clip.mp4"
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v2",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/abababababababababababababababababababababababababababababababab.bin"
|
||||
},
|
||||
{
|
||||
"kind": "mirror-v9",
|
||||
"value": "https://mirror.example/blob"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce_hex": "efefefefefefefefefefefef",
|
||||
"media_type": "video/mp4",
|
||||
"file_name": "clip.mp4",
|
||||
"dim": null,
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-present-empty-dim",
|
||||
"source_epoch": 5,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename photo.jpg",
|
||||
"dim "
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v2",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/abababababababababababababababababababababababababababababababab.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce_hex": "efefefefefefefefefefefef",
|
||||
"media_type": "image/jpeg",
|
||||
"file_name": "photo.jpg",
|
||||
"dim": "",
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "golden-exact-space-filename",
|
||||
"source_epoch": 5,
|
||||
"valid": true,
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename "
|
||||
],
|
||||
"expected": {
|
||||
"version": "encrypted-media-v2",
|
||||
"locators": [
|
||||
{
|
||||
"kind": "blossom-v1",
|
||||
"value": "https://media.example/abababababababababababababababababababababababababababababababab.bin"
|
||||
}
|
||||
],
|
||||
"ciphertext_sha256": "abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce_hex": "efefefefefefefefefefefef",
|
||||
"media_type": "image/jpeg",
|
||||
"file_name": " ",
|
||||
"dim": null,
|
||||
"thumbhash": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rejects-noncanonical-media-type",
|
||||
"source_epoch": 9,
|
||||
"valid": false,
|
||||
"error_contains": "canonical",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m Image/JPG",
|
||||
"filename photo.jpg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-overlong-filename",
|
||||
"source_epoch": 9,
|
||||
"valid": false,
|
||||
"error_contains": "file name",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-nul-in-filename",
|
||||
"source_epoch": 9,
|
||||
"valid": false,
|
||||
"error_contains": "file name",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename bad\u0000name.jpg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-duplicate-plaintext-hash",
|
||||
"source_epoch": 9,
|
||||
"valid": false,
|
||||
"error_contains": "exactly one plaintext_sha256",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"v encrypted-media-v2",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename photo.jpg",
|
||||
"plaintext_sha256 3434343434343434343434343434343434343434343434343434343434343434"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rejects-missing-version",
|
||||
"source_epoch": 9,
|
||||
"valid": false,
|
||||
"error_contains": "missing v",
|
||||
"tag": [
|
||||
"imeta",
|
||||
"locator blossom-v1 https://media.example/abababababababababababababababababababababababababababababababab.bin",
|
||||
"ciphertext_sha256 abababababababababababababababababababababababababababababababab",
|
||||
"plaintext_sha256 cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"nonce efefefefefefefefefefefef",
|
||||
"m image/jpeg",
|
||||
"filename photo.jpg"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"fixture_name": "marmot.transport.nostr.routing.v1/update-invalid-duplicate-relay/v1",
|
||||
"fixture_version": "1",
|
||||
"vector_type": "app_component_update",
|
||||
"component": {
|
||||
"id": "0x8004",
|
||||
"name": "marmot.transport.nostr.routing.v1",
|
||||
"document": "spec/app-components/nostr-routing-v1.md"
|
||||
},
|
||||
"encoding": {
|
||||
"format": "Marmot canonical encoding",
|
||||
"notes": [
|
||||
"bytes.hex is the MarmotNostrRoutingUpdateV1 payload (full replacement state), not the AppDataUpdate proposal wrapper.",
|
||||
"It carries nostr_group_id[32] then two relay entries that decode to the same byte string."
|
||||
]
|
||||
},
|
||||
"bytes": {
|
||||
"hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2c157773733a2f2f72656c61792d612e6578616d706c65157773733a2f2f72656c61792d612e6578616d706c65"
|
||||
},
|
||||
"expected": {
|
||||
"valid": false,
|
||||
"fields": {
|
||||
"nostr_group_id_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
"relays": [
|
||||
"wss://relay-a.example",
|
||||
"wss://relay-a.example"
|
||||
]
|
||||
},
|
||||
"errors": [
|
||||
"duplicate_relay"
|
||||
]
|
||||
},
|
||||
"notes": [
|
||||
"Implementations MUST reject this update during component validation; the reference decoder returns a duplicate-relay error."
|
||||
]
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"fixture_name": "marmot.transport.nostr.routing.v1/state-valid-two-relays/v1",
|
||||
"fixture_version": "1",
|
||||
"vector_type": "app_component_state",
|
||||
"component": {
|
||||
"id": "0x8004",
|
||||
"name": "marmot.transport.nostr.routing.v1",
|
||||
"document": "spec/app-components/nostr-routing-v1.md"
|
||||
},
|
||||
"encoding": {
|
||||
"format": "Marmot canonical encoding",
|
||||
"notes": [
|
||||
"bytes.hex is the component state bytes stored in AppDataDictionary.component_data.data, in the Marmot canonical encoding (spec/foundation/canonical-encoding.md): QUIC variable-length length prefixes.",
|
||||
"opaque nostr_group_id[32] is exactly 32 bytes with NO length prefix.",
|
||||
"relays<V> is a QUIC-varint byte length, then each MarmotNostrRelayV1.url<1..512> is a QUIC-varint length prefix followed by the URL bytes.",
|
||||
"bytes.component_data_hex is the OpenMLS ComponentData entry: component_id (uint16 0x8004) then the data as a variable-length byte vector wrapping bytes.hex."
|
||||
]
|
||||
},
|
||||
"bytes": {
|
||||
"hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2c157773733a2f2f72656c61792d612e6578616d706c65157773733a2f2f72656c61792d622e6578616d706c65",
|
||||
"component_data_hex": "8004404d000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2c157773733a2f2f72656c61792d612e6578616d706c65157773733a2f2f72656c61792d622e6578616d706c65"
|
||||
},
|
||||
"expected": {
|
||||
"valid": true,
|
||||
"fields": {
|
||||
"nostr_group_id_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
"relays": [
|
||||
"wss://relay-a.example",
|
||||
"wss://relay-b.example"
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
"notes": [
|
||||
"Relay order is byte-lexicographic over the URL content bytes and duplicate-free.",
|
||||
"This fixture does not include an AppDataUpdate wrapper."
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"fixture_name": "marmot.transport.nostr.routing.v1/update-valid-full-replacement/v1",
|
||||
"fixture_version": "1",
|
||||
"vector_type": "app_component_update",
|
||||
"component": {
|
||||
"id": "0x8004",
|
||||
"name": "marmot.transport.nostr.routing.v1",
|
||||
"document": "spec/app-components/nostr-routing-v1.md"
|
||||
},
|
||||
"encoding": {
|
||||
"format": "Marmot canonical encoding",
|
||||
"notes": [
|
||||
"bytes.hex is the MarmotNostrRoutingUpdateV1 payload, not the AppDataUpdate proposal wrapper.",
|
||||
"The update payload is a full replacement state (MarmotNostrRoutingUpdateV1 = MarmotNostrRoutingV1): it carries the 32-byte nostr_group_id followed by the new relays<V>, in the Marmot canonical encoding.",
|
||||
"This example changes the relay list to a single relay while keeping nostr_group_id; an update MAY also change nostr_group_id (a routing rotation)."
|
||||
]
|
||||
},
|
||||
"bytes": {
|
||||
"hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f16157773733a2f2f72656c61792d632e6578616d706c65"
|
||||
},
|
||||
"expected": {
|
||||
"valid": true,
|
||||
"fields": {
|
||||
"nostr_group_id_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
"relays": [
|
||||
"wss://relay-c.example"
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
"notes": [
|
||||
"The update is a full replacement state and begins with nostr_group_id[32]; it is decoded by the same codec as the state vector."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user