mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
docs(quartz): document Hex/HexKey utilities for discoverability
AI agents integrating Quartz were re-implementing hex encoding or pulling in third-party codecs because the built-in helpers weren't discoverable. - Add KDoc to the `HexKey` typealias, its extension functions (`toHexKey`/`hexToByteArray`/`hexToByteArrayOrNull`/`isValid`) and the `PUBKEY_LENGTH`/`EVENT_ID_LENGTH` constants. - Add KDoc to the `Hex` object and its public API (`isHex`/`isHex64`/`decode`/`encode`/`isEqual`). - Add a dedicated "Hex utilities" section + quick-reference rows to the quartz-integration skill, and fix the wrong `HexKey.decodeHex(hex)` snippet (no such API) to the real `hex.hexToByteArray()`. - Add a "Hex Encoding" section to the nostr-expert skill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HM5uueF4a17umGpjC8wcz
This commit is contained in:
@@ -345,6 +345,32 @@ object Nip04 {
|
||||
|
||||
**Note**: Use NIP-44 (`Nip44`) for new implementations. NIP-04 has security issues.
|
||||
|
||||
## Hex Encoding (HexKey ↔ ByteArray)
|
||||
|
||||
Pubkeys, event ids and signatures are lower-case hex. Quartz uses the `HexKey`
|
||||
typealias (`= String`) plus extensions in `nip01Core/core/HexKey.kt`, backed by
|
||||
the `Hex` object in `utils/Hex.kt`. **Use these — never hand-roll a byte loop or
|
||||
import a third-party hex codec.**
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isValid
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
val hex: HexKey = bytes.toHexKey() // ByteArray -> lower-case hex
|
||||
val back: ByteArray = hex.hexToByteArray() // hex -> ByteArray (throws on odd length)
|
||||
val safe: ByteArray? = input.hexToByteArrayOrNull() // null on invalid hex
|
||||
|
||||
Hex.isHex(input) // valid hex, any length
|
||||
Hex.isHex64(input) // ~30% faster fast-path for a 32-byte key/id
|
||||
hex.isValid() // 64 chars + valid hex (pubkey / event-id shape)
|
||||
Hex.isEqual(hex, bytes) // compare hex to bytes without decoding
|
||||
```
|
||||
|
||||
Constants `PUBKEY_LENGTH` / `EVENT_ID_LENGTH` (both 64) live in `nip01Core.core`.
|
||||
|
||||
## Bech32 Encoding (NIP-19)
|
||||
|
||||
Encoding uses extension functions on `ByteArray` (`nip19Bech32/ByteArrayExt.kt`);
|
||||
|
||||
@@ -134,13 +134,14 @@ val privKeyHex: String? = keyPair.privKey?.toHexKey()
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
|
||||
// ByteArray → hex
|
||||
val hex = byteArray.toHexKey()
|
||||
|
||||
// hex → ByteArray
|
||||
val bytes = HexKey.decodeHex(hex)
|
||||
val bytes = hex.hexToByteArray()
|
||||
|
||||
// Bech32 import (npub, nsec)
|
||||
val parsed = Nip19Parser.uriToRoute("npub1abc...")
|
||||
@@ -148,6 +149,55 @@ val parsed = Nip19Parser.uriToRoute("npub1abc...")
|
||||
val parsed = Nip19Parser.uriToRoute("nsec1abc...")
|
||||
```
|
||||
|
||||
> Hex ↔ ByteArray is a first-class utility in Quartz — see **§3.1 Hex utilities** below.
|
||||
|
||||
---
|
||||
|
||||
### 3.1 Hex utilities (HexKey ↔ ByteArray)
|
||||
|
||||
Nostr keys, event ids and signatures travel as lower-case hex strings. Quartz
|
||||
models this with the `HexKey` typealias (just a `String`) plus extension
|
||||
functions — **do not** write your own byte loop or pull in a third-party codec.
|
||||
|
||||
**Packages:** `com.vitorpamplona.quartz.nip01Core.core` (the extensions) and
|
||||
`com.vitorpamplona.quartz.utils` (the underlying `Hex` object).
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey // typealias = String
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey // ByteArray → hex
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray // hex → ByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isValid
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
// Encode / decode
|
||||
val hex: HexKey = pubKeyBytes.toHexKey() // lower-case, 2 chars per byte
|
||||
val bytes: ByteArray = hex.hexToByteArray() // throws on odd length
|
||||
|
||||
// Untrusted input → decode safely
|
||||
val maybe: ByteArray? = userInput.hexToByteArrayOrNull() // null if not valid hex
|
||||
|
||||
// Validate without decoding (no allocation)
|
||||
Hex.isHex(userInput) // even-length, all hex digits (any length)
|
||||
Hex.isHex64(userInput) // fast path for a 32-byte key/id (checks first 64 chars)
|
||||
hex.isValid() // 64 chars AND valid hex (pubkey / event-id shape)
|
||||
|
||||
// Compare a hex string to raw bytes without decoding
|
||||
Hex.isEqual(incomingHexId, myIdBytes)
|
||||
```
|
||||
|
||||
| Need | Call | Notes |
|
||||
|------|------|-------|
|
||||
| ByteArray → hex | `bytes.toHexKey()` | lower-case output |
|
||||
| hex → ByteArray (strict) | `hex.hexToByteArray()` | throws on odd length |
|
||||
| hex → ByteArray (safe) | `hex.hexToByteArrayOrNull()` | `null` on invalid hex |
|
||||
| is this valid hex? | `Hex.isHex(s)` / `Hex.isHex64(s)` | `isHex64` ~30% faster for keys/ids |
|
||||
| is this a pubkey/id shape? | `hex.isValid()` | 64 chars + valid hex |
|
||||
| hex == bytes? | `Hex.isEqual(hex, bytes)` | no decode allocation |
|
||||
|
||||
Constants `PUBKEY_LENGTH` and `EVENT_ID_LENGTH` (both `64`) live in the same
|
||||
`nip01Core.core` package.
|
||||
|
||||
---
|
||||
|
||||
## 4. Signing Events
|
||||
@@ -644,6 +694,9 @@ val results = store.query<Event>(Filter(search = "bitcoin"))
|
||||
| Sign event | `signer.sign(template)` | `nip01Core.signers` |
|
||||
| Serialize | `event.toJson()` | `nip01Core.core` |
|
||||
| Parse | `Event.fromJson(json)` | `nip01Core.core` |
|
||||
| ByteArray → hex | `bytes.toHexKey()` | `nip01Core.core` |
|
||||
| hex → ByteArray | `hex.hexToByteArray()` / `hex.hexToByteArrayOrNull()` | `nip01Core.core` |
|
||||
| Validate hex | `Hex.isHex(s)` / `Hex.isHex64(s)` / `hex.isValid()` | `utils`, `nip01Core.core` |
|
||||
| Normalize relay URL | `RelayUrlNormalizer.normalize("wss://...")` | `nip01Core.relay.normalizer` |
|
||||
| Setup relay client | `NostrClient(BasicOkHttpWebSocket.Builder { okhttp })` | `nip01Core.relay.client` |
|
||||
| Subscribe | `client.openReqSubscription(subId, mapOf(relay to filters), listener)` | `nip01Core.relay.client` |
|
||||
|
||||
@@ -22,17 +22,49 @@ package com.vitorpamplona.quartz.nip01Core.core
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
/** Makes the distinction between String and Hex * */
|
||||
/**
|
||||
* A lower-case hexadecimal string. This is the canonical wire format for the
|
||||
* 32-byte values Nostr deals with everywhere: public keys, event ids, and the
|
||||
* 64-byte Schnorr signature. It is only a [String] alias — it documents intent
|
||||
* and does no validation on its own — so pair it with [isValid] (or [Hex.isHex])
|
||||
* whenever the value comes from an untrusted source.
|
||||
*
|
||||
* Convert with the extension functions below rather than reaching for a byte
|
||||
* loop or a third-party codec:
|
||||
*
|
||||
* ```kotlin
|
||||
* import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
* import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
*
|
||||
* val hex: HexKey = pubKeyBytes.toHexKey() // ByteArray -> hex
|
||||
* val bytes: ByteArray = hex.hexToByteArray() // hex -> ByteArray
|
||||
* ```
|
||||
*
|
||||
* For byte-array-free, allocation-light checks use [Hex.isHex64] (32-byte keys
|
||||
* and ids) or [Hex.isEqual] (compare a hex string to raw bytes without decoding).
|
||||
*/
|
||||
typealias HexKey = String
|
||||
|
||||
/** Encodes these bytes as a lower-case [HexKey]. Inverse of [hexToByteArray]. */
|
||||
fun ByteArray.toHexKey(): HexKey = Hex.encode(this)
|
||||
|
||||
/**
|
||||
* Decodes this hex string into its bytes. Inverse of [toHexKey].
|
||||
*
|
||||
* Accepts upper- or lower-case input but requires an even length; throws
|
||||
* [IllegalArgumentException] on odd-length input. Use [hexToByteArrayOrNull]
|
||||
* when the string may contain non-hex characters.
|
||||
*/
|
||||
fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
|
||||
|
||||
/** Like [hexToByteArray] but returns null instead of throwing when this is not valid hex. */
|
||||
fun HexKey.hexToByteArrayOrNull(): ByteArray? = if (Hex.isHex(this)) Hex.decode(this) else null
|
||||
|
||||
/** True when this is a 64-char (32-byte) hex string — the shape of a pubkey or event id. */
|
||||
fun HexKey.isValid(): Boolean = length == PUBKEY_LENGTH && Hex.isHex(this)
|
||||
|
||||
/** Length in hex chars of a 32-byte public key. */
|
||||
const val PUBKEY_LENGTH = 64
|
||||
|
||||
/** Length in hex chars of a 32-byte event id. */
|
||||
const val EVENT_ID_LENGTH = 64
|
||||
|
||||
@@ -20,6 +20,24 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
/**
|
||||
* Fast, allocation-conscious hex codec used throughout Quartz for keys, event
|
||||
* ids and signatures. Backed by pre-computed lookup tables and benchmarked
|
||||
* against the secp256k1 codec and Kotlin's stdlib `HexFormat` (see
|
||||
* `benchmark/.../HexBenchmark.kt`).
|
||||
*
|
||||
* Most call sites should prefer the extension functions in
|
||||
* [com.vitorpamplona.quartz.nip01Core.core] — `ByteArray.toHexKey()` and
|
||||
* `HexKey.hexToByteArray()` — which delegate here. Reach for this object
|
||||
* directly when you want to validate without decoding ([isHex] / [isHex64]) or
|
||||
* compare a hex string to raw bytes without allocating ([isEqual]).
|
||||
*
|
||||
* ```kotlin
|
||||
* val hex = Hex.encode(bytes) // ByteArray -> lower-case hex
|
||||
* val bytes = Hex.decode(hex) // hex (any case) -> ByteArray
|
||||
* if (Hex.isHex64(id)) { ... } // is this a valid 32-byte hex id?
|
||||
* ```
|
||||
*/
|
||||
object Hex {
|
||||
private const val LOWER_CASE_HEX = "0123456789abcdef"
|
||||
private const val UPPER_CASE_HEX = "0123456789ABCDEF"
|
||||
@@ -36,6 +54,12 @@ object Hex {
|
||||
(LOWER_CASE_HEX[(it shr 4)].code shl 8) or LOWER_CASE_HEX[(it and 0xF)].code
|
||||
}
|
||||
|
||||
/**
|
||||
* True when [hex] is a non-null, even-length string of only hex digits
|
||||
* (upper or lower case). Rejects odd lengths and stray non-hex chars (e.g.
|
||||
* emoji in `p` tags) instead of throwing. ~47ns in debug on the Emulator;
|
||||
* use [isHex64] when the length is known to be 64.
|
||||
*/
|
||||
// 47ns in debug on the Emulator
|
||||
fun isHex(hex: String?): Boolean {
|
||||
if (hex == null) return false
|
||||
@@ -63,6 +87,12 @@ object Hex {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the first 64 chars of [hex] as hex digits — the fast path for
|
||||
* checking a 32-byte pubkey or event id. ~30% faster than [isHex] because
|
||||
* the length is fixed and the checks are unrolled. Assumes [hex] is at least
|
||||
* 64 chars long; it does not verify the total length.
|
||||
*/
|
||||
// 30% faster than isHex
|
||||
fun isHex64(hex: String): Boolean =
|
||||
try {
|
||||
@@ -144,6 +174,12 @@ object Hex {
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes [hex] (upper or lower case) into bytes. Requires an even length —
|
||||
* throws [IllegalArgumentException] otherwise. Does not itself validate the
|
||||
* characters, so guard untrusted input with [isHex] first (or use
|
||||
* `HexKey.hexToByteArrayOrNull()`).
|
||||
*/
|
||||
fun decode(hex: String): ByteArray {
|
||||
// faster version of hex decoder
|
||||
require(hex.length and 1 == 0) {
|
||||
@@ -154,6 +190,7 @@ object Hex {
|
||||
}
|
||||
}
|
||||
|
||||
/** Encodes [input] as a lower-case hex string (two chars per byte). */
|
||||
fun encode(input: ByteArray): String {
|
||||
val out = CharArray(input.size * 2)
|
||||
var outIdx = 0
|
||||
@@ -165,6 +202,12 @@ object Hex {
|
||||
return out.concatToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the hex string [id] encodes exactly the bytes [ourId], compared
|
||||
* without allocating a decode buffer. Handy for matching an incoming hex id
|
||||
* against bytes you already hold. Assumes [id] is at least `2 * ourId.size`
|
||||
* chars and lower-case (as produced by [encode]).
|
||||
*/
|
||||
fun isEqual(
|
||||
id: String,
|
||||
ourId: ByteArray,
|
||||
|
||||
Reference in New Issue
Block a user