【问题标题】:Generating (very) large non-repeating integer sequence without pre-shuffling在没有预洗牌的情况下生成(非常)大的非重复整数序列
【发布时间】:2016-08-29 22:04:14
【问题描述】:

背景

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

我想要做的(在 Linux 上,在 C 中)是生成一个一对一的 n 位值序列(假设数据现在存储在 128 位整数数组元素中)没有重叠/冲突的值。然后,我将一个伪随机 128 位值/数字作为“盐”,将其应用于时间戳,然后开始向服务器发送命令,增加预加盐/预散列值。

时间戳大小如此之大的原因是时间戳可能必须适应非常长的持续时间。


问题

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

如果选项 1 对您来说不够“随机”,请使用上述的 CRC-32 哈希 全局(32 位)计数器。之间存在一对一的映射(双射) N 位整数和它们的 CRC-N 因此仍然可以保证唯一性。

但是,我不知道:

  • 如果可以(有效地)扩展到 128 位数据。
  • 如果为序列提供初始种子的某种加法/乘法盐值会破坏它或引入冲突。

跟进

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

谢谢。

【问题讨论】:

  • 关于序列:Xorshift128 是一个快速的 128 位伪随机数生成器,周期为 2¹²⁸-1。也就是说,给定任何非零的 128 位数,它会产生另一个非零的 128 位数;该序列遍历所有 128 位无符号整数,除了零。所有周期为 (2ⁿ)-1 的 n 位 PRNG 都具有这样的特征,而不仅仅是线性同余的。
  • 未来证明你的程序,直到宇宙热死。我喜欢它..
  • 是的,您可以将 CRC 扩展到 128 位,您可以在 Wikipedia 的 LFSR 页面上找到它的系数,但是对于它所实现的,还有更有效的操作。是的,您可以将盐添加或乘以(如果奇怪的话)到任何等效函数中,而不会引入冲突(mod 2**128)。只要两个函数是 1:1,链接它们的结果也必须是 1:1。我有这样的功能,但我必须将我的搜索工具扩展到 128 位才能找到合适的参数。
  • 我不确定我是否明白这一点。当然,任何旧的 128 位 LFSR 都可以,但除了浪费处理器时间之外,我认为这样做没有任何意义。为什么不直接发送呢?如果您出于某种原因需要隐藏它,那么您将需要真正的加密; LFSR 根本没有给你任何安全性。

标签: c linux random hash uniqueidentifier


【解决方案1】:

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

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

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

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

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

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


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

// _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;
}

【讨论】:

  • 两个答案都很棒(+1),但这更符合我的要求。谢谢@all!
  • 不客气!仅供参考,上面的代码触手可及,因为我正在编写它:stackoverflow.com/questions/38472504/… 但是,我在尝试编写一次性参数生成程序时陷入了困境。它可以使用 很多 的 CPU 和很多 更多 内存,这让我的系统交换变得疯狂。我需要重新架构。所以,暂时搁置了。
  • 您向我发送了有关您的 vim/checkpatch 问题和我的建议的评论,但随后该问题被删除。您是否仍需要帮助 [并将重新发布] 还是您想通了?
【解决方案2】:

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

加密是输入值到输出值的可逆的一对一映射,每个集合都是另一个集合的完全排列。

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

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

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

【讨论】:

    【解决方案3】:

    在线性同余生成器和加密函数之间的某个地方存在可以将线性计数转换为可通过的伪随机数的散列。

    如果您碰巧有 128 位整数类型(例如,为 64 位目标构建时在 GCC 中使用 __int128),或者愿意手动实现如此长的乘法,那么您可以在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,但您可以使用 bignumber 库或编译器扩展来进行算术运算。为了清楚起见,我依赖于编译器扩展。所有操作都依赖于类 C 的无符号溢出行为(取任意精度结果的低位)。

    【讨论】:

      猜你喜欢
      • 2017-08-21
      • 2011-07-23
      • 2011-04-24
      • 2010-12-24
      • 1970-01-01
      • 2010-11-11
      • 1970-01-01
      • 1970-01-01
      • 2020-11-21
      相关资源
      最近更新 更多