diff options
Diffstat (limited to 'axon_test.go')
| -rw-r--r-- | axon_test.go | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/axon_test.go b/axon_test.go new file mode 100644 index 0000000..b7f5bd6 --- /dev/null +++ b/axon_test.go | |||
| @@ -0,0 +1,550 @@ | |||
| 1 | package axon_test | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "crypto/ed25519" | ||
| 5 | "encoding/hex" | ||
| 6 | "encoding/json" | ||
| 7 | "os" | ||
| 8 | "path/filepath" | ||
| 9 | "testing" | ||
| 10 | "time" | ||
| 11 | |||
| 12 | "axon" | ||
| 13 | ) | ||
| 14 | |||
| 15 | // -------------------------------------------------------------------------- | ||
| 16 | // Helpers | ||
| 17 | // -------------------------------------------------------------------------- | ||
| 18 | |||
| 19 | func mustDecodeHex(t *testing.T, s string) []byte { | ||
| 20 | t.Helper() | ||
| 21 | b, err := hex.DecodeString(s) | ||
| 22 | if err != nil { | ||
| 23 | t.Fatalf("hex decode %q: %v", s, err) | ||
| 24 | } | ||
| 25 | return b | ||
| 26 | } | ||
| 27 | |||
| 28 | func hexOf(b []byte) string { return hex.EncodeToString(b) } | ||
| 29 | |||
| 30 | // knownSeed returns a fixed 32-byte seed so tests are fully deterministic. | ||
| 31 | func knownSeed(n byte) []byte { | ||
| 32 | seed := make([]byte, 32) | ||
| 33 | for i := range seed { | ||
| 34 | seed[i] = n + byte(i) | ||
| 35 | } | ||
| 36 | return seed | ||
| 37 | } | ||
| 38 | |||
| 39 | // -------------------------------------------------------------------------- | ||
| 40 | // Test: canonical_tags | ||
| 41 | // -------------------------------------------------------------------------- | ||
| 42 | |||
| 43 | func TestCanonicalTags(t *testing.T) { | ||
| 44 | vectors := loadVectors(t) | ||
| 45 | tags := vectors.TagsInput | ||
| 46 | |||
| 47 | enc, err := axon.CanonicalTags(tags) | ||
| 48 | if err != nil { | ||
| 49 | t.Fatalf("CanonicalTags: %v", err) | ||
| 50 | } | ||
| 51 | if len(enc) == 0 { | ||
| 52 | t.Fatal("CanonicalTags returned empty bytes") | ||
| 53 | } | ||
| 54 | |||
| 55 | h, err := axon.CanonicalTagsHash(tags) | ||
| 56 | if err != nil { | ||
| 57 | t.Fatalf("CanonicalTagsHash: %v", err) | ||
| 58 | } | ||
| 59 | if h == [32]byte{} { | ||
| 60 | t.Fatal("CanonicalTagsHash returned zero hash") | ||
| 61 | } | ||
| 62 | |||
| 63 | // Verify sort order: tags with the same name are sorted by first value. | ||
| 64 | // "e"/"reply-id" < "e"/"root-id" lexicographically. "e" < "p". | ||
| 65 | // So expected order: e/reply-id, e/root-id, p/alice | ||
| 66 | expectHash := mustDecodeHex(t, vectors.CanonicalTagsHash) | ||
| 67 | if hexOf(h[:]) != hexOf(expectHash) { | ||
| 68 | t.Errorf("CanonicalTagsHash mismatch:\n got %s\n want %s", hexOf(h[:]), hexOf(expectHash)) | ||
| 69 | } | ||
| 70 | |||
| 71 | expectEnc := mustDecodeHex(t, vectors.CanonicalTagsHex) | ||
| 72 | if hexOf(enc) != hexOf(expectEnc) { | ||
| 73 | t.Errorf("canonical_tags bytes mismatch:\n got %s\n want %s", hexOf(enc), hexOf(expectEnc)) | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | func TestCanonicalTagsDuplicateError(t *testing.T) { | ||
| 78 | tags := []axon.Tag{ | ||
| 79 | {Name: "e", Values: []string{"same-id", "root"}}, | ||
| 80 | {Name: "e", Values: []string{"same-id", "reply"}}, | ||
| 81 | } | ||
| 82 | _, err := axon.CanonicalTags(tags) | ||
| 83 | if err == nil { | ||
| 84 | t.Fatal("expected error for duplicate (name, first_value) pair") | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | func TestCanonicalTagsEmpty(t *testing.T) { | ||
| 89 | enc, err := axon.CanonicalTags(nil) | ||
| 90 | if err != nil { | ||
| 91 | t.Fatalf("CanonicalTags(nil): %v", err) | ||
| 92 | } | ||
| 93 | // Should encode as uint16(0) = 2 zero bytes. | ||
| 94 | if len(enc) != 2 || enc[0] != 0 || enc[1] != 0 { | ||
| 95 | t.Errorf("empty tags encoding: got %x, want 0000", enc) | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | // -------------------------------------------------------------------------- | ||
| 100 | // Test: canonical_payload | ||
| 101 | // -------------------------------------------------------------------------- | ||
| 102 | |||
| 103 | func TestCanonicalPayload(t *testing.T) { | ||
| 104 | vectors := loadVectors(t) | ||
| 105 | kp := axon.NewKeyPairFromSeed(mustDecodeHex(t, vectors.SeedHex)) | ||
| 106 | content := mustDecodeHex(t, vectors.ContentHex) | ||
| 107 | |||
| 108 | payload, err := axon.CanonicalPayload(kp.PubKey, vectors.CreatedAt, vectors.Kind, content, vectors.TagsInput) | ||
| 109 | if err != nil { | ||
| 110 | t.Fatalf("CanonicalPayload: %v", err) | ||
| 111 | } | ||
| 112 | |||
| 113 | // Validate structure: first 2 bytes must be 0x00 0x20 (= 32). | ||
| 114 | if payload[0] != 0 || payload[1] != 32 { | ||
| 115 | t.Errorf("payload[0:2] = %x, want 0020", payload[0:2]) | ||
| 116 | } | ||
| 117 | // Bytes [2:34] = pubkey. | ||
| 118 | if hexOf(payload[2:34]) != hexOf(kp.PubKey) { | ||
| 119 | t.Errorf("pubkey in payload mismatch") | ||
| 120 | } | ||
| 121 | // Total length: 2 + 32 + 8 + 2 + 4 + len(content) + 32 | ||
| 122 | want := 2 + 32 + 8 + 2 + 4 + len(content) + 32 | ||
| 123 | if len(payload) != want { | ||
| 124 | t.Errorf("payload length = %d, want %d", len(payload), want) | ||
| 125 | } | ||
| 126 | |||
| 127 | expectPayload := mustDecodeHex(t, vectors.CanonicalPayload) | ||
| 128 | if hexOf(payload) != hexOf(expectPayload) { | ||
| 129 | t.Errorf("canonical_payload mismatch:\n got %s\n want %s", hexOf(payload), hexOf(expectPayload)) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | // -------------------------------------------------------------------------- | ||
| 134 | // Test: event id | ||
| 135 | // -------------------------------------------------------------------------- | ||
| 136 | |||
| 137 | func TestEventID(t *testing.T) { | ||
| 138 | vectors := loadVectors(t) | ||
| 139 | kp := axon.NewKeyPairFromSeed(mustDecodeHex(t, vectors.SeedHex)) | ||
| 140 | content := mustDecodeHex(t, vectors.ContentHex) | ||
| 141 | |||
| 142 | id, err := axon.EventID(kp.PubKey, vectors.CreatedAt, vectors.Kind, content, vectors.TagsInput) | ||
| 143 | if err != nil { | ||
| 144 | t.Fatalf("EventID: %v", err) | ||
| 145 | } | ||
| 146 | if len(id) != 32 { | ||
| 147 | t.Fatalf("EventID returned %d bytes, want 32", len(id)) | ||
| 148 | } | ||
| 149 | |||
| 150 | expectID := mustDecodeHex(t, vectors.EventID) | ||
| 151 | if hexOf(id) != hexOf(expectID) { | ||
| 152 | t.Errorf("event id mismatch:\n got %s\n want %s", hexOf(id), hexOf(expectID)) | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | // -------------------------------------------------------------------------- | ||
| 157 | // Test: sign and verify | ||
| 158 | // -------------------------------------------------------------------------- | ||
| 159 | |||
| 160 | func TestSignVerify(t *testing.T) { | ||
| 161 | kp := axon.NewKeyPairFromSeed(knownSeed(0x01)) | ||
| 162 | |||
| 163 | e := &axon.Event{ | ||
| 164 | CreatedAt: 1700000000, | ||
| 165 | Kind: axon.KindMessage, | ||
| 166 | Content: []byte("hello axon"), | ||
| 167 | Tags: []axon.Tag{{Name: "p", Values: []string{"alice"}}}, | ||
| 168 | } | ||
| 169 | if err := axon.Sign(e, kp); err != nil { | ||
| 170 | t.Fatalf("Sign: %v", err) | ||
| 171 | } | ||
| 172 | |||
| 173 | if len(e.ID) != 32 { | ||
| 174 | t.Errorf("ID is %d bytes, want 32", len(e.ID)) | ||
| 175 | } | ||
| 176 | if len(e.Sig) != ed25519.SignatureSize { | ||
| 177 | t.Errorf("Sig is %d bytes, want 64", len(e.Sig)) | ||
| 178 | } | ||
| 179 | |||
| 180 | if err := axon.Verify(e); err != nil { | ||
| 181 | t.Fatalf("Verify: %v", err) | ||
| 182 | } | ||
| 183 | |||
| 184 | // Tampering with content must invalidate. | ||
| 185 | e2 := *e | ||
| 186 | e2.Content = []byte("tampered") | ||
| 187 | if err := axon.Verify(&e2); err == nil { | ||
| 188 | t.Fatal("Verify should fail after content tamper") | ||
| 189 | } | ||
| 190 | |||
| 191 | // Tampering with signature must invalidate. | ||
| 192 | e3 := *e | ||
| 193 | sig := make([]byte, len(e.Sig)) | ||
| 194 | copy(sig, e.Sig) | ||
| 195 | sig[0] ^= 0xff | ||
| 196 | e3.Sig = sig | ||
| 197 | if err := axon.Verify(&e3); err == nil { | ||
| 198 | t.Fatal("Verify should fail after sig tamper") | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | func TestVerifyWrongKey(t *testing.T) { | ||
| 203 | kp1 := axon.NewKeyPairFromSeed(knownSeed(0x01)) | ||
| 204 | kp2 := axon.NewKeyPairFromSeed(knownSeed(0x02)) | ||
| 205 | |||
| 206 | e := &axon.Event{ | ||
| 207 | CreatedAt: time.Now().Unix(), | ||
| 208 | Kind: axon.KindMessage, | ||
| 209 | Content: []byte("test"), | ||
| 210 | } | ||
| 211 | if err := axon.Sign(e, kp1); err != nil { | ||
| 212 | t.Fatalf("Sign: %v", err) | ||
| 213 | } | ||
| 214 | |||
| 215 | // Replace pubkey with kp2's pubkey; should fail. | ||
| 216 | e.PubKey = kp2.PubKey | ||
| 217 | if err := axon.Verify(e); err == nil { | ||
| 218 | t.Fatal("Verify should fail with wrong pubkey") | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | // -------------------------------------------------------------------------- | ||
| 223 | // Test: challenge sign/verify | ||
| 224 | // -------------------------------------------------------------------------- | ||
| 225 | |||
| 226 | func TestChallengeSignVerify(t *testing.T) { | ||
| 227 | kp := axon.NewKeyPairFromSeed(knownSeed(0x03)) | ||
| 228 | nonce := make([]byte, 32) | ||
| 229 | for i := range nonce { | ||
| 230 | nonce[i] = byte(i) | ||
| 231 | } | ||
| 232 | relayURL := "wss://relay.example.com" | ||
| 233 | |||
| 234 | sig := axon.SignChallenge(kp, nonce, relayURL) | ||
| 235 | if !axon.VerifyChallenge(kp.PubKey, nonce, relayURL, sig) { | ||
| 236 | t.Fatal("VerifyChallenge failed for valid signature") | ||
| 237 | } | ||
| 238 | if axon.VerifyChallenge(kp.PubKey, nonce, "wss://other.relay.com", sig) { | ||
| 239 | t.Fatal("VerifyChallenge should fail for different relay URL") | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | // -------------------------------------------------------------------------- | ||
| 244 | // Test: X25519 key conversion | ||
| 245 | // -------------------------------------------------------------------------- | ||
| 246 | |||
| 247 | func TestX25519Conversion(t *testing.T) { | ||
| 248 | kp := axon.NewKeyPairFromSeed(knownSeed(0x01)) | ||
| 249 | |||
| 250 | // Converting same Ed25519 keypair from both sides should yield same shared secret. | ||
| 251 | kp2 := axon.NewKeyPairFromSeed(knownSeed(0x02)) | ||
| 252 | |||
| 253 | shared1, err := axon.DHSharedSecret(kp.PrivKey, kp2.PubKey) | ||
| 254 | if err != nil { | ||
| 255 | t.Fatalf("DHSharedSecret(kp->kp2): %v", err) | ||
| 256 | } | ||
| 257 | shared2, err := axon.DHSharedSecret(kp2.PrivKey, kp.PubKey) | ||
| 258 | if err != nil { | ||
| 259 | t.Fatalf("DHSharedSecret(kp2->kp): %v", err) | ||
| 260 | } | ||
| 261 | if shared1 != shared2 { | ||
| 262 | t.Errorf("DH not commutative:\n kp->kp2: %s\n kp2->kp: %s", | ||
| 263 | hexOf(shared1[:]), hexOf(shared2[:])) | ||
| 264 | } | ||
| 265 | if shared1 == [32]byte{} { | ||
| 266 | t.Fatal("shared secret is all zeros") | ||
| 267 | } | ||
| 268 | } | ||
| 269 | |||
| 270 | // -------------------------------------------------------------------------- | ||
| 271 | // Test: DM encrypt/decrypt round-trip | ||
| 272 | // -------------------------------------------------------------------------- | ||
| 273 | |||
| 274 | func TestDMEncryptDecrypt(t *testing.T) { | ||
| 275 | alice := axon.NewKeyPairFromSeed(knownSeed(0x0a)) | ||
| 276 | bob := axon.NewKeyPairFromSeed(knownSeed(0x0b)) | ||
| 277 | |||
| 278 | plaintext := []byte("secret message from alice to bob") | ||
| 279 | |||
| 280 | ct, err := axon.EncryptDM(alice.PrivKey, bob.PubKey, plaintext) | ||
| 281 | if err != nil { | ||
| 282 | t.Fatalf("EncryptDM: %v", err) | ||
| 283 | } | ||
| 284 | |||
| 285 | pt, err := axon.DecryptDM(bob.PrivKey, alice.PubKey, ct) | ||
| 286 | if err != nil { | ||
| 287 | t.Fatalf("DecryptDM: %v", err) | ||
| 288 | } | ||
| 289 | if string(pt) != string(plaintext) { | ||
| 290 | t.Errorf("decrypt mismatch: got %q, want %q", pt, plaintext) | ||
| 291 | } | ||
| 292 | |||
| 293 | // Tamper with ciphertext — authentication should fail. | ||
| 294 | ct2 := make([]byte, len(ct)) | ||
| 295 | copy(ct2, ct) | ||
| 296 | ct2[len(ct2)-1] ^= 0xff | ||
| 297 | if _, err := axon.DecryptDM(bob.PrivKey, alice.PubKey, ct2); err == nil { | ||
| 298 | t.Fatal("DecryptDM should fail on tampered ciphertext") | ||
| 299 | } | ||
| 300 | |||
| 301 | // Decrypt with wrong key should fail. | ||
| 302 | carol := axon.NewKeyPairFromSeed(knownSeed(0x0c)) | ||
| 303 | if _, err := axon.DecryptDM(carol.PrivKey, alice.PubKey, ct); err == nil { | ||
| 304 | t.Fatal("DecryptDM should fail with wrong recipient key") | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | // -------------------------------------------------------------------------- | ||
| 309 | // Test: MessagePack encode/decode | ||
| 310 | // -------------------------------------------------------------------------- | ||
| 311 | |||
| 312 | func TestMsgPackRoundTrip(t *testing.T) { | ||
| 313 | kp := axon.NewKeyPairFromSeed(knownSeed(0x05)) | ||
| 314 | e := &axon.Event{ | ||
| 315 | CreatedAt: 1700000000, | ||
| 316 | Kind: axon.KindMessage, | ||
| 317 | Content: []byte("msgpack test"), | ||
| 318 | Tags: []axon.Tag{ | ||
| 319 | {Name: "p", Values: []string{"recipient"}}, | ||
| 320 | {Name: "e", Values: []string{"ref-id", "root"}}, | ||
| 321 | }, | ||
| 322 | } | ||
| 323 | if err := axon.Sign(e, kp); err != nil { | ||
| 324 | t.Fatalf("Sign: %v", err) | ||
| 325 | } | ||
| 326 | |||
| 327 | data, err := axon.MarshalEvent(e) | ||
| 328 | if err != nil { | ||
| 329 | t.Fatalf("MarshalEvent: %v", err) | ||
| 330 | } | ||
| 331 | if len(data) == 0 { | ||
| 332 | t.Fatal("MarshalEvent returned empty bytes") | ||
| 333 | } | ||
| 334 | |||
| 335 | e2, err := axon.UnmarshalEvent(data) | ||
| 336 | if err != nil { | ||
| 337 | t.Fatalf("UnmarshalEvent: %v", err) | ||
| 338 | } | ||
| 339 | |||
| 340 | // Verify round-tripped event is still valid. | ||
| 341 | if err := axon.Verify(e2); err != nil { | ||
| 342 | t.Fatalf("Verify after msgpack round-trip: %v", err) | ||
| 343 | } | ||
| 344 | |||
| 345 | if hexOf(e.ID) != hexOf(e2.ID) { | ||
| 346 | t.Errorf("ID mismatch after round-trip") | ||
| 347 | } | ||
| 348 | if hexOf(e.Sig) != hexOf(e2.Sig) { | ||
| 349 | t.Errorf("Sig mismatch after round-trip") | ||
| 350 | } | ||
| 351 | if string(e.Content) != string(e2.Content) { | ||
| 352 | t.Errorf("Content mismatch after round-trip: got %q, want %q", e2.Content, e.Content) | ||
| 353 | } | ||
| 354 | } | ||
| 355 | |||
| 356 | // -------------------------------------------------------------------------- | ||
| 357 | // Test vector generation and validation | ||
| 358 | // -------------------------------------------------------------------------- | ||
| 359 | |||
| 360 | // Vectors is the schema for testdata/vectors.json. | ||
| 361 | type Vectors struct { | ||
| 362 | // canonical_tags vector | ||
| 363 | TagsInput []axon.Tag `json:"tags_input"` | ||
| 364 | CanonicalTagsHex string `json:"canonical_tags_hex"` | ||
| 365 | CanonicalTagsHash string `json:"canonical_tags_hash"` | ||
| 366 | |||
| 367 | // canonical_payload vector | ||
| 368 | SeedHex string `json:"seed_hex"` | ||
| 369 | PubKeyHex string `json:"pubkey_hex"` | ||
| 370 | CreatedAt int64 `json:"created_at"` | ||
| 371 | Kind uint16 `json:"kind"` | ||
| 372 | ContentHex string `json:"content_hex"` | ||
| 373 | CanonicalPayload string `json:"canonical_payload"` | ||
| 374 | |||
| 375 | // event id | ||
| 376 | EventID string `json:"event_id"` | ||
| 377 | |||
| 378 | // signature (sign with known seed; Ed25519 is deterministic so sig is stable) | ||
| 379 | SigHex string `json:"sig_hex"` | ||
| 380 | } | ||
| 381 | |||
| 382 | func vectorsPath() string { | ||
| 383 | return filepath.Join("testdata", "vectors.json") | ||
| 384 | } | ||
| 385 | |||
| 386 | func loadVectors(t *testing.T) Vectors { | ||
| 387 | t.Helper() | ||
| 388 | data, err := os.ReadFile(vectorsPath()) | ||
| 389 | if err != nil { | ||
| 390 | t.Fatalf("load vectors: %v (run TestGenerateVectors first)", err) | ||
| 391 | } | ||
| 392 | var v Vectors | ||
| 393 | if err := json.Unmarshal(data, &v); err != nil { | ||
| 394 | t.Fatalf("parse vectors: %v", err) | ||
| 395 | } | ||
| 396 | return v | ||
| 397 | } | ||
| 398 | |||
| 399 | // TestGenerateVectors generates testdata/vectors.json from known inputs. | ||
| 400 | // Run once with -run TestGenerateVectors to produce the file, then the other | ||
| 401 | // tests validate against it. The file is committed to the repo. | ||
| 402 | func TestGenerateVectors(t *testing.T) { | ||
| 403 | seed := knownSeed(0x01) | ||
| 404 | kp := axon.NewKeyPairFromSeed(seed) | ||
| 405 | |||
| 406 | tags := []axon.Tag{ | ||
| 407 | {Name: "p", Values: []string{"alice"}}, | ||
| 408 | {Name: "e", Values: []string{"root-id", "root"}}, | ||
| 409 | {Name: "e", Values: []string{"reply-id", "reply"}}, | ||
| 410 | } | ||
| 411 | content := []byte("hello axon") | ||
| 412 | createdAt := int64(1700000000) | ||
| 413 | kind := axon.KindMessage | ||
| 414 | |||
| 415 | // canonical_tags | ||
| 416 | tagsEnc, err := axon.CanonicalTags(tags) | ||
| 417 | if err != nil { | ||
| 418 | t.Fatalf("CanonicalTags: %v", err) | ||
| 419 | } | ||
| 420 | tagsHash, err := axon.CanonicalTagsHash(tags) | ||
| 421 | if err != nil { | ||
| 422 | t.Fatalf("CanonicalTagsHash: %v", err) | ||
| 423 | } | ||
| 424 | |||
| 425 | // canonical_payload | ||
| 426 | payload, err := axon.CanonicalPayload(kp.PubKey, createdAt, kind, content, tags) | ||
| 427 | if err != nil { | ||
| 428 | t.Fatalf("CanonicalPayload: %v", err) | ||
| 429 | } | ||
| 430 | |||
| 431 | // event id | ||
| 432 | id, err := axon.EventID(kp.PubKey, createdAt, kind, content, tags) | ||
| 433 | if err != nil { | ||
| 434 | t.Fatalf("EventID: %v", err) | ||
| 435 | } | ||
| 436 | |||
| 437 | // sign | ||
| 438 | e := &axon.Event{ | ||
| 439 | CreatedAt: createdAt, | ||
| 440 | Kind: kind, | ||
| 441 | Content: content, | ||
| 442 | Tags: tags, | ||
| 443 | } | ||
| 444 | if err := axon.Sign(e, kp); err != nil { | ||
| 445 | t.Fatalf("Sign: %v", err) | ||
| 446 | } | ||
| 447 | // Verify the event so we know the sig is valid. | ||
| 448 | if err := axon.Verify(e); err != nil { | ||
| 449 | t.Fatalf("Verify generated event: %v", err) | ||
| 450 | } | ||
| 451 | |||
| 452 | v := Vectors{ | ||
| 453 | TagsInput: tags, | ||
| 454 | CanonicalTagsHex: hexOf(tagsEnc), | ||
| 455 | CanonicalTagsHash: hexOf(tagsHash[:]), | ||
| 456 | |||
| 457 | SeedHex: hexOf(seed), | ||
| 458 | PubKeyHex: hexOf(kp.PubKey), | ||
| 459 | CreatedAt: createdAt, | ||
| 460 | Kind: kind, | ||
| 461 | ContentHex: hexOf(content), | ||
| 462 | CanonicalPayload: hexOf(payload), | ||
| 463 | |||
| 464 | EventID: hexOf(id), | ||
| 465 | SigHex: hexOf(e.Sig), | ||
| 466 | } | ||
| 467 | |||
| 468 | data, err := json.MarshalIndent(v, "", " ") | ||
| 469 | if err != nil { | ||
| 470 | t.Fatalf("marshal vectors: %v", err) | ||
| 471 | } | ||
| 472 | |||
| 473 | if err := os.MkdirAll("testdata", 0o755); err != nil { | ||
| 474 | t.Fatalf("mkdir testdata: %v", err) | ||
| 475 | } | ||
| 476 | if err := os.WriteFile(vectorsPath(), data, 0o644); err != nil { | ||
| 477 | t.Fatalf("write vectors: %v", err) | ||
| 478 | } | ||
| 479 | |||
| 480 | t.Logf("vectors written to %s", vectorsPath()) | ||
| 481 | t.Logf("canonical_tags_hash: %s", hexOf(tagsHash[:])) | ||
| 482 | t.Logf("event_id: %s", hexOf(id)) | ||
| 483 | } | ||
| 484 | |||
| 485 | // TestVectors validates all vectors in testdata/vectors.json. This is the | ||
| 486 | // primary cross-language compatibility check. | ||
| 487 | func TestVectors(t *testing.T) { | ||
| 488 | v := loadVectors(t) | ||
| 489 | |||
| 490 | seed := mustDecodeHex(t, v.SeedHex) | ||
| 491 | kp := axon.NewKeyPairFromSeed(seed) | ||
| 492 | |||
| 493 | if hexOf(kp.PubKey) != v.PubKeyHex { | ||
| 494 | t.Errorf("pubkey mismatch:\n got %s\n want %s", hexOf(kp.PubKey), v.PubKeyHex) | ||
| 495 | } | ||
| 496 | |||
| 497 | // canonical_tags | ||
| 498 | tagsEnc, err := axon.CanonicalTags(v.TagsInput) | ||
| 499 | if err != nil { | ||
| 500 | t.Fatalf("CanonicalTags: %v", err) | ||
| 501 | } | ||
| 502 | if hexOf(tagsEnc) != v.CanonicalTagsHex { | ||
| 503 | t.Errorf("canonical_tags bytes mismatch:\n got %s\n want %s", | ||
| 504 | hexOf(tagsEnc), v.CanonicalTagsHex) | ||
| 505 | } | ||
| 506 | |||
| 507 | tagsHash, err := axon.CanonicalTagsHash(v.TagsInput) | ||
| 508 | if err != nil { | ||
| 509 | t.Fatalf("CanonicalTagsHash: %v", err) | ||
| 510 | } | ||
| 511 | if hexOf(tagsHash[:]) != v.CanonicalTagsHash { | ||
| 512 | t.Errorf("canonical_tags_hash mismatch:\n got %s\n want %s", | ||
| 513 | hexOf(tagsHash[:]), v.CanonicalTagsHash) | ||
| 514 | } | ||
| 515 | |||
| 516 | // canonical_payload | ||
| 517 | content := mustDecodeHex(t, v.ContentHex) | ||
| 518 | payload, err := axon.CanonicalPayload(kp.PubKey, v.CreatedAt, v.Kind, content, v.TagsInput) | ||
| 519 | if err != nil { | ||
| 520 | t.Fatalf("CanonicalPayload: %v", err) | ||
| 521 | } | ||
| 522 | if hexOf(payload) != v.CanonicalPayload { | ||
| 523 | t.Errorf("canonical_payload mismatch:\n got %s\n want %s", | ||
| 524 | hexOf(payload), v.CanonicalPayload) | ||
| 525 | } | ||
| 526 | |||
| 527 | // event id | ||
| 528 | id, err := axon.EventID(kp.PubKey, v.CreatedAt, v.Kind, content, v.TagsInput) | ||
| 529 | if err != nil { | ||
| 530 | t.Fatalf("EventID: %v", err) | ||
| 531 | } | ||
| 532 | if hexOf(id) != v.EventID { | ||
| 533 | t.Errorf("event_id mismatch:\n got %s\n want %s", hexOf(id), v.EventID) | ||
| 534 | } | ||
| 535 | |||
| 536 | // verify the stored signature | ||
| 537 | sig := mustDecodeHex(t, v.SigHex) | ||
| 538 | e := &axon.Event{ | ||
| 539 | ID: id, | ||
| 540 | PubKey: kp.PubKey, | ||
| 541 | CreatedAt: v.CreatedAt, | ||
| 542 | Kind: v.Kind, | ||
| 543 | Content: content, | ||
| 544 | Sig: sig, | ||
| 545 | Tags: v.TagsInput, | ||
| 546 | } | ||
| 547 | if err := axon.Verify(e); err != nil { | ||
| 548 | t.Errorf("Verify stored vector event: %v", err) | ||
| 549 | } | ||
| 550 | } | ||
