Как-то утром на глаза попалась статья о проверяемом генераторе случайных чисел на блокчейне Waves platform.

Общая картина была понятна, а вот способ конкретной реализации — нет. Какие-то коды, подписи, что, куда, зачем?

Несколько консультаций у автора оракула, в результате получилось объединить логику розыгрыша (реализована на PHP) с алгоритмом получения случайного числа.

  1. В момент старта турнира/раунда мы запрашиваем у оракула первую часть кода (R-code).

    В этот момент нет информации ни о количестве игроков, ни о количестве призовых мест, ни о размере призовых выплат и вообще о существовании лотереи. Оракул посредством транзакции выдает персональный случайный код, который в дальнейшем может быть использован только один раз и только тем, кто его запросил. Кстати, R-code можно «закупить» (имеется в виду стоимость транзакции запроса + компенсация оракулу за ответную транзакцию, это сумма порядка $0.015 по текущему курсу, сам код выдается бесплатно) заранее сразу несколько, чтобы потом не ждать получения ответной транзакции. Я сделал небольшой регулярно пополняемый буфер в БД.
  2. Турнир длится стандартно 60 блоков блокчейна Waves platform, на данный момент это примерно 1 час. Турнир считается состоявшимся и закрытым, если после 60 блоков в нем будет не менее двух билетов, иначе время активности турнира продлевается на следующие 60 блоков.
  3. Сразу после закрытия турнира мы формируем и отправляем дата транзакцию (за нее также платим комиссию примерно $0.005), при необходимости — несколько, в которой зафиксированы все условия розыгрыша и упорядоченный список игроков (билетов) из которого нам необходимо выбрать победителей.
  4. На данном этапе у нас уже есть первая часть кода (R-code) плюс ID дата транзакции (TXID). Мы отправляем их на подпись оракулу в виде конкатенации (R-code + TXID), опять платим комиссию+компенсацию. Оракул проверяет полученные данные на предмет уникальности и принадлежности, а в ответ посылает нам вторую часть кода (S-code) в формате sha256, которая и является отправной точкой для генератора случайных чисел.
  5. Чтобы получить случайное число, которое будет указывать на порядковый номер победившего билета, мы преобразовываем S-code из бинарных данных sha256 в шестнадцатиричное (HEX) представление. Затем из получившейся HEX строки, получаем число. Получаем остаток от деления получившегося числа на количество билетов (all_tickets) и прибавляем к результату 1 (чтобы получить цифру 1 до all_tickets). В итоге мы получаем порядковый номер победителя.
  6. Если по условиям розыгрыша победителей несколько, то повторяем предыдущие операции в количестве равном количеству призовых мест. При этом каждый раз удаляем из списка билет который уже выиграл и уменьшаем all_tickets на 1, а вместо S-code указываем предыдущее полученное число.

Разберем конкретный реальный пример, турнир №119:

Всего 7 билетов (all_tickets)
Стоимость билета 50 монет (Bet)
Игровой сбор 10% (Fee)

По условиям лотереи 30% попадают в призовые, т.е. в данном случае 2 билета должны получить приз, размер которого считается по формуле (Bet*all_tickets-Fee)/2.

1. Получили R-code: RdbAiAhKhveAtR4eyTKq75noMxdcEoxbE6BvojJjM13VE

2. После закрытия турнира имеем список билетов в виде пар: номер + адрес (адрес кошелька с которого была оплата участия в турнире). Заметим, что адреса могут повторяться, это означает, что один участник купил несколько билетов в один турнир, это не возбраняется правилами.

Отправили дата транзакцию: 82JTMzhHM5xEA2fQ9Qscd5QAJU3DAd8nShLjdVHTer5S

3. Запросили S-code: FTF3uRyaa4F2uAyD6z5a3CNbTXbQLc7fSR6CFNVjgZYV с комментарием (R-code + TXID):
RdbAiAhKhveAtR4eyTKq75noMxdcEoxbE6BvojJjM13VE 82JTMzhHM5xEA2fQ9Qscd5QAJU3DAd8nShLjdVHTer5S

4. Получили S-code: Ri89jHB4UXZDXY6gT1m4LBDXGMTaYzHozMk4nxiuqVXdC

5. Определили победителей.

6. Отправили выплаты

В результате мы имеем в блокчейне пошаговую фиксацию процедуры розыгрыша призов с возможностью проверить ее в любое время. Подтасовать результаты со стороны организатора практически невозможно, по крайней мере сделать это незаметно уже не получится.

determine the winner № 1

All_tickets:
Index: 1 Ticket:139
Index: 2 Ticket:141
Index: 3 Ticket:143
Index: 4 Ticket:145
Index: 5 Ticket:147
Index: 6 Ticket:149
Index: 7 Ticket:151

1. bin -> hex ( bin2hex(sha256(S-code)) ): Ri89jHB4UXZDXY6gT1m4LBDXGMTaYzHozMk4nxiuqVXdC -> 0xdaf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596

2. hex -> gmp number: 0xdaf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 -> 99037963059744689166154019807924045947962565922868104113173478160267437352342

3. gmp -> modulo (mod=7): 99037963059744689166154019807924045947962565922868104113173478160267437352342 -> 4

4. modulo -> ticket: 4 -> 145

determine the winner № 2

All_tickets:

Index: 1 Ticket:139
Index: 2 Ticket:141
Index: 3 Ticket:143
Index: 4 Ticket:147
Index: 5 Ticket:149
Index: 6 Ticket:151

1. bin -> hex ( bin2hex(sha256(previous hex)) ): daf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 -> 0x9560e77525e9ea2db92cdb8484dc52046ccafac7c719b8859ff55f0eb92834a0
2. hex -> gmp number: 0x9560e77525e9ea2db92cdb8484dc52046ccafac7c719b8859ff55f0eb92834a0 -> 67565829218838067182838043983962684143266386786567427968312120473742580659360
3. gmp -> modulo (mod=6): 67565829218838067182838043983962684143266386786567427968312120473742580659360 -> 1
4. modulo -> ticket: 1 -> 139

End.

Комментарии (23)


  1. vassabi
    30.05.2019 11:40

    вместо S-code указываем предыдущее полученное число.
    хмм… а нет ли тут скрытого предпочтения? Т.е. вероятность, что вы выиграли 1ым — «размазана равномерно», а вот 2й выигрыш и далее — чаще выпадает на начало очереди и нечетные номера?


    1. Earthsound Автор
      30.05.2019 12:04

      нечетные номера в какой момент? очередь уменьшается и индексируется заново при каждой итерации. Т.е. если изначально билет 145 был в очереди четным, то после определения первого победителя, он станет нечетным (не по номеру билета, а по порядку в очереди).
      А почему на начало будет большее предпочтение?


    1. mayorovp
      30.05.2019 16:57

      sha256 — криптостойкая функция, что означает, в том числе, равномерное распределение результата для любого практически достижимого способа получения исходных данных


      1. vassabi
        30.05.2019 19:07

        т.е. не предыдущее число, а sha256 от предыдущего числа?


        1. Earthsound Автор
          30.05.2019 19:28

          да, если быть точным, то sha256 от предыдущего числа


  1. mayorovp
    30.05.2019 16:58

    Не очень понимаю смысл перевода байт в hex- или base64-представление, кодирования в ascii и последующее хеширования.


    Не проще ли напрямую хешировать массив байт?


    1. Earthsound Автор
      30.05.2019 17:18

      Буду благодарен, если подскажете как в PHP из daf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 получить число 1 без промежуточных преобразований


      1. mayorovp
        30.05.2019 17:21

        Не вижу связи между daf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 и числом 1.


        А если вы хотели загрузить набор байт в gmp — то вам нужна функция gmp_import


        1. Earthsound Автор
          30.05.2019 17:24

          В таком виде мы получаем данные от оракула и нам надо превратить их в порядковый номер очереди


          1. mayorovp
            30.05.2019 17:28

            Не важно в каком виде вы получаете данные от оракула — их всегда можно перевести к последовательности байт (в PHP, кажется, это называется "бинарная строка").


            Дальше с этой строкой можно делать всё что вам нужно по алгоритму — импортировать в gmp и делить на число участников.


            1. Earthsound Автор
              30.05.2019 17:29

              Вы перечислили именно то, что делается и описано в статье.


              1. mayorovp
                30.05.2019 17:34

                Нет. Вы вместо бинарной строки используете её hex-представление. Которое мало того что в два раза больше — так ещё и неоднозначно.


                Если кто-то решит повторить ваш алгоритм на другом языке программирования — ему придется бороться со следующими проблемами:


                1. hex-представление может использовать прописные буквы вместо строчных, что изменит его хеш;
                2. hex-представление может содержать пробелы, что изменит его хеш;
                3. hex-представление может начинаться с префикса "0x" или заканчиваться суффиксом "h", что изменит его хеш;
                4. сам факт, что нужно считать хеш не от набора байт, а от его hex-представления, неочевиден.


                1. Earthsound Автор
                  30.05.2019 17:44

                  Я поэтому и спросил, знаете ли вы возможность более простого преобразования bin to int посредством PHP? Минуя hex.
                  Возможно это упростило бы код и позволило бы избежать лишних операций.
                  В статье приведен пример конкретной реализации на PHP, на других языках никакой борьбы не будет, получаете bin и доступными средствами преобразовываете его в int, если есть возможность, то без hex
                  А в данном примере hex представление является однозначным, т.к. мы получаем его самостоятельно, а не из сторонних источников и формат нам известен.
                  Другими словами: было удобно сделать именно так. Если можно проще — с удовольствием исправлю.


                  1. mayorovp
                    30.05.2019 18:01

                    Я поэтому и спросил, знаете ли вы возможность более простого преобразования bin to int посредством PHP? Минуя hex.

                    Так я и ответил же: gmp_import, а дальше всё по вашему алгоритму.


                    В статье приведен пример конкретной реализации на PHP, на других языках никакой борьбы не будет, получаете bin и доступными средствами преобразовываете его в int, если есть возможность, то без hex

                    Увы, в таком случае получится другой результат. То есть провалидировать результаты вашей лотереи не получится.


                    1. Earthsound Автор
                      30.05.2019 18:47

                      Я понял о чем вы!
                      Спасибо, подумаю над решением этого вопроса.
                      Хотя нет…
                      По большому счету суть не поменяется, результат останется прежним.


                      1. mayorovp
                        30.05.2019 19:31

                        Как он останется прежним, когда sha256("1234") не равно sha256("\x12\x34")?


                        1. Earthsound Автор
                          30.05.2019 19:42

                          Пробуем валидацию на сторонних ресурсах?
                          На сайте gchq.github.io/CyberChef/#recipe=SHA2('256') в INPUT даем наш S-code, в приведенном примере это:
                          Ri89jHB4UXZDXY6gT1m4LBDXGMTaYzHozMk4nxiuqVXdC
                          На выходе получаем:
                          daf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596
                          Дополняем 0x и отправляем на сайт defuse.ca/big-number-calculator.htm в виде:
                          (0xdaf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 % 7 ) + 1
                          Получаем: 4
                          Как и в приведенном примере.


                          1. mayorovp
                            30.05.2019 20:10

                            Это вы хеш hex-представления посчитали. А теперь добавляем перед sha256 шаг fromhex (он нужен потому что сайт не позволяет удобно ввести бинарную строку) — и получаем вместо вашего daf5802953dcb27f89972e38e8900b898733f6a613e6e1c6c5491362c1832596 какой-то 98f358536272a4550fdea5dbb1020a305b8ac5e38b1ec5af920f250d8a38f064.


                            1. Earthsound Автор
                              30.05.2019 20:29

                              Подождите…
                              Что и из чего вы получаете с помощью fromhex?


                            1. Earthsound Автор
                              30.05.2019 21:28

                              Приведите пожалуйста последовательность действий и результаты каждого действия начиная с:
                              Ri89jHB4UXZDXY6gT1m4LBDXGMTaYzHozMk4nxiuqVXdC


                              1. mayorovp
                                30.05.2019 21:48

                                fromhex, sha256


                                1. Earthsound Автор
                                  30.05.2019 22:42
                                  -1

                                  всё равно не могу понять, зачем fromhex?
                                  какую строку сайт не дает удобно ввести?
                                  У нас есть Ri89jHB4UXZDXY6gT1m4LBDXGMTaYzHozMk4nxiuqVXdC — ее мы и вставляем на сайте.


                                1. Earthsound Автор
                                  31.05.2019 06:59

                                  На вопрос так и не ответили