RipRun runs rip-chain's three routers in Java FILIUS, each router's random set by reflection to the core's ripSeed, and writes the frames, every change of each RIP table and the calls. converge: the tables fill, a ping at 45 s. expire: R3 falls silent at 10 s; R2 marks its network 16 hops only at its own next beacon, R1 passes it on at once, the ping gets ICMP 3/0. Both match: per sending card the same messages at the same times, every table through the same states. The first 5 s are compared by the state the tables reach, since Java's routers start one after the other. Measured on the JDK: Java's pause divides nextFloat() by 3 in float; the core does too now (a millisecond off in one draw of about 10,000 before). Scenarios name their RIP routers in the new field rip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
// Ported from filius-ipad (GPLv2/GPLv3):
|
|
// State/TopologyNetworkRuntimeEngine.swift (TopologyJavaCompatibleRandom)
|
|
// A bit-exact java.util.Random: the 48-bit linear congruential generator
|
|
// Java FILIUS draws its RIP beacon jitter and ephemeral ports from. BigInt
|
|
// keeps the 48-bit arithmetic exact; a double would lose the low bits.
|
|
|
|
const MULTIPLIER = 0x5deece66dn;
|
|
const ADDEND = 0xbn;
|
|
const MASK = (1n << 48n) - 1n;
|
|
const INT32_MAX = 2147483647n;
|
|
|
|
export class JavaCompatibleRandom {
|
|
private state: bigint;
|
|
|
|
constructor(seed: bigint | number) {
|
|
this.state = (BigInt.asUintN(64, BigInt(seed)) ^ MULTIPLIER) & MASK;
|
|
}
|
|
|
|
/** The internal 48-bit state, for tests and for copying a generator. */
|
|
get seed(): bigint {
|
|
return this.state;
|
|
}
|
|
|
|
clone(): JavaCompatibleRandom {
|
|
const copy = new JavaCompatibleRandom(0);
|
|
copy.state = this.state;
|
|
return copy;
|
|
}
|
|
|
|
next(bits: number): number {
|
|
if (!(bits > 0 && bits <= 32)) throw new RangeError(`bits out of range: ${bits}`);
|
|
this.state = (this.state * MULTIPLIER + ADDEND) & MASK;
|
|
return Number(this.state >> BigInt(48 - bits));
|
|
}
|
|
|
|
nextInt(upperBound: number): number {
|
|
if (!(upperBound > 0)) throw new RangeError(`upperBound must be positive: ${upperBound}`);
|
|
const bound = BigInt(upperBound);
|
|
if ((upperBound & -upperBound) === upperBound) {
|
|
return Number((bound * BigInt(this.next(31))) >> 31n);
|
|
}
|
|
for (;;) {
|
|
const bits = BigInt(this.next(31));
|
|
const value = bits % bound;
|
|
if (bits - value + (bound - 1n) <= INT32_MAX) return Number(value);
|
|
}
|
|
}
|
|
|
|
nextFloat(): number {
|
|
return Math.fround(this.next(24) / (1 << 24));
|
|
}
|
|
|
|
/** Java's nextDouble (what Math.random draws): 53 random bits over 2^53. */
|
|
nextDouble(): number {
|
|
return (this.next(26) * 2 ** 27 + this.next(27)) / 2 ** 53;
|
|
}
|
|
|
|
/**
|
|
* Java RIP: 30 s · (f/3 + 0.84), truncated — somewhere between 25.2 s and
|
|
* 35.2 s. f/3 divides a float by an int, in float (JLS 5.6.2): one draw in
|
|
* about 10,000 comes out a millisecond apart from dividing in double
|
|
* (measured on OpenJDK 21).
|
|
*/
|
|
nextRipJitterMilliseconds(): number {
|
|
return Math.trunc(30_000 * (Math.fround(this.nextFloat() / 3) + 0.84));
|
|
}
|
|
}
|