src/signing/nonce.test.ts
v0.2.0 · 5.5 KB
import { describe, expect, it } from 'vitest';
import {
buildCancelNonce,
buildNonce,
buildOrderNonce,
DEFAULT_NONCE_TAG,
DEFAULT_RECV_WINDOW_MS,
isOurNonce,
isRecvTimeWithinWindow,
MAX_RECV_WINDOW_MS,
NONCE_CLIENT_BITS,
NONCE_RANDOM_MAX,
NONCE_TAG_MAX,
nonceReduceIntent,
parseNonce,
recvTimeFor,
recvTimeOf,
} from './nonce.js';
const NOW = 1_780_000_000_000; // ms, synthetic round clock (2026)
describe('nonce layout', () => {
it('has 20 client bits: 12 tag + 1 flag + 7 random', () => {
expect(NONCE_CLIENT_BITS).toBe(20);
expect(NONCE_TAG_MAX).toBe(4095);
expect(NONCE_RANDOM_MAX).toBe(127);
expect(DEFAULT_NONCE_TAG).toBeGreaterThan(0);
expect(DEFAULT_NONCE_TAG).toBeLessThanOrEqual(NONCE_TAG_MAX);
expect(DEFAULT_RECV_WINDOW_MS).toBe(60_000);
expect(MAX_RECV_WINDOW_MS).toBe(100_000);
});
it('places recv_time in the high 44 bits and round-trips every field', () => {
const nonce = buildNonce({ recvTimeMs: NOW + 60_000, tag: 0x5a5, reduceIntent: true, random: 77 });
expect(nonce >> 20n).toBe(BigInt(NOW + 60_000));
const parts = parseNonce(nonce);
expect(parts).toEqual({
recvTimeMs: NOW + 60_000,
clientBits: (0x5a5 << 8) | (1 << 7) | 77,
tag: 0x5a5,
reduceIntent: true,
random: 77,
});
expect(parseNonce(nonce.toString())).toEqual(parts);
expect(recvTimeOf(nonce)).toBe(NOW + 60_000);
});
it('live nonce magnitude is above 2^53 (must be parsed from a string)', () => {
const nonce = buildNonce({ recvTimeMs: NOW + 60_000, random: 0 });
expect(nonce > BigInt(Number.MAX_SAFE_INTEGER)).toBe(true);
expect(nonce.toString()).toHaveLength(19); // ~1.87e18
});
it('uses the default tag and a random field when omitted', () => {
const a = buildNonce({ recvTimeMs: NOW });
const parts = parseNonce(a);
expect(parts.tag).toBe(DEFAULT_NONCE_TAG);
expect(parts.reduceIntent).toBe(false);
expect(parts.random).toBeGreaterThanOrEqual(0);
expect(parts.random).toBeLessThanOrEqual(NONCE_RANDOM_MAX);
// 200 draws stay inside the 7-bit field and are not all equal
const draws = new Set<number>();
for (let i = 0; i < 200; i++) draws.add(parseNonce(buildNonce({ recvTimeMs: NOW })).random);
expect(draws.size).toBeGreaterThan(1);
for (const d of draws) expect(d).toBeLessThanOrEqual(NONCE_RANDOM_MAX);
});
it('validates fields', () => {
expect(() => buildNonce({ recvTimeMs: NOW, tag: 4096 })).toThrow(RangeError);
expect(() => buildNonce({ recvTimeMs: NOW, tag: -1 })).toThrow(RangeError);
expect(() => buildNonce({ recvTimeMs: NOW, random: 128 })).toThrow(RangeError);
expect(() => buildNonce({ recvTimeMs: -1 })).toThrow(RangeError);
expect(() => buildNonce({ recvTimeMs: 1.5 })).toThrow(RangeError);
expect(() => buildNonce({ recvTimeMs: 2 ** 44 })).toThrow(RangeError);
expect(() => parseNonce(-1n)).toThrow(RangeError);
expect(() => parseNonce('abc')).toThrow(TypeError);
});
});
describe('recvTimeFor / buildOrderNonce / buildCancelNonce', () => {
it('defaults to now + 60 s and refuses windows above the documented 100 s', () => {
expect(recvTimeFor(NOW)).toBe(NOW + 60_000);
expect(recvTimeFor(NOW, 30_000)).toBe(NOW + 30_000);
expect(() => recvTimeFor(NOW, 100_001)).toThrow(RangeError);
expect(() => recvTimeFor(NOW, 0)).toThrow(RangeError);
expect(() => recvTimeFor(-5)).toThrow(RangeError);
});
it('buildOrderNonce carries tag and flag bit; buildCancelNonce carries 20 random bits', () => {
const o = buildOrderNonce(NOW, { tag: 0x123, reduceIntent: true, random: 3 });
expect(parseNonce(o)).toEqual({
recvTimeMs: NOW + 60_000,
clientBits: (0x123 << 8) | 128 | 3,
tag: 0x123,
reduceIntent: true,
random: 3,
});
const c = buildCancelNonce(NOW, { random: 0xfffff });
expect(c >> 20n).toBe(BigInt(NOW + 60_000));
expect(c & 0xfffffn).toBe(0xfffffn);
expect(() => buildCancelNonce(NOW, { random: 0x100000 })).toThrow(RangeError);
const auto = buildCancelNonce(NOW);
expect(auto >> 20n).toBe(BigInt(NOW + 60_000));
});
});
describe('isOurNonce / nonceReduceIntent', () => {
it('recognises the tag and reads the flag bit', () => {
const mine = buildNonce({ recvTimeMs: NOW, tag: 0x321, reduceIntent: true, random: 5 });
expect(isOurNonce(mine, 0x321)).toBe(true);
expect(isOurNonce(mine, 0x322)).toBe(false);
expect(isOurNonce(mine.toString(), 0x321)).toBe(true);
expect(nonceReduceIntent(mine)).toBe(true);
const entry = buildNonce({ recvTimeMs: NOW, tag: 0x321, reduceIntent: false, random: 5 });
expect(nonceReduceIntent(entry)).toBe(false);
});
it('treats a foreign nonce with zero client bits as not ours', () => {
const foreign = BigInt(NOW) << 20n;
expect(isOurNonce(foreign)).toBe(false);
expect(isOurNonce(foreign, 0)).toBe(true); // tag 0 is legal but matches untagged orders: do not use it
});
it('uses the default tag when none is passed', () => {
expect(isOurNonce(buildNonce({ recvTimeMs: NOW }))).toBe(true);
expect(() => isOurNonce(1n, 5000)).toThrow(RangeError);
});
});
describe('isRecvTimeWithinWindow', () => {
it('accepts a deadline in (now, now + 100 s]', () => {
const n = buildOrderNonce(NOW);
expect(isRecvTimeWithinWindow(n, NOW)).toBe(true);
expect(isRecvTimeWithinWindow(n, NOW + 60_000)).toBe(false); // expired
expect(isRecvTimeWithinWindow(n, NOW - 50_000)).toBe(false); // clock drifted: too far ahead
expect(isRecvTimeWithinWindow(n, NOW - 40_000)).toBe(true);
});
});