Improve LCRNG seed reversal speed ~50x

Big thanks to Parzival from the RNG discord community for chiseling the LCRNG search space down to the best performing implementation possible.

50x? Down from O(2^8) -> O(2^3) is 32x, but we no longer need to access two heap arrays (262KB no longer needed!). Everything can be calculated tightly with the stack.

f641f3eab2/RNG/LCG_Reversal.py (L31)

Rainbow tables is the only faster implementation. However, nobody is gonna hog many GB of RAM for O(1) reversals. This is ~O(2^3), down from O(2^8). Much better than the days of O(2^16)!
This commit is contained in:
Kurt 2022-10-16 18:52:43 -07:00
parent d012134d23
commit 2a190dd6b3
2 changed files with 69 additions and 181 deletions

View file

@ -6,57 +6,17 @@ namespace PKHeX.Core;
/// Seed reversal logic for the <see cref="LCRNG"/> algorithm.
/// </summary>
/// <remarks>
/// Use a meet-in-the-middle attack to reduce the search space to 2^8 instead of 2^16
/// flag/2^8 tables are precomputed and constant (unrelated to rand pairs)
/// https://crypto.stackexchange.com/a/10609
/// Abuses the pattern in Low16 bit results of rand() to reduce the search space to 2^2 instead of 2^16.
/// https://github.com/StarfBerry/poke-scripts/blob/f641f3eab264e2caf1ee7c3c0e5553a9d8086921/RNG/LCG_Reversal.py#L19-L93
/// </remarks>
public static class LCRNGReversal
{
// Bruteforce cache for searching seeds
private const int cacheSize = 1 << 16;
private static readonly byte[] low8 = new byte[cacheSize];
private static readonly bool[] flags = new bool[cacheSize];
private const uint Mult = LCRNG.Mult;
private const uint Add = LCRNG.Add;
private const uint k2 = Mult << 8;
static LCRNGReversal()
{
// Populate Meet Middle Arrays
var f = flags;
var b = low8;
for (uint i = 0; i <= byte.MaxValue; i++)
{
// the second rand() also has 16 bits that aren't known. It is a 16 bit value added to either side.
// to consider these bits and their impact, they can at most increment/decrement the result by 1.
// with the current calc setup, the search loop's calculated value may be -1 (loop does subtraction)
// since LCGs are linear (hence the name), there's no values in adjacent cells. (no collisions)
// if we mark the prior adjacent cell, we eliminate the need to check flags twice on each loop.
uint right = (Mult * i) + Add;
ushort val = (ushort)(right >> 16);
f[val] = true; b[val] = (byte)i;
--val;
f[val] = true; b[val] = (byte)i;
// now the search only has to access the flags array once per loop.
}
}
private const uint LCRNG_MOD = 0x67D3; // u32(0x67d3 * LCRNG_MUL) < 2^16 (for seed and seed + 0x67d3, we have a good chance that the 16bit high of the next output will be the same for both)
private const uint LCRNG_PAT = 0xD3E; // pattern in the distribution of the "low solutions" modulo 0x67d3
private const uint LCRNG_INC = 0x4034; // (((diff * 0x67d3 + 0x4034) >> 16) * 0xd3e) % 0x67d3 line up with the first 16bit low solution modulo 0x67d3 if it exists (see diff definition in code)
/// <summary>
/// Finds all seeds that can generate the <see cref="pid"/> by two successive rand() calls.
/// </summary>
/// <param name="result">Result storage array, to be populated starting at index 0.</param>
/// <param name="pid">PID to be reversed into seeds that generate it.</param>
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeeds(Span<uint> result, uint pid)
{
uint first = pid << 16;
uint second = pid & 0xFFFF_0000;
return GetSeeds(result, first, second);
}
/// <summary>
/// Finds all seeds that can generate the IVs by two successive rand() calls.
/// Finds all seeds that can generate the IVs, with a vblank skip between the two IV rand() calls.
/// </summary>
/// <param name="result">Result storage array, to be populated starting at index 0.</param>
/// <param name="hp" >Entity IV for HP</param>
@ -77,22 +37,20 @@ public static class LCRNGReversal
/// Finds all the origin seeds for two 16 bit rand() calls
/// </summary>
/// <param name="result">Result storage array, to be populated starting at index 0.</param>
/// <param name="first">First rand() call, 16 bits, already shifted left 16 bits.</param>
/// <param name="second">Second rand() call, 16 bits, already shifted left 16 bits.</param>
/// <param name="first">First rand() call, 15 bits, already shifted left 16 bits.</param>
/// <param name="second">Second rand() call, 15 bits, already shifted left 16 bits.</param>
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeeds(Span<uint> result, uint first, uint second)
{
var diff = (second - (first * LCRNG.Mult)) >> 16;
var start = ((((diff * LCRNG_MOD) + LCRNG_INC) >> 16) * LCRNG_PAT) % LCRNG_MOD;
int ctr = 0;
uint k1 = second - (first * Mult);
for (uint i = 0, k3 = k1; i <= 255; ++i, k3 -= k2)
// at most 3 iterations
for (var low = start; low < 0x1_0000; low += LCRNG_MOD)
{
ushort val = (ushort)(k3 >> 16);
if (!flags[val])
continue;
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next(seed);
if ((next & 0xFFFF0000) == second)
var seed = first | low;
if ((LCRNG.Next(seed) & 0xffff0000) == second)
result[ctr++] = LCRNG.Prev(seed);
}
return ctr;
@ -107,42 +65,27 @@ public static class LCRNGReversal
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeedsIVs(Span<uint> result, uint first, uint second)
{
var diff = (second - (first * LCRNG.Mult)) >> 16;
var start1 = ((((diff * LCRNG_MOD) + LCRNG_INC) >> 16) * LCRNG_PAT) % LCRNG_MOD;
var start2 = (((((diff ^ 0x8000) * LCRNG_MOD) + LCRNG_INC) >> 16) * LCRNG_PAT) % LCRNG_MOD;
int ctr = 0;
// Check with the top bit of the first call both
// flipped and unflipped to account for only knowing 15 bits
uint search1 = second - (first * Mult);
uint search2 = second - ((first ^ 0x80000000) * Mult);
for (uint i = 0; i <= 255; i++, search1 -= k2, search2 -= k2)
{
ushort val = (ushort)(search1 >> 16);
if (flags[val])
{
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next(seed);
if ((next & 0x7FFF0000) == second)
{
var origin = LCRNG.Prev(seed);
result[ctr++] = origin;
result[ctr++] = origin ^ 0x80000000;
}
}
val = (ushort)(search2 >> 16);
if (flags[val])
{
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next(seed);
if ((next & 0x7FFF0000) == second)
{
var origin = LCRNG.Prev(seed);
result[ctr++] = origin;
result[ctr++] = origin ^ 0x80000000;
}
}
}
AddSeeds(result, start1, first, second, ref ctr);
AddSeeds(result, start2, first, second, ref ctr);
return ctr;
}
private static void AddSeeds(Span<uint> result, uint start, uint first, uint second, ref int ctr)
{
// at most 3 iterations
for (var low = start; low <= 0x1_0000; low += LCRNG_MOD)
{
var test = first | low;
if ((LCRNG.Next(test) & 0x7fff0000) != second)
continue;
var seed = LCRNG.Prev(test);
result[ctr++] = seed;
result[ctr++] = seed ^ 0x80000000;
}
}
}

View file

@ -6,54 +6,14 @@ namespace PKHeX.Core;
/// Seed reversal logic for the <see cref="LCRNG"/> algorithm, with a gap in between the two observed rand() results.
/// </summary>
/// <remarks>
/// Use a meet-in-the-middle attack to reduce the search space to 2^8 instead of 2^16
/// flag/2^8 tables are precomputed and constant (unrelated to rand pairs)
/// https://crypto.stackexchange.com/a/10609
/// Abuses the pattern in Low16 bit results of rand() to reduce the search space to 2^3 instead of 2^16
/// https://github.com/StarfBerry/poke-scripts/blob/f641f3eab264e2caf1ee7c3c0e5553a9d8086921/RNG/LCG_Reversal.py#L19-L93
/// </remarks>
public static class LCRNGReversalSkip
{
// Bruteforce cache for searching seeds
private const int cacheSize = 1 << 16;
private static readonly byte[] low8 = new byte[cacheSize];
private static readonly bool[] flags = new bool[cacheSize];
private const uint Mult = unchecked(LCRNG.Mult * LCRNG.Mult); // 0xC2A29A69
private const uint Add = unchecked(LCRNG.Add * (LCRNG.Mult + 1)); // 0xE97E7B6A
private const uint k2 = Mult << 8;
static LCRNGReversalSkip()
{
// Populate Meet Middle Arrays
var f = flags;
var b = low8;
for (uint i = 0; i <= byte.MaxValue; i++)
{
// the second rand() also has 16 bits that aren't known. It is a 16 bit value added to either side.
// to consider these bits and their impact, they can at most increment/decrement the result by 1.
// with the current calc setup, the search loop's calculated value may be -1 (loop does subtraction)
// since LCGs are linear (hence the name), there's no values in adjacent cells. (no collisions)
// if we mark the prior adjacent cell, we eliminate the need to check flags twice on each loop.
uint right = (Mult * i) + Add;
ushort val = (ushort)(right >> 16);
f[val] = true; b[val] = (byte)i;
--val;
f[val] = true; b[val] = (byte)i;
// now the search only has to access the flags array once per loop.
}
}
/// <summary>
/// Finds all seeds that can generate the <see cref="pid"/> with a discarded rand() between the two halves.
/// </summary>
/// <param name="result">Result storage array, to be populated starting at index 0.</param>
/// <param name="pid">PID to be reversed into seeds that generate it.</param>
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeeds(Span<uint> result, uint pid)
{
uint first = pid << 16;
uint second = pid & 0xFFFF_0000;
return GetSeeds(result, first, second);
}
private const uint LCRNG_MOD_2 = 0x3A89; // u32(0x3a89 * LCRNG_MUL_2) < 2^16 (for seed and seed + 0x3a89, we have a good chance that the 16bit high of the next output will be the same for both)
private const uint LCRNG_PAT_2 = 0x2E4C; // pattern in the distribution of the "low solutions" modulo 0x3a89
private const uint LCRNG_INC_2 = 0x5831; // (((diff * 0x3a89 + 0x5831) >> 16) * 0x2e4c) % 0x3a89 line up with the first 16bit low solution modulo 0x3a89 if it exists (see diff definition in code)
/// <summary>
/// Finds all seeds that can generate the IVs, with a vblank skip between the two IV rand() calls.
@ -82,19 +42,16 @@ public static class LCRNGReversalSkip
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeeds(Span<uint> result, uint first, uint third)
{
var diff = (third - LCRNG.Next2(first)) >> 16;
var start = ((((diff * LCRNG_MOD_2) + LCRNG_INC_2) >> 16) * LCRNG_PAT_2) % LCRNG_MOD_2;
int ctr = 0;
uint search = third - (first * Mult);
for (uint i = 0; i <= 255; ++i, search -= k2)
// at most 5 iterations
for (var low = start; low < 0x1_0000; low += LCRNG_MOD_2)
{
ushort val = (ushort)(search >> 16);
if (flags[val])
{
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next2(seed);
if ((next & 0xFFFF0000) == third)
result[ctr++] = LCRNG.Prev(seed);
}
var seed = first | low;
if ((LCRNG.Next2(seed) & 0xffff0000) == third)
result[ctr++] = LCRNG.Prev(seed);
}
return ctr;
}
@ -108,40 +65,28 @@ public static class LCRNGReversalSkip
/// <returns>Count of results added to <see cref="result"/></returns>
public static int GetSeedsIVs(Span<uint> result, uint first, uint third)
{
var diff = (third - LCRNG.Next2(first)) >> 16;
var start1 = ((((diff * LCRNG_MOD_2) + LCRNG_INC_2) >> 16) * LCRNG_PAT_2) % LCRNG_MOD_2;
var start2 = (((((diff ^ 0x8000) * LCRNG_MOD_2) + LCRNG_INC_2) >> 16) * LCRNG_PAT_2) % LCRNG_MOD_2;
int ctr = 0;
uint search1 = third - (first * Mult);
uint search3 = third - ((first ^ 0x80000000) * Mult);
for (uint i = 0; i <= 255; i++, search1 -= k2, search3 -= k2)
{
ushort val = (ushort)(search1 >> 16);
if (flags[val])
{
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next2(seed);
if ((next & 0x7FFF0000) == third)
{
var origin = LCRNG.Prev(seed);
result[ctr++] = origin;
result[ctr++] = origin ^ 0x80000000;
}
}
val = (ushort)(search3 >> 16);
if (flags[val])
{
// Verify PID calls line up
var seed = first | (i << 8) | low8[val];
var next = LCRNG.Next2(seed);
if ((next & 0x7FFF0000) == third)
{
var origin = LCRNG.Prev(seed);
result[ctr++] = origin;
result[ctr++] = origin ^ 0x80000000;
}
}
}
AddSeeds(result, start1, first, third, ref ctr);
AddSeeds(result, start2, first, third, ref ctr);
return ctr;
}
private static void AddSeeds(Span<uint> result, uint start, uint first, uint second, ref int ctr)
{
// at most 5 iterations
for (var low = start; low <= 0x1_0000; low += LCRNG_MOD_2)
{
var test = first | low;
if ((LCRNG.Next2(test) & 0x7fff0000) != second)
continue;
var seed = LCRNG.Prev(test);
result[ctr++] = seed;
result[ctr++] = seed ^ 0x80000000;
}
}
}