无需预洗牌即可生成(非常)大的非重复整数序列

Generating (very) large non-repeating integer sequence without pre-shuffling

背景

我有一个我写的简单媒体 client/server,我想生成一个非显而易见的时间值,我随每个命令从客户端发送到服务器。时间戳将包含相当多的数据(纳秒分辨率,即使由于现代操作系统中定时器采样的限制,它并不真正准确),等等。

我正在尝试做的(在 Linux,在 C 中)是生成一对一的 n 位值序列(假设数据存储在 128 位数组中-int 元素)没有 overlapping/colliding 值。然后我将伪随机 128 位 value/number 作为“盐”,将其应用于时间戳,然后开始向服务器发送命令,增加 pre-salted/pre-hashed 值。

时间戳大小之所以如此之大,是因为时间戳可能必须容纳非常长的持续时间。


问题

如何用初始盐值完成这样的序列(非碰撞)? The best approach that sounds along the lines of my goal is from this post, which notes:

If option 1 isn't "random" enough for you, use the CRC-32 hash of said global (32-bit) counter. There is a 1-to-1 mapping (bijection) between N-bit integers and their CRC-N so uniqueness will still be guaranteed.

然而,我不知道:


跟进

我知道我可以使用来自 libssl 的 128 位随机散列或类似的东西,但我希望远程服务器使用相同的盐值,能够将散列时间戳转换回它们的真实时间戳值。

谢谢。

简短的回答是加密。使用一组 128 位值将它们输入 AES 并获得一组不同的 128 位值。因为加密是可逆的,所以输出保证对于具有固定密钥的唯一输入是唯一的。

加密是输入值到输出值的可逆一对一映射,每组都是另一组的完整排列。

既然您大概不会重复输入,那么 ECB 模式可能就足够了,除非您想要更高程度的安全性。如果重复使用相同的输入,ECB 模式很容易受到攻击,这里似乎不是这种情况。

对于小于 128 位的输入,则使用固定填充方法使其长度合适。只要输入的唯一性不受影响,填充就可以相当灵活。在任一端(或内部字段的开头)填充零可能就足够了。

我不知道你的详细要求,请随时修改我的建议。

您可以使用线性同余生成器。使用正确的参数,可以保证产生具有完整周期(即无冲突)的非重复序列[唯一]序列。

这是 random(3)TYPE_0 模式下使用的。我将其调整为完整的 unsigned int 范围,种子可以是任何 unsigned int(请参阅下面的示例代码)。

我相信它可以扩展到 64 或 128 位。我会看一下:https://en.wikipedia.org/wiki/Linear_congruential_generator 以了解有关防止碰撞和良好随机性的参数约束。

按照 wiki 页面指南,您可以生成一个可以将 任何 128 位值作为种子并且在生成所有可能的 128 位数字之前不会重复的值。

您可能需要编写一个程序来生成合适的参数对,然后测试它们的 "best" 随机性。这将是一次性操作。

获得它们后,只需将这些参数代入实际应用中的方程式即可。


这是我在寻找类似东西时一直在玩的一些代码:

// _prngstd -- get random number
static inline u32
_prngstd(prng_p prng)
{
    long rhs;
    u32 lhs;

    // NOTE: random is faster and has a _long_ period, but it _only_ produces
    // positive integers but jrand48 produces positive _and_ negative
#if 0
    rhs = jrand48(btc->btc_seed);
    lhs = rhs;
#endif

    // this has collisions
#if 0
    rhs = rand();
    PRNG_FLIP;
#endif

    // this has collisions because it defaults to TYPE_3
#if 0
    rhs = random();
    PRNG_FLIP;
#endif

    // this is random in TYPE_0 (linear congruential) mode
#if 0
    prng->prng_state = ((prng->prng_state * 1103515245) + 12345) & 0x7fffffff;
    rhs = prng->prng_state;
    PRNG_FLIP;
#endif

    // this is random in TYPE_0 (linear congruential) mode with the mask
    // removed to get full range numbers
    // this does _not_ produce overlaps
#if 1
    prng->prng_state = ((prng->prng_state * 1103515245) + 12345);
    rhs = prng->prng_state;
    lhs = rhs;
#endif

    return lhs;
}

在线性同余生成器和加密函数之间的某处存在可以将线性计数转换为可通过的伪随机数的哈希。

如果您碰巧手边有 128 位整数类型(例如,__int128 在 GCC 中构建 64 位目标时),或者愿意手动实现如此长的乘法,那么您可以扩展 SplitMix64 中使用的结构。我做了一个相当肤浅的搜索并提出了以下参数:

uint128_t mix(uint128_t x) {
    uint128_t m0 = (uint128_t)0xecfb1b9bc1f0564f << 64
                 | 0xc68dd22b9302d18d;
    uint128_t m1 = (uint128_t)0x4a4cf0348b717188 << 64
                 | 0xe2aead7d60f8a0df;
    x ^= x >> 59;
    x *= m0;
    x ^= x >> 60;
    x *= m1;
    x ^= x >> 84;
    return x;
}

及其逆:

uint128_t unmix(uint128_t x) {
    uint128_t im0 = (uint128_t)0x367ce11aef44b547 << 64
                  | 0x424b0c012b51d945;
    uint128_t im1 = (uint128_t)0xef0323293e8f059d << 64
                  | 0x351690f213b31b1f;
    x ^= x >> 84;
    x *= im1;
    x ^= x >> 60 ^ x >> (2 * 60);
    x *= im0;
    x ^= x >> 59 ^ x >> (2 * 59);
    return x;
}

我不确定你是想要一个随机序列,还是想要一种混淆任意时间戳的方法(既然你说你想解码这些值,它们一定比线性计数器更有趣),但是一个足够简单地派生自另一个:

uint128_t encode(uint128_t time, uint128_t salt) {
    return mix((time + 1) * salt);
}

uint128_t generate(uint128_t salt) {
    static uint128_t t = 0;
    return encode(t++, salt);
}

static uint128_t inv(uint128_t d) {
    uint128_t i = d;

    while (i * d != 1) {
        i *= 2 - i * d;
    }
    return i;
}

uint128_t decode(uint128_t etime, uint128_t salt) {
    return unmix(etime) * inv(salt) - 1;
}

请注意,salt 选择 2127 个非重复 128 位值序列之一(我们丢失一位,因为 salt 必须是奇数), 但是有 (2128)!可能已经生成的可能序列。在其他地方,我正在考虑扩展参数化以便可以访问更多这些序列,但我开始使用上述方法来增加序列的随机性以隐藏参数可能选择不那么随机的任何问题(但可以证明是不同的)序列。

显然 uint128_t 不是标准类型,因此我的答案不是 C,但您可以使用大数字库或编译器扩展来进行算术运算。为清楚起见,我依赖于编译器扩展。所有操作都依赖于类 C 的无符号溢出行为(取任意精度结果的低位)。