RSA-рандом на блокчейне

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

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

RSA-рандом на блокчейне

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

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

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

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

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

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

RSA-рандом на блокчейне

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

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

RSA-рандом на блокчейне

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

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

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

RSA-рандом на блокчейне

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

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

RSA-рандом на блокчейне

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)
  • Публичный ключ
  • Потенциальный выигрыш (зависит от ставки игрока)

RSA-рандом на блокчейне

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

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

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

RSA-рандом на блокчейне

RSA-рандом на блокчейне

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):

RSA-рандом на блокчейне

Функция содержит 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 блоков. Параметр настраивается при деплое смарт-контракта. Если за отведенное время ничего не происходит, пользователь выигрывает. В этом случае приз нужно отправить на свой адрес самостоятельно. Получается, серверу «не выгодно обманывать», ведь это ведет к проигрышу. Ниже – пример.

RSA-рандом на блокчейне

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

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

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

Источник: habr.com

Добавить комментарий