-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
crypto: add uuidv7 monotonic counter #62601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
5e312c1
19bb93c
a5f3be5
2f90718
c798973
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -348,6 +348,11 @@ let uuidData; | |
| let uuidNotBuffered; | ||
| let uuidBatch = 0; | ||
|
|
||
| let uuidDataV7; | ||
| let uuidBatchV7 = 0; | ||
| let v7LastTimestamp = -1; | ||
| let v7Counter = 0; | ||
|
|
||
| let hexBytesCache; | ||
| function getHexBytes() { | ||
| if (hexBytesCache === undefined) { | ||
|
|
@@ -415,35 +420,77 @@ function randomUUID(options) { | |
| return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID(); | ||
| } | ||
|
|
||
| function writeTimestamp(buf, offset) { | ||
| function advanceV7(seed) { | ||
| const now = DateNow(); | ||
| const msb = now / (2 ** 32); | ||
| if (now > v7LastTimestamp) { | ||
| v7LastTimestamp = now; | ||
| v7Counter = seed & 0xFFF; | ||
| } else { | ||
| v7Counter++; | ||
| if (v7Counter > 0xFFF) { | ||
| v7LastTimestamp++; | ||
| v7Counter = 0; | ||
| } | ||
| } | ||
|
Comment on lines
+425
to
+434
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is problematic if the clock moves backwards significantly, as the timestamp ends up frozen until such a time as the clock catches up. I don't know how other implementations handle this possibility. |
||
| } | ||
|
|
||
| function writeTimestampAndCounterV7(buf, offset) { | ||
| const ts = v7LastTimestamp; | ||
| const msb = ts / (2 ** 32); | ||
| buf[offset] = msb >>> 8; | ||
| buf[offset + 1] = msb; | ||
| buf[offset + 2] = now >>> 24; | ||
| buf[offset + 3] = now >>> 16; | ||
| buf[offset + 4] = now >>> 8; | ||
| buf[offset + 5] = now; | ||
| buf[offset + 2] = ts >>> 24; | ||
| buf[offset + 3] = ts >>> 16; | ||
| buf[offset + 4] = ts >>> 8; | ||
| buf[offset + 5] = ts; | ||
| buf[offset + 6] = (v7Counter >>> 8) & 0x0f; | ||
| buf[offset + 7] = v7Counter & 0xff; | ||
| } | ||
|
|
||
| function getBufferedUUIDv7() { | ||
| uuidData ??= secureBuffer(16 * kBatchSize); | ||
| if (uuidData === undefined) | ||
| function getBufferedUUIDv7(monotonic) { | ||
| uuidDataV7 ??= secureBuffer(16 * kBatchSize); | ||
| if (uuidDataV7 === undefined) | ||
| throw new ERR_OPERATION_FAILED('Out of memory'); | ||
|
|
||
| if (uuidBatch === 0) randomFillSync(uuidData); | ||
| uuidBatch = (uuidBatch + 1) % kBatchSize; | ||
| const offset = uuidBatch * 16; | ||
| writeTimestamp(uuidData, offset); | ||
| return serializeUUID(uuidData, 0x70, 0x80, offset); | ||
| if (uuidBatchV7 === 0) randomFillSync(uuidDataV7); | ||
| uuidBatchV7 = (uuidBatchV7 + 1) % kBatchSize; | ||
| const offset = uuidBatchV7 * 16; | ||
| if (monotonic) { | ||
| const seed = ((uuidDataV7[offset + 6] & 0x0f) << 8) | uuidDataV7[offset + 7]; | ||
| advanceV7(seed); | ||
| writeTimestampAndCounterV7(uuidDataV7, offset); | ||
| } else { | ||
| const now = DateNow(); | ||
| const msb = now / (2 ** 32); | ||
| uuidDataV7[offset] = msb >>> 8; | ||
| uuidDataV7[offset + 1] = msb; | ||
| uuidDataV7[offset + 2] = now >>> 24; | ||
| uuidDataV7[offset + 3] = now >>> 16; | ||
| uuidDataV7[offset + 4] = now >>> 8; | ||
| uuidDataV7[offset + 5] = now; | ||
| } | ||
| return serializeUUID(uuidDataV7, 0x70, 0x80, offset); | ||
| } | ||
|
|
||
| function getUnbufferedUUIDv7() { | ||
| function getUnbufferedUUIDv7(monotonic) { | ||
| uuidNotBuffered ??= secureBuffer(16); | ||
| if (uuidNotBuffered === undefined) | ||
| throw new ERR_OPERATION_FAILED('Out of memory'); | ||
| randomFillSync(uuidNotBuffered, 6); | ||
| writeTimestamp(uuidNotBuffered, 0); | ||
| if (monotonic) { | ||
| const seed = ((uuidNotBuffered[6] & 0x0f) << 8) | uuidNotBuffered[7]; | ||
| advanceV7(seed); | ||
| writeTimestampAndCounterV7(uuidNotBuffered, 0); | ||
| } else { | ||
| const now = DateNow(); | ||
| const msb = now / (2 ** 32); | ||
| uuidNotBuffered[0] = msb >>> 8; | ||
| uuidNotBuffered[1] = msb; | ||
| uuidNotBuffered[2] = now >>> 24; | ||
| uuidNotBuffered[3] = now >>> 16; | ||
| uuidNotBuffered[4] = now >>> 8; | ||
| uuidNotBuffered[5] = now; | ||
| } | ||
| return serializeUUID(uuidNotBuffered, 0x70, 0x80); | ||
| } | ||
|
|
||
|
|
@@ -452,11 +499,14 @@ function randomUUIDv7(options) { | |
| validateObject(options, 'options'); | ||
| const { | ||
| disableEntropyCache = false, | ||
| monotonic = true, | ||
|
araujogui marked this conversation as resolved.
Outdated
|
||
| } = options || kEmptyObject; | ||
|
|
||
| validateBoolean(disableEntropyCache, 'options.disableEntropyCache'); | ||
| validateBoolean(monotonic, 'options.monotonic'); | ||
|
|
||
| return disableEntropyCache ? getUnbufferedUUIDv7() : getBufferedUUIDv7(); | ||
| return disableEntropyCache ? | ||
| getUnbufferedUUIDv7(monotonic) : getBufferedUUIDv7(monotonic); | ||
| } | ||
|
|
||
| function createRandomPrimeJob(type, size, options) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,24 +56,37 @@ const { | |
| const timestamp = parseInt(timestampHex, 16); | ||
|
|
||
| assert(timestamp >= before, `Timestamp ${timestamp} < before ${before}`); | ||
| assert(timestamp <= after, `Timestamp ${timestamp} > after ${after}`); | ||
| // The monotonic counter may have overflowed in a prior call and advanced | ||
| // v7LastTimestamp by 1 ms beyond wall-clock time (RFC 9562 §6.2 allows this). | ||
| assert(timestamp <= after + 1, `Timestamp ${timestamp} > after+1 ${after + 1}`); | ||
| } | ||
|
|
||
| { | ||
| let prev = randomUUIDv7(); | ||
| for (let i = 0; i < 100; i++) { | ||
| const curr = randomUUIDv7(); | ||
| // UUIDs with later timestamps must sort after earlier ones. | ||
| // Within the same millisecond, ordering depends on random bits, | ||
| // so we only assert >= on the timestamp portion. | ||
| const prevTs = parseInt(prev.replace(/-/g, '').slice(0, 12), 16); | ||
| const currTs = parseInt(curr.replace(/-/g, '').slice(0, 12), 16); | ||
| assert(currTs >= prevTs, | ||
| `Timestamp went backwards: ${currTs} < ${prevTs}`); | ||
| // With a monotonic counter in rand_a, each UUID must be strictly greater | ||
| // than the previous regardless of whether the timestamp changed. | ||
| assert(curr > prev, | ||
| `UUID ordering violated: ${curr} <= ${prev}`); | ||
| prev = curr; | ||
| } | ||
| } | ||
|
|
||
| // Sub-millisecond ordering: a tight synchronous burst exercises the counter | ||
| // increment path and must also produce strictly increasing UUIDs. | ||
| { | ||
| const burst = []; | ||
| for (let i = 0; i < 500; i++) { | ||
| burst.push(randomUUIDv7()); | ||
| } | ||
| for (let i = 1; i < burst.length; i++) { | ||
| assert(burst[i] > burst[i - 1], | ||
| `Sub-millisecond ordering violated at index ${i}: ` + | ||
| `${burst[i]} <= ${burst[i - 1]}`); | ||
| } | ||
| } | ||
|
Comment on lines
62
to
+88
|
||
|
|
||
| // Ensure randomUUIDv7 takes no arguments (or ignores them gracefully). | ||
| { | ||
| const uuid = randomUUIDv7(); | ||
|
|
@@ -92,13 +105,22 @@ const { | |
| assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex); | ||
| assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex); | ||
|
|
||
| // monotonic: false — rand_a is random; UUIDs must still be valid but are not | ||
| // guaranteed to be strictly ordered within the same millisecond. | ||
| assert.match(randomUUIDv7({ monotonic: false }), uuidv7Regex); | ||
| assert.match(randomUUIDv7({ monotonic: false, disableEntropyCache: true }), uuidv7Regex); | ||
|
|
||
| assert.throws(() => randomUUIDv7(1), { | ||
| code: 'ERR_INVALID_ARG_TYPE', | ||
| }); | ||
|
|
||
| assert.throws(() => randomUUIDv7({ disableEntropyCache: '' }), { | ||
| code: 'ERR_INVALID_ARG_TYPE', | ||
| }); | ||
|
|
||
| assert.throws(() => randomUUIDv7({ monotonic: 1 }), { | ||
| code: 'ERR_INVALID_ARG_TYPE', | ||
| }); | ||
| } | ||
|
|
||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prematurely incrementing the timestamp is a permissible way to handle counter overflow in the RFC, but I think we need to be really clear in the documentation that the potential timestamp drift in the generated UUIDs is effectively unbounded, and could diverge ad infinitum if generating UUIDs at very high frequency.