Skip to content

TimestampFragment values overflow when bits >= 48 #8

@seimmuc

Description

@seimmuc

Here's a snippet from TimestampFragment's constructor:

if (bits >= 58) {
// nanosecond time unit
this.timeUnit = BigInt(1);
this.epoch = BigInt(epoch * 10 ** 6);
} else if (bits >= 48) {
// microsecond time unit
this.timeUnit = BigInt(10 ** 3);
this.epoch = BigInt(epoch * 10 ** 3);
} else {
// millisecond time unit
this.timeUnit = BigInt(10 ** 6);
this.epoch = BigInt(epoch);
}

The number of bits allocated to the timestamp fragment determines the timestamp's precision. This behavior is not documented, except in a private method's docs which the user of this library would not see. It is also counterintuitive, and limiting. The user should be able to control when the extra bits should be used to improve timestamp precision, or if they should merely extend the lifespan of their ID format. For example, it is impossible to create a Snowflakify object for Mastodon's snowflake IDs (48-bit millisecond timestamp with UNIX epoch + 16-bits sequence number) without overwriting its private properties.

The original snowflake allocates 42 bits to the timestamp which is stored with a millisecond precision. This makes it good for 139 years, or 69 years into the future if the first bit is reserved for compatibility with signed integers. Using Discord's epoch (which is this library's default), this makes the standard snowflake ID usable until 2154-05-15T07:35:11.103Z; or 2084-09-06T15:47:35.551Z with the reserved bit. As of right now only 39 bits are actually used, and the remaining 2-3 bits are and will remain zeroes until June 2032. Meanwhile Mastodon's IDs are good until 10889 CE, which is probably a massive overkill.

The hard-coded magic values in TimestampFragment's constructor were not chosen with future proofing or an earlier custom epoch in mind. If you call new TimestampFragment(48).getValue(), it will return a 49-bit bigint. Since Snowflakify.nextId() doesn't check or mask fragment values, it will happily add it to the ID, which either invades the bits of the fragment on the left or grows the ID above totalBits size. The 48-bit timestamps broke this way on 2023-12-02T19:29:36.710Z, while 58-bit ones followed on 2024-02-18T23:59:36.151Z.

The reason I'm writing this as an issue as opposed to a pull request is because I don't know the best way to fix this. Ideally the magic numbers should be removed completely and replaced with another constructor parameter, but this could break things for any current users of this library. Adding an optional parameter might be the best option, but it feels wrong. And should TimestampFragment.getValue() just mask the return value or detect overflows and throw an error?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions