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

В статье рассказываем, как удалось решить проблему на примере игр. Первой из них стала Waves Xmas Tree. Для разработки нам понадобился генератор случайных чисел.



Изначально мы планировали генерировать число на основании информации из блокчейна. Однако потом стало ясно: число могут подтасовать, а значит решение не подходит.

Мы придумали обходной путь: использовать схему «коммит-раскрытие». Сервер «загадывал» число от 1 до 5, добавлял к нему «соль», а затем хэшировал результат при помощи функции Keccak. Cервер заранее деплоил смарт-контракт с уже сохраненным числом. Получается, игра сводидась к тому, что пользователь угадывал число, скрытое хэшем.

Игрок делал ставку, а сервер отправлял загаданное число и «соль» на смарт-контракт. Простым языком, раскрывал карты. После этого сервер сверял цифры и решал, победил пользователь или проиграл.

Если сервер не присылал число или «соль» для проверки, пользователь побеждал. В этом случае для каждой игры было необходимо заранее деплоить смарт-контракт и закладывать в него потенциальный выигрыш. Оказалось, это неудобно, долго и дорого. На тот момент другого безопасного решения не было.

Недавно команда Tradisys предожила добавить в протокол Waves функцию rsaVerify(). Она проверяет валидность RSA-подписи на основании публичного и приватного ключа. В итоге функция была добавлена.

Мы разработали три игры: Dice Roller, Coin Flip и Ride On Waves. В каждой реализована технология случайного числа. Разберемся, как это работает.



Рассмотрим генерацию случайного числа на примере Ride on Waves. Смарт-контракт можно найти здесь.

Перейдите во вкладку Script и выберите Decompiled. Увидите код смарт-контракта (он же скрипт).



Код смарт-контракта содержит набор функций. Те, что помечены как @Callable, могут запускаться с помощью Invocation-транзакций. Нас интересуют две функции: bet и withdraw:

  • func bet (playerChoice)
  • func withdraw (gameId,rsaSign)

1. Пользователь выбирает длину отрезка и размер ставки.



2. Клиент формирует bet-функцию. Для изображения выше это будет bet («50»).

3. Клиент отправляет Invocation-транзакцию на адрес смарт-контракта (broadcast InvocationTx). Транзакция в качестве Сall-параметра содержит функцию bet. Это означает, что Invocation-транзакция запускает выполнение bet-функции (choice: String) на смарт-контракте.



4. Рассмотрим bet-функцию:

@Callable(i)
func bet (playerChoice) = {
    let newGameNum = IncrementGameNum()
    let gameId = toBase58String(i.transactionId)
    let pmt = extract(i.payment)
    let betNotInWaves = isDefined(pmt.assetId)
    let feeNotInWaves = isDefined(pmt.assetId)
    let winAmt = ValidateBetAndDefineWinAmt(pmt.amount, playerChoice)
    let txIdUsed = isDefined(getString(this, gameId))
    if (betNotInWaves)
        then throw ("Bet amount must be in Waves")
        else if (feeNotInWaves)
            then throw ("Transaction's fee must be in Waves")
            else if (txIdUsed)
                then throw ("Passed txId had been used before. Game aborted.")
                else {
                    let playerPubKey58 = toBase58String(i.callerPublicKey)
                    let gameDataStr = FormatGameDataStr(STATESUBMITTED, playerChoice, playerPubKey58, height, winAmt, "")
                    ScriptResult(WriteSet(cons(DataEntry(RESERVATIONKEY, ValidateAndIncreaseReservedAmt(winAmt)), cons(DataEntry(GAMESCOUNTERKEY, newGameNum), cons(DataEntry(gameId, gameDataStr), nil)))), TransferSet(cons(ScriptTransfer(SERVER, COMMISSION, unit), nil)))
                    }
    }

Функция записывает в стейт смарт-контракта новую игру. А именно:

  • Уникальный идентификатор новой игры (game id)
  • Game state = SUBMITTED
  • Выбор игрока (длина отрезка 50)
  • Публичный ключ
  • Потенциальный выигрыш (зависит от ставки игрока)



Так выглядит запись данных в блокчейне (ключ-значение):

{
    "type": "string",
    "value": "03WON_0283_448t8Jn9P3717UnXFEVD5VWjfeGE5gBNeWg58H2aJeQEgJ_06574069_09116020000_0229",
    "key": "2GKTX6NLTgUrE4iy9HtpSSHpZ3G8W4cMfdjyvvnc21dx"
  }

«Ключ» (key) – game id новой игры. Остальные данные содержатся в строке поля «значение» (value). Эти записи хранятся во вкладке Data смарт-контракта:





5. Сервер «смотрит» на смарт-контракт и находит отправленную транзакцию (новую игру) с помощью Api блокчейна. Game id новой игры уже записан в блокчейне, а значит изменить или повлиять на нее уже нельзя

6. Сервер формирует withdraw-функцию (gameId, rsaSign). Например, такую:

withdraw ("FwsuaaShC6DMWdSWQ5osGWtYkVbTEZrsnxqDbVx5oUpq", "base64:Gy69dKdmXUEsAmUrpoWxDLTQOGj5/qO8COA+QjyPVYTAjxXYvEESJbSiCSBRRCOAliqCWwaS161nWqoTL/TltiIvw3nKyd4RJIBNSIgEWGM1tEtNwwnRwSVHs7ToNfZ2Dvk/GgPUqLFDSjnRQpTHdHUPj9mQ8erWw0r6cJXrzfcagKg3yY/0wJ6AyIrflR35mUCK4cO7KumdvC9Mx0hr/ojlHhN732nuG8ps4CUlRw3CkNjNIajBUlyKQwpBKmmiy3yJa/QM5PLxqdppmfFS9y0sxgSlfLOgZ51xRDYuS8NViOA7c1JssH48ZtDbBT5yqzRJXs3RnmZcMDr/q0x6Bg==")

7. Сервер отправляет Invocation-транзакцию на смарт-контракт (broadcast InvocationTx). Транзакция содержит вызов сформированной withdraw-функции (gameId, rsaSign):



Функция содержит game id новой игры и результат RSA-подписи уникального идентификатора приватным ключом. Результат подписи неизменен.

Что это значит?

Берем одно и то же значение (game id) и применяем к нему метод RSA-подписи. Будем всегда получать один и тот же результат. Так работает RSA-алгоритм. Нельзя манипулировать финальным числом, так как game id и результат применения RSA не известен. Подбирать число также бессмысленно.

8. Блокчейн принимает транзакцию. Она запускает withdraw-функцию (gameId, rsaSign)

9. Внутри withdraw-функции происходит вывоз GenerateRandInt-функции (gameId, rsaSign). Это и есть генератор случайных чисел


# @return 1 ... 100
func GenerateRandInt (gameId,rsaSign) = {
   	# verify RSA signature to proof random
    let rsaSigValid = rsaVerify (SHA256, toBytes(gameId), rsaSign, RSAPUBLIC)
    if (rsaSigValid)
        then {
            let rand = (toInt(sha256(rsaSign)) % 100)
            if ((0 > rand))
                then ((-1 * rand) + 1)
                else (rand + 1)
            }
        else throw ("Invalid RSA signature")
    }

rand – и есть случайное число.

Сначала берется строка, которая является результатом RSA-подписи game id приватным ключом (rsaSign). Затем хэшируется с помощью SHA-256 (sha256(rsaSign)).

Мы не можем предсказать результат подписи и последующего хэширования. Поэтому невозможно повлиять на генерацию случайного числа. Чтобы получить число в определенном диапазоне (например, от 1 до 100), применяется функция преобразования toInt и %100 (аналог mod).

В начале статьи мы упоминали функцию rsaVerify(), которая позволяет проверить валидность RSA-подписи приватным ключом по публичному. Вот часть GenerateRandInt (gameId,rsaSign):

rsaVerify (SHA256, toBytes(gameId), rsaSign, RSAPUBLIC)

На вход передается публичный ключ RSAPUBLIC и строка rsaSign. Подпись проверяется на валидность. Число генерируется в случае успешной проверки. В обратном случае система считает, что подпись не валидна (Invalid RSA signature).

Сервер должен подписать game id игры приватным ключом и отправить валидную Rsa-подпись в течение 2880 блоков. Параметр настраивается при деплое смарт-контракта. Если за отведенное время ничего не происходит, пользователь выигрывает. В этом случае приз нужно отправить на свой адрес самостоятельно. Получается, серверу «не выгодно обманывать», ведь это ведет к проигрышу. Ниже – пример.



Пользователь играет в Dice Roller. Выбрал 2 из 6 граней кубика, ставка – 14 WAVES. Если сервер не пришлет валидную RSA-подпись на смарт-контракт в течение установленного времени (2880 блоков), пользователь заберет 34.44 WAVES.

Для генерации чисел в играх мы используем оракул – внешнюю, не блокчейновую систему. Сервер осуществляет RSA-подпись game id. Смарт-контракт проверяет валидность подписи и определяет победителя. Если сервер не прислал ничего, то пользователь автоматически побеждает.

Описанный метод гарантирует, что манипуляция технически невозможна. Во всех играх Tradisys используется алгоритм, поэтому они честные и прозрачные. Все поддается публичной проверке. Это и обеспечивает честность.

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


  1. amarao
    20.08.2019 12:09

    Я правильно понимаю, что речь про азартные игры на деньги (gambling)?


    1. Tradisys Автор
      20.08.2019 12:43

      Только в тех странах, где криптовалюты приравниваются к финансовому инструменту. Россия и большинство других стран на текущий момент к таким не относятся.


  1. Disasm
    20.08.2019 13:17

    Берем одно и то же значение (game id) и применяем к нему метод RSA-подписи. Будем всегда получать один и тот же результат.

    У вас какая-то странная RSA-подпись. В нормальной каждый раз должен получаться разный результат для одного и того же подписываемого блока данных.


    1. deemru
      20.08.2019 15:12

      Вы ошибаетесь.


      Сама подпись RSA всегда была и остаётся детерминированной, разница только в схеме padding-а, которая может быть рандомизирована или нет.


      Если само сообщение содержит рандомизированную часть, то padding может быть без ущерба для схемы детерминирован.


      1. Disasm
        20.08.2019 15:18

        Так тут и нет никакой рандомизированной части, как я понимаю. Любой злоумышленник, имеющий доступ к "подписывалке", может заранее узнать подпись, а следовательно и то "случайное" число, которое получится в GenerateRandInt. Если бы в реализации подписи фигурировал случайный паддинг, это было бы невозможно.


        1. Tradisys Автор
          20.08.2019 15:49

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


        1. deemru
          20.08.2019 15:54

          Любой злоумышленник, имеющий доступ к "подписывалке", может заранее узнать подпись

          Гениально.


  1. Luxo
    20.08.2019 15:23

    Мы придумали обходной путь: использовать схему «коммит-раскрытие».
    Кто мы? И честно — это вы придумали?
    Оказалось, это неудобно, долго и дорого. На тот момент другого безопасного решения не было.
    А разве привидённая схема удобней, короче или дешевле? Количество необходимых транзакций так же 2 — по одной от игрока и казино. Стоимость думаю приблизительно одинакова. Удобство — такое же. Сделал ставку, подождал пока она разрешится.

    И на счёт RSA. По-моему можно генерировать разные валидные подписи к тем же данным. Что ведёт к тому, что вы можете легко обыгрывать игрока.


    1. deemru
      20.08.2019 16:10

      Да, действительно, многое зависит от предварительного кодирования сообщения (в основном есть ли там фиксированный паддинг или нет), но попросили именно детерминированную схему проверки, поэтому получили на уровне блокчейна набор s"${digestPrefix}withRSA", который гарантирует проверку на EMSA-PKCS1-v1_5.


      1. k06a
        20.08.2019 18:00

        Тоже не понял чем приведенная схема качественно отличается от стандартной commit-reveal, вы должны были уменьшить число транзакций хотя бы, а получили новые проблемы: если ваш привтаник один раз утечет выиграть у вас можно будет в любой момент в будущем. С тем же успехом можно было commits собрать в дерево меркла и reveal делать с меркл-пруфом. В известном всем проекте DICE2WIN помимо commit-reveal еще есть защита от реорга блокчейна.


        1. deemru
          20.08.2019 18:13

          В предложенной схеме я вижу достаточность 2 транзакций на игру (одна от пользователя и одна от условного казино).


          Можете рассказать как в 2 транзакции сделать commit-reveal про который вы говорите?


          1. k06a
            20.08.2019 18:16

            До этого:


            1. Commit сервером
            2. Ставка юзером
            3. Раскрытие сервером

            У вас:


            1. Ставка юзером
            2. Раскрытие сервером

            Так?


            1. deemru
              20.08.2019 18:20

              Так


              1. Luxo
                20.08.2019 22:37

                Не так.


                1. Ставка юзером с комитом.
                2. Раскрытие сервером.


                1. deemru
                  20.08.2019 22:45

                  Если придираться, то ваш вариант лучше, да.