В первой статье я рассказал о теории блокчейна, о том, что она может сделать для вашего программного проекта и основах взаимодействия с блокчейном Nxt в PHP.
Сегодня я собираюсь представить небольшую лотерею, написанную на Go.
Требования: Golang (тестировался с Go 1.6.2) NRS 1.10.1 (https://bitbucket.org/JeanLucPicard/nxt/downloads/)
Программа полностью функциональна и запускает лотерею каждое воскресенье [1]. Она была изначально написана на PHP, оба источника доступны для загрузки из облака данных Nxt [2].
Логика приложения
Данными для этого приложения служат “билеты”, отправленные пользователями во вложениях.
Участник лотереи посылает 10 NXT на счет лотереи и прикрепляет публичное незашифрованное сообщение со строкой из 5 чисел, в диапазоне от 0 до 30, разделенных запятыми. Для получения награды, сообщение должно быть незашифрованным, чтобы его можно было публично проверить с помощью блокчейна.
Приложение делает запрос к серверу Nxt, чтобы извлечь все транзакции из лотерейного счета, сортирует их, выбирает только действительные транзакции и создает срез карт (многомерный массив в PHP) учетных записей игроков и строки их чисел. Оно также рассчитывает общую сумму NXT, выплачиваемую игрокам из суммы всех действительных билетов.
Получив все действительные данные, приложение проводит три раунда лотереи. Каждый раунд получает часть от общей суммы выплат, разделенных между победителями. В раунде 5, приложение находит пользователя(ей), которые правильно угадали 5 чисел, и посылает награды. В раунде 4, приложение делает то же самое для пользователей, которые угадали 4 числа, срез участвующих билетов теперь меньше на количество победителей раунда 5. Аналогично для раунда 3.
В этом суть.
Немного подробностей
Для каждого из трех раундов лотерея генерирует последовательности 5 чисел и сравнивает их со строками чисел в билетах, пока один или несколько победителей не будут найдены. Можно сказать, что “силы” лотереи это выигрышная последовательность на билетах.
С ограниченным числом пользователей это, кажется, единственным разумным способом ведения лотереи без сбора и хранения большого джек-пота в течение нескольких месяцев и лет.
Давайте посмотрим на функцию, которая генерирует последовательности 5 чисел и возвращает их массив вызывающей функции. В среднем, эта функция вызывается сотни тысяч раз, для поиска последовательность из 5 чисел, соответствующих одному из билетов, когда у нас есть ограниченное число участников. Она занимает доли секунды. В PHP это занимает чуть больше времени (на секунду или две), хотя производительность PHP 7 действительно хороша
func genFive(seed string) [5]int { var r [5]int seedInt, _ := strconv.Atoi(seed) d := false for a := offset; a < offset+5; a++ { rand.Seed(int64(seedInt + offset)) var dup [31]int d = false r[0] = rand.Intn(31) r[1] = rand.Intn(31) r[2] = rand.Intn(31) r[3] = rand.Intn(31) r[4] = rand.Intn(31) for _, v := range r { dup[v]++ } for k, _ := range dup { if dup[k] > 1 { d = true } } offset = offset + 5 if d == false { return r } } return r }
Важной характеристикой блокчейн-приложения лотереи является то, что оно должно быть полностью свободной от доверия.
Каждый может подтвердить, что результаты лотереи не были подтасованы. Логичное и простое решение этой проблемы заключается в создании последовательности чисел с детерминированным начальным значением – сидом (seed).
Проблема с детерминированными значениями: если они известны заранее, последовательности чисел можно предсказать и результаты лотереи могут быть подтасованы. Для решения этой проблемы мы вновь обратиться к блокчейну Nxt, чтобы найти источник значений с функцией getSeed().
func getSeed() (string, string) { type BlockchainStatus struct { NumberOfBlocks int `json:"numberOfBlocks"` } var status BlockchainStatus if seedBlockOutput, b := sendQuery("requestType=getBlockchainStatus", true); b != false { if err := json.Unmarshal([]byte(seedBlockOutput), &status); err != nil { fmt.Println(err) } } seedBlockHeight := strconv.Itoa(status.NumberOfBlocks - 11) type BlockId struct { Block string `json:"block"` } var block BlockId if seedBlockId, b := sendQuery("requestType=getBlockId&height=" +seedBlockHeight, true); b != false { if err := json.Unmarshal([]byte(seedBlockId), &block); err != nil { fmt.Println(err) } } seed := block.Block[len(block.Block)-5:] return seed, seedBlockHeight }
Приложение работает в 18:00 UTC в воскресенье.
Первое, что делает функция getSeed() это извлекает идентификатор блока, который был сгенерирован 10 блоков до запуска приложения (как видно в локальной копии блокчейна на лотерейной ноде) и берет его 5 последних цифр в качестве сида. Из-за задержки сети и случайных перестроек 1-3 блоков блокчейна, лотерейная нода не может видеть тот же блок, что и другие ноды. Число 10 для получения блока для сида было выбрано, чтобы быть уверенным, что блок не будет реорганизован.
Можно утверждать, что существует теоретическая возможность того, что идентификатор блока является предсказуемым. Шансы этого являются крошечными, на мой взгляд, но я оставлю это для читателей, чтобы обсудить и решить.
Теперь, когда приложение имеет свой сид, оно может выполнять свою работу таким образом, что пользователям нет необходимости доверять лотерейному хосту.
Исходный код Go не включает в себя процедуру проверки прошлых результатов.
В исходном коде PHP это есть, он является полностью функциональным и может использоваться для самостоятельной проверки всех прошлых результатов с детерминированным сидом из блокчейна.
Для Go я использую эту функцию для отправки запросов к Nxt серверу и возврату результата.
func sendQuery(Query string, Active bool) (output string, b bool) { output = "" b = false if Active == false { output = "Function disabled" return } body := strings.NewReader(Query) req, err := http.NewRequest("POST", "http://127.0.0.1:7876/nxt", body) if err != nil { output = fmt.Sprintf("%s", err) return } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { output = fmt.Sprintf("%s", err) return } bo, err := ioutil.ReadAll(resp.Body) defer resp.Body.Close() output = fmt.Sprintf("%s", bo) match, _ := regexp.MatchString(".*errorDescription.*", output) if match == true { fileHandle, _ := os.OpenFile("./error.log", os.O_APPEND, 0666) writer := bufio.NewWriter(fileHandle) defer fileHandle.Close() fmt.Fprintln(writer, output) writer.Flush() return } b = true return }
Результаты возвращаются в виде строки JSON и должны быть распакованы в правильные структуры.
validPlayers := make([]map[string]string, 0) lotteryAccount := "NXT-YXC4-RB92-F6MQ-2ZRA6" type Attachment struct { Message string `json:"message"` MessageIsText bool `json:"messageIsText"` } type Transaction struct { Timestamp int `json:"timestamp"` AmountNQT string `json:"amountNQT"` ID string `json:"transaction"` SenderRS string `json:"senderRS"` RecipientRS string `json:"recipientRS"` Attached Attachment `json:"attachment"` } type Response struct { Transactions []Transaction `json:"transactions"` } Query := "requestType=getBlockchainTransactions&account=" + lotteryAccount + "&type=0&subtype=0&executedOnly=true" if v, a := sendQuery(Query, true); a == true { var transactions Response if err := json.Unmarshal([]byte(v), &transactions); err != nil { fmt.Println(err) } p := 0 for k, _ := range transactions.Transactions { // code to check tickets for validity. // if transaction satisfies all criteria // add it to the slice of valid tickets. validPlayers = append(validPlayers, make(map[string]string)) validPlayers[p][txSender] = lotteryNumbers p++ } }
Теперь, когда “validPlayers” имеет все хорошие билеты мы можем начать игру.
process() получает целое число (5, 4, или 3) и другие параметры, в том числе validPlayers и запускает три раунда лотереи. Делает вызов функции getWinners(), которая вызывает один раз genFive(), чтобы генерировать последовательности чисел, пока, по крайней мере, один победитель не будет найден. getWinners() возвращает результаты функции process(), которая посылает награду, удаляет билет победителя из подходящих билетов и возвращает оставшиеся билеты функции main() для последующих раундов. Существует вспомогательная функция preparePlayers(), которая воссоздает validPlayers без пустых мест, освобожденных от удаленных билетов.
Я призываю каждого программиста попробовать кодить на блокчейне Nxt. Это очень легко с его богатым API, навешенным на всю функциональность ядра. https://nxtwiki.org/wiki/The_Nxt_API
Моим следующим приложением будет, вероятно, приложение опроса, с неизменными записями голосов, сохраненных в блокчейн. Как вы думаете, приложение, вроде этого может найти применение в современном мире? Кстати, Nxt имеет свою собственную встроенную систему голосования. Легко забыть, что есть в Nxt, потому что он имеет так много возможностей, и все эти функции доступны через API, программируемый разработчиками ядра для пользователей. Вы можете «намайнить» ваши первые монеты NXT, чтобы провести транзакции в проекте Удачная нода (Lucky node), запустив работающую публичную ноду, перейдите на nxtforum.org, и вы узнаете, как это сделать.
Пожалуйста, оставьте свои комментарии и предложения.
2. Чтобы получить доступ к облаку данных Nxt Data, загрузите и установите NRS (Nxt Reference Software 1.10.1) и найдите ключевое слово ‘lottery’. Исходный код также можно загрузить с любого из публичных серверов Nxt с открытым API, например:
Go: http://23.94.134.161:7876/nxt?requestType=downloadTaggedData&transaction=7872865106538381099&retrieve=true
PHP: http://23.94.134.161:7876/nxt?requestType=downloadTaggedData&transaction=13031806327722095646&retrieve=true
НАЗАД К СТАТЬЕ.