Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -415,35 +420,55 @@ 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 +428 to +431
Copy link
Copy Markdown
Member

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.

}
Comment on lines +423 to +432
Copy link
Copy Markdown
Member

@Renegade334 Renegade334 Apr 19, 2026

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From postgresql: https://raw.githubusercontent.com/postgres/postgres/dbf217c1c7c2744a18db489c255255e07cfbb110/src/backend/utils/adt/uuid.c

If the wall clock returns a value that isn't at least SUBMS_MINIMAL_STEP_NS ahead of the last call, the returned timestamp is synthetically bumped forward

Copy link
Copy Markdown
Member Author

@araujogui araujogui Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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)
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;
const seed = ((uuidDataV7[offset + 6] & 0x0f) << 8) | uuidDataV7[offset + 7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidDataV7, offset);
return serializeUUID(uuidDataV7, 0x70, 0x80, offset);
}

function getUnbufferedUUIDv7() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered, 6);
writeTimestamp(uuidNotBuffered, 0);
const seed = ((uuidNotBuffered[6] & 0x0f) << 8) | uuidNotBuffered[7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidNotBuffered, 0);
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
}

Expand Down
25 changes: 18 additions & 7 deletions test/parallel/test-crypto-randomuuidv7.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,28 @@ const {
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
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new monotonic-ordering assertions only exercise the default (buffered) code path. Since this PR changes both buffered and disableEntropyCache: true (unbuffered) implementations, it would be good to add at least one ordering check for randomUUIDv7({ disableEntropyCache: true }) as well, to ensure the unbuffered counter/timestamp logic stays monotonic too.

Copilot uses AI. Check for mistakes.

// Ensure randomUUIDv7 takes no arguments (or ignores them gracefully).
{
const uuid = randomUUIDv7();
Expand Down
Loading