diff --git a/README.md b/README.md index 6d10058..1c5d735 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,10 @@ random60: 0x03699c13c10128b5 **Global lookup by public ID** GSI: `GSI1PK = PUBLICID#{orderlyid}`, `GSI1SK = CONST` -**Postgres / MySQL** -- Store both `id_text VARCHAR(64) UNIQUE` and `id_bin BINARY(20)` +**SQL storage** +- Store the text form as `id_text VARCHAR(69) UNIQUE` +- Use `VARCHAR(64)` only if checksum suffixes are never stored +- Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL - Index `id_bin` for range scans - Use `id_text` for APIs/logs diff --git a/orderlyid_test.go b/orderlyid_test.go index 4db6307..74e292a 100644 --- a/orderlyid_test.go +++ b/orderlyid_test.go @@ -54,7 +54,11 @@ func TestChecksumRoundTrip(t *testing.T) { t.Fatalf("parse with checksum failed: %v", err) } // Tamper last char - bad := id[:len(id)-1] + "0" + repl := byte('0') + if id[len(id)-1] == repl { + repl = '1' + } + bad := id[:len(id)-1] + string(repl) if _, err := Parse(bad); err == nil { t.Fatalf("expected checksum mismatch") } else if !errors.Is(err, ErrInvalidChecksum) { diff --git a/spec/0001-spec.md b/spec/0001-spec.md index 8c23a47..6bc21ea 100644 --- a/spec/0001-spec.md +++ b/spec/0001-spec.md @@ -54,23 +54,22 @@ OrderlyID provides typed, time-sortable, globally unique identifiers optimized f ## 6. Checksum -1. `now_ms = system_clock_ms()`. If privacy enabled, quantize. -2. If another ID in the same ms: increment 12-bit seq; wrap allowed. -3. Draw 60 bits from CSPRNG. -4. Pack fields into 160-bit body, encode to 32 Base32 chars. -5. Emit `"_"`; append checksum if configured. - -*Ordering semantics*: within same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. +- Domain: HRP-expanded `"_"` plus payload symbols. +- Algorithm: Bech32 polymod, truncated to 20 bits and encoded as 4 Crockford Base32 characters. +- Verification: if present, parsers MUST validate the checksum and reject mismatches. +- False-accept probability: approximately 1 in 1,048,576. +- The checksum is an integrity check for transcription errors, not cryptographic authentication. ## 7. Generation -1. `now_ms = system_clock_ms()`. If privacy enabled, quantize. -2. If another ID in the same ms: increment 12-bit seq; wrap allowed. -3. Draw 60 bits from CSPRNG. -4. Pack fields into 160-bit body, encode to 32 Base32 chars. -5. Emit `"_"`; append checksum if configured. +1. `now_ms = system_clock_ms()`. If privacy bucketing is enabled, quantize to the configured bucket size. +2. If another ID is generated in the same millisecond, increment the 12-bit `seq`; wrap is allowed. +3. Draw 60 bits from a CSPRNG. +4. Pack fields into the 160-bit body in big-endian order. +5. Encode the body into exactly 32 Crockford Base32 characters. +6. Emit `"_"`; append `"-"` if checksum is configured. -*Ordering semantics*: within same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. +*Ordering semantics*: within the same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. --- @@ -96,7 +95,7 @@ OrderlyID provides typed, time-sortable, globally unique identifiers optimized f ## 10. Interop & Tests -- **SQL**: store `id_text VARCHAR(64)` and `id_bin BINARY(20)`; index `id_bin` for range scans. +- **SQL**: store the text form as `id_text VARCHAR(69) UNIQUE`; use `VARCHAR(64)` only if checksum suffixes are never stored. Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL; index `id_bin` for range scans. - **DynamoDB**: use as sort key; shard global feeds via virtual partitions. - **APIs/logs**: emit typed ID with checksum externally; redact tail if sensitive. - **Conformance**: implementations claiming v1 MUST: diff --git a/spec/README.md b/spec/README.md index 69fb674..d645b09 100644 --- a/spec/README.md +++ b/spec/README.md @@ -94,7 +94,7 @@ checksum: 9xgg ## Interop & storage guidance -- **SQL**: store both `id_text VARCHAR(64) UNIQUE` and `id_bin BINARY(20)`; index `id_bin` for range scans. +- **SQL**: store the text form as `id_text VARCHAR(69) UNIQUE`; use `VARCHAR(64)` only if checksum suffixes are never stored. Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL; index `id_bin` for range scans. - **DynamoDB**: use ID as sort key; to avoid hot partitions, hash into virtual shards. - **APIs & logs**: prefer the typed public ID; redact tail chars if sensitive.