Waves corporate blog
Decentralized networks
Cryptography
Cryptocurrencies
August 20

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

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

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

+9
1.8k 17
Comments 15
Similar posts