Пишем блокчейн. Пользовательский клиент. (Консольный интерфейс)

in Team Ukraine4 years ago

Интерфейс это важная составляющая приложений, благодаря которому
исполняются все необходимые действия заложенные в ядро программы. Так
например, интерфейсами можно считать API сайтов, внешние (extern) функции
библиотек, входные аргументы программы. Но когда речь заходит об обычных
пользователях, то масштаб термина «интерфейс» сужается всего до двух его
составляющих: CLI (консольный интерфейс) и GUI (графический интерфейс).
Плюсом консольного интерфейса, со стороны лёгкости его написания,
является отсутствие (либо предельный минимализм) дизайна. Благодаря этому
качеству, сначала первым пишется консольный интерфейс (для первоначальной
проверки работоспособности всей программы), а лишь потом графический.

blockchain_nko.jpeg

Клиентское приложение будет состоять из двух частей:

  1. Инициализация, при которой необходимо указать файл на приватный
    ключ пользователя (либо создать такой ключ), а также на адрес узлов.

  2. Консольный интерфейс, который будет реализован через стандартный
    поток ввода (stdin). В нём можно будет прописывать команды проверки
    баланса, отправления транзакций и вывода цепочки блоков.
    Стоит начать с инициализации. Примером входа в программу
    должна служить следующая строка:
    ——————————————————————————————————
    ./client -loaduser:private.key -loadaddr:addrlist.json
    ——————————————————————————————————
    Указывается подгружаемый приватный ключ, который создаст объект
    структуры User, а также подгружается список адресов. Стоит заметить, что
    список адресов должен иметь формат json. Помимо параметра подгрузки
    пользователя (-loaduser:) можно использовать параметр создания нового
    пользователя (-newuser:). Если приватный ключ уже создан и хранится в файле,
    а в параметре newuser указан этот же файл, тогда произойдёт перезапись
    существующего файла, создав тем самым новый приватный ключ.
    Пример подгружаемого файла для параметра loaduser, указывающего на
    адреса localhost (так как поля до ‘:’ пусты) с портами 8080, 9090:
    ——————————————————————————————————
    [
    ":8080",
    ":9090"
    ]
    ——————————————————————————————————
    Если какой-либо один из файлов не был указан или в файле была
    допущена ошибка (невалидный приватный ключ, неправильный синтаксис
    JSON формата), тогда программа завершится с ошибкой.
    Также программа завершится с ошибкой, если список адресов будет
    пустым (характерно только для клиентского приложения).
    Так как клиент является исполняемым приложением, его пакет именуется
    как «main».
    ——————————————————————————————————
    package main
    ——————————————————————————————————

                                Функция init.
    

——————————————————————————————————
func init() {
if len(os.Args) < 2 {
panic("failed: len(os.Args) < 2")
}
var (
addrStr = ""
userNewStr = ""
userLoadStr = ""
)
var (
addrExist = false
userNewExist = false
userLoadExist = false
)
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
switch {
case strings.HasPrefix(arg, "-loadaddr:"):
addrStr = strings.Replace(arg, "-loadaddr:", "", 1)
addrExist = true
case strings.HasPrefix(arg, "-newuser:"):
userNewStr = strings.Replace(arg, "-newuser:", "", 1)
userNewExist = true
case strings.HasPrefix(arg, "-loaduser:"):
userLoadStr = strings.Replace(arg, "-loaduser:", "", 1)
userLoadExist = true
}
}
if !(userNewExist || userLoadExist) || !addrExist {
panic("failed: !(userNewExist || userLoadExist) || !addrExist")
}
err := json.Unmarshal([]byte(readFile(addrStr)), &Addresses)
if err != nil {
panic("failed: load addresses")
}
if len(Addresses) == 0 {
panic("failed: len(Addresses) == 0")
}
if userNewExist {
User = userNew(userNewStr)
}
if userLoadExist {
User = userLoad(userLoadStr)
}
if User == nil {
panic("failed: load user")
}
}
——————————————————————————————————
Перебирает все аргументы программы, пытается найти в начале строку-
команду («-loaduser:», «-newuser:», «-loadaddr:») и если находит, тогда берёт
аргумент этой команды с указанием того, что команда была найдена. Если не
была найдена команда работающая с пользователем или с адресами, тогда
выведится ошибка, иначе глобальные переменные обновятся. Использует
функции readFile, userNew, userLoad, а также глобальные
переменные Addresses и User.

                            Функция readFile.

——————————————————————————————————
func readFile(filename string) string {
data, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}
return string(data)
}
——————————————————————————————————
Читает весь файл и заносит его содержимое в переменную строкового
типа.

                           Функция userNew.

——————————————————————————————————
func userNew(filename string) *bc.User {
user := bc.NewUser()
if user == nil {
return nil
}
err := writeFile(filename, user.Purse())
if err != nil {
return nil
}
return user
}
——————————————————————————————————
Создаёт нового пользователя при помощи функции bc.NewUser. Также
использует функцию writeFile для создания файла и записи.

                            Функция userLoad.

——————————————————————————————————
func userLoad(filename string) *bc.User {
priv := readFile(filename)
if priv == "" {
return nil
}
user := bc.LoadUser(priv)
if user == nil {
return nil
}
return user
}
——————————————————————————————————

                  Глобальная переменная Addresses.
                  Глобальная переменная User.

——————————————————————————————————
var (
Addresses []string
User *bc.User
)
——————————————————————————————————

                          Функция writeFile.

——————————————————————————————————
func writeFile(filename string, data string) error {
return ioutil.WriteFile(filename, []byte(data), 0644)
}
——————————————————————————————————
После того как было сделано всё с инициализацией, следует приступать к
консольному интерфейсу.

                         Функция handleClient.

——————————————————————————————————
func handleClient() {
var (
message string
splited []string
)
for {
message = inputString("> ")
splited = strings.Split(message, " ")
switch splited[0] {
case "/exit":
os.Exit(0)
case "/user":
if len(splited) < 2 {
fmt.Println("failed: len(user) < 2\n")
continue
}
switch splited[1] {
case "address":
userAddress()
case "purse":
userPurse()
case "balance":
userBalance()
default:
fmt.Println("command undefined\n")
}
case "/chain":
if len(splited) < 2 {
fmt.Println("failed: len(chain) < 2\n")
continue
}
switch splited[1] {
case "print":
chainPrint()
case "tx":
chainTX(splited[1:])
case "balance":
chainBalance(splited[1:])
case "block":
chainBlock(splited[1:])
case "size":
chainSize()
default:
fmt.Println("command undefined\n")
}
default:
fmt.Println("command undefined\n")
}
}
}
——————————————————————————————————
Функция состоит из бесконечного цикла, в котором содержится ввод
пользователя, при помощи функции inputString . Строка разбивается по
символа пробела, после чего идёт сравнение первого разделения со строковыми
командами («/exit», «/user», «/chain»). Если команда не была обнаружена,
выведится предупреждение. У каждой из команд, за исключением «/exit»,
существуют свои аргументы. Так например, у команды «/user» существуют
аргументы «address», «purse» , «balance», а у команды
«/chain» аргументы «print» , «tx», «balance», «block»,
«size» . И каждый из этих аргументов представлен своей функцией.

                           Функция inputString.

——————————————————————————————————
func inputString(begin string) string {
fmt.Print(begin)
msg, _ := bufio.NewReader(os.Stdin).ReadString('\n')
return strings.Replace(msg, "\n", "", 1)
}
——————————————————————————————————
Сначала выводит сообщение, которое указано в аргументе, как строка
приветствия для ввода. Далее читается строка из стандартного потока ввода до
обнаружения символа новой строки. В конце, из переменной msg удаляется
символ новой строки и возвращается полученное значение.

                             Функция userAddress.

——————————————————————————————————
func userAddress() {
fmt.Println("Address:", User.Address(), "\n")
}
——————————————————————————————————

                             Функция userPurse.

——————————————————————————————————
func userPurse() {
fmt.Println("Purse:", User.Purse(), "\n")
}
——————————————————————————————————

                            Функция userBalance.

——————————————————————————————————
func userBalance() {
printBalance(User.Address())
}
——————————————————————————————————
Использует функцию printBalance.

                            Функция chainPrint.

——————————————————————————————————
func chainPrint() {
for i := 0; ; i++ {
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_BLOCK,
Data: fmt.Sprintf("%d", i),
})
if res == nil || res.Data == ""{
break
}
fmt.Printf("[%d] => %s\n", i+1, res.Data)
}
fmt.Println()
}
——————————————————————————————————
Запрашивает у первого узла (в списке адресов) блоки, до тех пор, пока в
качестве ответа не будет обнаружена пустая строка. Используется константа
GET_BLOCK.

                               Функция chainTX.

——————————————————————————————————
func chainTX(splited []string) {
if len(splited) != 3 {
fmt.Println("failed: len(splited) != 3\n")
return
}
num, err := strconv.Atoi(splited[2])
if err != nil {
fmt.Println("failed: strconv.Atoi(num)\n")
return
}
for _, addr := range Addresses {
res := nt.Send(addr, &nt.Package{
Option: GET_LHASH,
})
if res == nil {
continue
}
tx := bc.NewTransaction(User, bc.Base64Decode(res.Data), splited[1],
uint64(num))
res = nt.Send(addr, &nt.Package{
Option: ADD_TRNSX,
Data: bc.SerializeTX(tx),
})
if res == nil {
continue
}
if res.Data == "ok" {
fmt.Printf("ok: (%s)\n", addr)
} else {
fmt.Printf("fail: (%s)\n", addr)
}
}
fmt.Println()
}
——————————————————————————————————
В качестве аргумента «tx» предполагается два аргумента - получатель и
отправляемые средства. Функция перебирает список адресов, отправляя при
этом сгенерированную транзакцию под их блокчейн (подстраиваясь под
последний хеш). Используются константы GET_LHASH и ADD_TRNSX.

                          Функция chainBalance.

——————————————————————————————————
func chainBalance(splited []string) {
if len(splited) != 2 {
fmt.Println("fail: len(splited) != 2\n")
return
}
printBalance(splited[1])
}
——————————————————————————————————

                           Функция chainBlock.

——————————————————————————————————
func chainBlock(splited []string) {
if len(splited) != 2 {
fmt.Println("failed: len(splited) != 2\n")
return
}
num, err := strconv.Atoi(splited[1])
if err != nil {
fmt.Println("failed: strconv.Atoi(num)\n")
return
}
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_BLOCK,
Data: fmt.Sprintf("%d", num-1),
})
if res == nil || res.Data == "" {
fmt.Println("failed: getBlock\n")
return
}
fmt.Printf("[%d] => %s\n", num, res.Data)
}
——————————————————————————————————

                          Функция chainSize.

——————————————————————————————————
func chainSize() {
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_CSIZE,
})
if res == nil || res.Data == "" {
fmt.Println("failed: getSize\n")
return
}
fmt.Printf("Size: %s blocks\n\n", res.Data)
}
——————————————————————————————————
Использует константу GET_CSIZE.

                         Функция printBalance.

——————————————————————————————————
func printBalance(useraddr string) {
for _, addr := range Addresses {
res := nt.Send(addr, &nt.Package{
Option: GET_BLNCE,
Data: useraddr,
})
if res == nil {
continue
}
fmt.Printf("Balance (%s): %s coins\n", addr, res.Data)
}
fmt.Println()
}
——————————————————————————————————
Перебирает список адресов, запрашивая у каждого текущий баланс,
относительно их блокчейна. Используется константа GET_BLNCE.

                         Константа GET_BLOCK.
                         Константа GET_LHASH.
                         Константа ADD_TRNSX.
                         Константа GET_CSIZE.
                         Константа GET_BLNCE.

——————————————————————————————————
const (
ADD_BLOCK = iota + 1
ADD_TRNSX
GET_BLOCK
GET_LHASH
GET_BLNCE
GET_CSIZE
)
——————————————————————————————————
Константа ADD_BLOCK необходима при реализации узла.

                              Функция main.

——————————————————————————————————
func main() {
handleClient()
}
——————————————————————————————————
Точка входа в программу.
Данное приложение использует следующие пакеты:
——————————————————————————————————
import (
bc "./blockchain"
nt "./network"
"bufio"
"encoding/json"
"io/ioutil"
"fmt"
"os"
"strconv"
"strings"
)
——————————————————————————————————

                                 Узел

Строение приложения узла очень схоже с клиентским приложением, даже
некоторые функции полностью одинаковы (в исходном коде такие функции
вынесены в файл «values.go»). Приложение узла имеет лишь часть с
инициализацией. Таким образом, сам узел способен лишь обрабатывать
информацию посылаемую клиентом, либо же другим узлом (при получении
блока) на автоматизированном уровне, без ручного вмешательства.
Для запуска приложения, следует использовать следующий пример:
——————————————————————————————————
./node -serve::8080 -loaduser:private.key -loadchain:chain.db -loadaddr:addr.json
——————————————————————————————————
Точно также, как для loaduser можно применить newuser (если
пользователь ещё не создан), для loadchain можно применить newchain.
Так как узел является исполняемым приложением, его пакет именуется
как «main».
——————————————————————————————————
package main
——————————————————————————————————

                               Функция init.

——————————————————————————————————
func init() {
if len(os.Args) < 2 {
panic("failed: len(os.Args) < 2")
}
var (
serveStr = ""
addrStr = ""
userNewStr = ""
userLoadStr = ""
chainNewStr = ""
chainLoadStr = ""
)
var (
serveExist = false
addrExist = false
userNewExist
= false
userLoadExist = false
chainNewExist = false
chainLoadExist = false
)
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
switch {
case strings.HasPrefix(arg, "-serve:"):
serveStr = strings.Replace(arg, "-serve:", "", 1)
serveExist = true
case strings.HasPrefix(arg, "-loadaddr:"):
addrStr = strings.Replace(arg, "-loadaddr:", "", 1)
addrExist = true
case strings.HasPrefix(arg, "-newuser:"):
userNewStr = strings.Replace(arg, "-newuser:", "", 1)
userNewExist = true
case strings.HasPrefix(arg, "-loaduser:"):
userLoadStr = strings.Replace(arg, "-loaduser:", "", 1)
userLoadExist = true
case strings.HasPrefix(arg, "-newchain:"):
chainNewStr = strings.Replace(arg, "-newchain:", "", 1)
chainNewExist = true
case strings.HasPrefix(arg, "-loadchain:"):
chainLoadStr = strings.Replace(arg, "-loadchain:", "", 1)
chainLoadExist = true
}
}
if
!(userNewExist || userLoadExist) || !(chainNewExist || chainLoadExist) ||
!serveExist || !addrExist {
panic("failed: !(userNewExist || userLoadExist)"+
"|| !(chainNewExist || chainLoadExist) || !serveExist ||
!addrExist")
}
Serve = serveStr
var addresses []string
err := json.Unmarshal([]byte(readFile(addrStr)), &addresses)
if err != nil {
panic("failed: load addresses")
}
var mapaddr = make(map[string]bool)
for _, addr := range addresses {
if addr == Serve {
continue
}
if _, ok := mapaddr[addr]; ok {
continue
}
mapaddr[addr] = true
Addresses = append(Addresses, addr)
}
if userNewExist {
User = userNew(userNewStr)
}
if userLoadExist {
User = userLoad(userLoadStr)
}
if User == nil {
panic("failed: load user")
}
if chainNewExist {
Filename = chainNewStr
Chain = chainNew(chainNewStr)
}
if chainLoadExist {
Filename = chainLoadStr
Chain = chainLoad(chainLoadStr)
}
if Chain == nil {
panic("failed: load chain")
}
Block = bc.NewBlock(User.Address(), Chain.LastHash())
}
——————————————————————————————————
Данная функция является более модифицированной версией, по
сравнению с клиентской. Здесь помимо указания адресов и приватного ключа,
необходимо указать файл локальной БД и адрес с портом на принятие данных.
Также используются глобальные переменные Addresses, User и
функции readFile, userNew, userLoad, как в клиентском
приложении. Помимо их, используются ещё такие переменные как Chain,
Block, Filename, Serve и функции chainNew, chainLoad.

                    Глобальная переменная Chain.
                    Глобальная переменная Block.
                    Глобальная переменная Filename.
                    Глобальная переменная Serve.

——————————————————————————————————
var (
Filename
string
Serve string
Chain *bc.BlockChain
Block *bc.Block
)
——————————————————————————————————

                            Функция chainNew.

——————————————————————————————————
func chainNew(filename string) *bc.BlockChain {
err := bc.NewChain(filename, User.Address())
if err != nil {
return nil
}
return bc.LoadChain(filename)
}
——————————————————————————————————

                            Функция chainLoad.

——————————————————————————————————
func chainLoad(filename string) *bc.BlockChain {
chain := bc.LoadChain(filename)
if chain == nil {
return nil
}
return chain
}
——————————————————————————————————

                           Функция main.

——————————————————————————————————
func main() {
nt.Listen(Serve, handleServer)
for {
fmt.Scanln()
}
}
——————————————————————————————————
Точка входа. Использует функцию handleServer.

                           Функция handleServer.

——————————————————————————————————
func handleServer(conn nt.Conn, pack *nt.Package) {
nt.Handle(ADD_BLOCK, conn, pack, addBlock)
nt.Handle(ADD_TRNSX, conn, pack, addTransaction)
nt.Handle(GET_BLOCK, conn, pack, getBlock)
nt.Handle(GET_LHASH, conn, pack, getLastHash)
nt.Handle(GET_BLNCE, conn, pack, getBalance)
nt.Handle(GET_CSIZE, conn, pack, getChainSize)
}
——————————————————————————————————
Использует константы указанные в клиентской части а также
функции addBlock, addTransaction, getBlock, getLastHash, getBalance, getChainSize.

                            Функция addBlock.

——————————————————————————————————
func addBlock(pack *nt.Package) string {
splited := strings.Split(pack.Data, SEPARATOR)
if len(splited) != 3 {
return "fail"
}
block := bc.DeserializeBlock(splited[2])
if !block.IsValid(Chain, Chain.Size()) {
currSize := Chain.Size()
num, err := strconv.Atoi(splited[1])
if err != nil {
return "fail"
}
if currSize < uint64(num) {
go compareChains(splited[0], uint64(num))
return "ok "
}
return "fail"
}
Mutex.Lock()
Chain.AddBlock(block)
Block = bc.NewBlock(User.Address(), Chain.LastHash())
Mutex.Unlock()
if IsMining {
BreakMining <- true
IsMining = false
}
return "ok"
}
——————————————————————————————————
Принимает в данных пакета строку формата:
address+SEPARATOR+chainSize+SEPARATOR+block, где address - это адрес
отправителя (узла), chainSize - размер блокчейна отправителя, block -
переданный блок для добавления, SEPARATOR - константа. Возвращает
строку «fail», в качестве ошибки, и строку «ok», в качестве успешного
выполнения. Далее функция проверяет, является ли блок валидным по
сравнению с существующим блокчейном. Если да, тогда блок добавляется в
блокчейн, блок обнуляется (очищается от транзакций) и если в это время
происходит майнинг, тогда майнинг прекращается и устанавливается
состояние «без майнинга». Если нет, тогда вычисляется текущий размер
блокчейна. Просматривается размер блокчейна отправителя-узла и если цепь
отправителя больше, тогда проверить его данные на корректность при помощи
функции compareChains. Используется глобальная переменная Mutex.

                        Функция addTransaction.

——————————————————————————————————
func addTransaction(pack *nt.Package) string {
var tx = bc.DeserializeTX(pack.Data)
if tx == nil || len(Block.Transactions) == bc.TXS_LIMIT {
return "fail"
}
Mutex.Lock()
err := Block.AddTransaction(Chain, tx)
Mutex.Unlock()
if err != nil {
return "fail"
}
if len(Block.Transactions) == bc.TXS_LIMIT {
go func() {
Mutex.Lock()
block := *Block
IsMining = true
Mutex.Unlock()
res := (&block).Accept(Chain, User, BreakMining)
Mutex.Lock()
IsMining = false
if res == nil && bytes.Equal(block.PrevHash, Block.PrevHash) {
Chain.AddBlock(&block)
pushBlockToNet(&block)
}
Block = bc.NewBlock(User.Address(), Chain.LastHash())
Mutex.Unlock()
}()
}
return "ok"
}
——————————————————————————————————
Сначала идёт проверка транзакции, является ли её структура валидной и
не превышен ли предел в блоке. Далее, если это последняя транзакция (после её
добавления количество транзакций в блоке равняется пределу), тогда запустить
параллельную функцию. В ней скопировать данные из глобальной переменной
Block в локальную переменную block. Указать, что происходит майнинг, далее
производить майнинг. После завершения майнинга, изменить переменную
IsMining на false. Проверить результат майнинга, вернул ли метод Accept
нулевой адрес и равны ли до сих пор хеши предыдущего блока в переменных
block и Block (нужно при случаях, когда блок замайнился у другой ноды
быстрее). Если всё успешно, тогда добавить блок в свой блокчейн и оповестить
все другие узлы, о том, что создался новый блок. После всех действий -
90обнулить блок. Данный код использует функцию pushBlockToNet для
оповещения и пересылки блока других узлам.

                            Функция getBlock.

——————————————————————————————————
func getBlock(pack *nt.Package) string {
num, err := strconv.Atoi(pack.Data)
if err != nil {
return ""
}
size := Chain.Size()
if uint64(num) < size {
return selectBlock(Chain, num)
}
return ""
}
——————————————————————————————————
Использует функцию selectBlock.

                           Функция getLastHash.

——————————————————————————————————
func getLastHash(pack *nt.Package) string {
return bc.Base64Encode(Chain.LastHash())
}
——————————————————————————————————

                           Функция getBalance.

——————————————————————————————————
func getBalance(pack *nt.Package) string {
return fmt.Sprintf("%d", Chain.Balance(pack.Data, Chain.Size()))
}
——————————————————————————————————

                           Функция getChainSize.

——————————————————————————————————
func getChainSize(pack *nt.Package) string {
return fmt.Sprintf("%d", Chain.Size())
}
——————————————————————————————————

                           Константа SEPARATOR.

——————————————————————————————————
const (
SEPARATOR = "SEPARATOR"
)
——————————————————————————————————

                        Глобальная переменная IsMining.
                        Глобальная переменная BreakMining.

——————————————————————————————————
var (
IsMining bool
BreakMining = make(chan bool)
)
——————————————————————————————————

                          Функция compareChains.

——————————————————————————————————
func compareChains(address string, num uint64) {
filename := "temp_" + hex.EncodeToString(bc.GenerateRandomBytes(8))
file, err := os.Create(filename)
if err != nil {
return
}
file.Close()
defer func() {
os.Remove(filename)
}()
res := nt.Send(address, &nt.Package{
Option: GET_BLOCK,
Data: fmt.Sprintf("%d", 0),
})
if res == nil {
return
}
genesis := bc.DeserializeBlock(res.Data)
if genesis == nil {
return
}
if !bytes.Equal(genesis.CurrHash, hashBlock(genesis)) {
return
}
db, err := sql.Open("sqlite3", filename)
if err != nil {
return
}
defer db.Close()
_, err = db.Exec(bc.CREATE_TABLE)
chain := &bc.BlockChain{
DB: db,
}
chain.AddBlock(genesis)
for i := uint64(1); i < num; i++ {
res := nt.Send(address, &nt.Package{
Option: GET_BLOCK,
Data: fmt.Sprintf("%d", i),
})
if res == nil {
return
}
block := bc.DeserializeBlock(res.Data)
if block == nil {
return
}
if !block.IsValid(chain, i) {
return
}
chain.AddBlock(block)
}
Mutex.Lock()
Chain.DB.Close()
os.Remove(Filename)
copyFile(filename, Filename)
Chain = bc.LoadChain(Filename)
Block = bc.NewBlock(User.Address(), Chain.LastHash())
Mutex.Unlock()
if IsMining {
BreakMining <- true
IsMining = false
}
}
——————————————————————————————————
Создаёт временный файл, запрашивает у узла генезис-блок, вносит
генезис-блок в файл, как в локальную БД. Далее, через цикл, запрашивает
блоки до указанного максимума (размера блокчейна ноды). Если все блоки
прошли проверку, тогда удалить текущий файл связанный с блокчейном и
заменить его на временный файл. Далее загрузить новый блокчейн и обнулить
блок. Проверить, запущен ли майнинг, и если да, тогда остановить его.
Используются функции hashBlock и copyFile.

                       Глобальная переменная Mutex.

——————————————————————————————————
var (
Mutex sync.Mutex
)
——————————————————————————————————
Необходима для блокировки параллельных частей кода, которые
изменяют состояние программы методом редактирования глобальных
переменных.

                           Функция pushBlockToNet.

——————————————————————————————————
func pushBlockToNet(block *bc.Block) {
var (
sblock = bc.SerializeBlock(block)
msg = Serve + SEPARATOR + fmt.Sprintf("%d", Chain.Size()) +
SEPARATOR + sblock
)
for _, addr := range Addresses {
go nt.Send(addr, &nt.Package{
Option: ADD_BLOCK,
Data: msg,
})
}
}
——————————————————————————————————

                           Функция selectBlock.

——————————————————————————————————
func selectBlock(chain *bc.BlockChain, i int) string {
var block string
row := chain.DB.QueryRow("SELECT Block FROM BlockChain WHERE Id=$1",
i+1)
row.Scan(&block)
return block
}
——————————————————————————————————

                           Функция hashBlock.

——————————————————————————————————
func hashBlock(block * bc.Block) []byte {
var tempHash []byte
for _, tx := range block.Transactions {
tempHash = bc.HashSum(bytes.Join(
[][]byte{
tempHash,
tx.CurrHash,
},
[]byte{},
))
}
var list []string
for hash := range block.Mapping {
list = append(list, hash)
}
sort.Strings(list)
for _, hash := range list {
tempHash = bc.HashSum(bytes.Join(
[][]byte{
tempHash,
[]byte(hash),
bc.ToBytes(block.Mapping[hash]),
},
[]byte{},
))
}
return bc.HashSum(bytes.Join(
[][]byte{
tempHash,
bc.ToBytes(uint64(block.Difficulty)),
block.PrevHash,
[]byte(block.Miner),
[]byte(block.TimeStamp),
},
[]byte{},
))
}
——————————————————————————————————

                           Функция copyFile.

——————————————————————————————————
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}
——————————————————————————————————
Данное приложение использует следующие пакеты:
——————————————————————————————————
import (
bc "./blockchain"
nt "./network"
"encoding/json"
"fmt"
"os"
"strings"
"io/ioutil"
"bytes"
"database/sql"
"encoding/hex"
_ "github.com/mattn/go-sqlite3"
"io"
"strconv"
"sync"
"sort "
)
——————————————————————————————————

                      Компиляция клиента и узла:

——————————————————————————————————
$ go build client.go
$ go build node.go
——————————————————————————————————
Теперь, чтобы проверить работоспособность клиента и узла, можно
запустить три процесса, два из которых будут представлять майнеров (или же
узлов) и один будет являться клиентом. Нумерация процессов будет начинаться
с единицы и номер будет указываться в фигурных скобках (например, третий
процесс = {3}).
Предполагается, что у всех пользователей уже имеется файл с адресами
узлов (addr.json).

              Запуск приложений выглядит следующим образом:

——————————————————————————————————
{1} ./node -serve::8080 -newuser:node1.key -newchain:chain1.db -loadaddr:addr.json
{2} ./node -serve::9090 -newuser:node2.key -newchain:chain2.db -loadaddr:addr.json
{3} ./client -loaduser:node1.key -loadaddr:addr.json
——————————————————————————————————
Из этих запусков видно, что используется дважды один и тот же файл с
приватным ключом (node1.key). Иными словами, пользователь, обладающий
данным приватным ключом, будет являться одновременно узлом, способным
майнить блоки, и клиентом, который будет создавать транзакции. Из этого
примера будет вытекать сразу же противоречие между двумя майнерами, так
как в одном блокчейне пользователь имеет баланс (так как он же является
создателем генезис-блока), а в другом его баланс равен нулю. Соответственно,
после майнинга нового блока (первым узлом) должен будет произойти soft fork,
который отклонит блокчейн у одного майнера и примет блокчейн второго
майнера. После этого, действия майнеров синхронизируются и соответственно
появится возможность одновременного майнинга на разных узлах.
Запустив все три процесса, только процесс связанный с клиентом {3}
отобразит ожидание ввода из терминала. Написав команду «/user balance»
можно просмотреть баланс текущего пользователя относительно блокчейна у
разных узлов*:
——————————————————————————————————

/user balance
Balance (:8080): 100 coins
Balance (:9090): 0 coins
——————————————————————————————————
В данном примере существует противоречие двух разных блокчейнов,
генезис-блоки которых имеют разных майнеров. Чтобы решить данное
противоречие, необходимо сделать один блокчейн больше другого.
Соответственно нужно первому узлу отправить транзакции, для их добавления
в блок и последующего его майнинга. Это можно осуществить командой
«/chain tx ^receiver^ ^value^».
——————————————————————————————————
/chain tx aaa 3
ok: (:8080)
fail: (:9090)
/chain tx bbb 2
ok: (:8080)
fail: (:9090)
——————————————————————————————————
Первый узел будет успешно принимать транзакции, в то время как
второй, их будет отвергать (из-за того, что по его блокчейну у пользователя
недостаточно средств для отправки средств).
Если посмотреть на процесс первого узла {1}, в момент занесения
последней транзакции, то можно увидеть, что он майнит блок.
Результат майнинга:
——————————————————————————————————
Mining: AAANWmdWf/TVO8BgO82ItoFObf1z9pdorinr4dd7yvg=
——————————————————————————————————
После майнинга, блокчейн двух узлов должен синхронизироваться.
Успешность «срастания» блокчейнов можно наблюдать со стороны клиента,
если он снова захочет узнать текущий свой баланс.
——————————————————————————————————
/user balance
Balance (:8080): 96 coins
Balance (:9090): 96 coins
——————————————————————————————————
Пользователь имел баланс равный 100 монетам, до создания блока.
Потратил он 5 монет (3 отправились пользователю «aaa», 2 отправились
пользователю «bbb»). Но так как он же и является майнером, ему из хранилища
передалась 1 монета за успешный майнинг. Таким же образом можно
посмотреть баланс самого хранилища, при помощи команды «/chain balance
STORAGE-CHAIN».
——————————————————————————————————
/chain balance STORAGE-CHAIN
Balance (:8080): 99 coins
Balance (:9090): 99 coins
——————————————————————————————————
После успешной синхронизации узлов появится неизбежно и
конкуренция за майнинг блока. Создадим ещё две транзакции для генерации
блока.
——————————————————————————————————
/chain tx qqq 1
ok: (:8080)
ok: (:9090)
/chain tx www 1
ok: (:8080)
ok: (:9090)
——————————————————————————————————
На этот раз, все два узла приняли транзакции и начали одновременный
майнинг. Это можно наблюдать в процессах {1} и {2}.
——————————————————————————————————
{1} Mining: WfJq7U4MI/ZyCAfFrLEIoX84HmoJaO9PnOkzI+8Hchw=
{2} Mining: AAABb7uK/R3/rDcCDel4o7S6m1vFDYS5UsJBIOAV0mU=
——————————————————————————————————
Ситуация «выигрыша» неопределена и носит вероятностный характер,
так как один узел может начать с более лучшей позиции значения nonce или
обладать большими вычислительными мощностями, чем другой узел. В данном
случае, в гонке за майнинг блока победил второй процесс {2}, при этом первый
процесс {1} сразу же прекратил вычисление хеша, как только принял
информацию о найденном хеше другим узлом.
Если посмотреть текущий баланс пользователя, то он будет равен 94
монетам, то-есть награду за майнинг данный пользователь не получил. При
этом, если посмотреть баланс майнера, который вычислил блок, то он будет
равен одной монете.

Помимо просмотра баланса (как своего, так и других пользователей) и
создания транзакций, можно посмотреть весь блокчейн при помощи команды
«/chain print».
——————————————————————————————————

/chain print
...
[3] => {
"Nonce": 3177602599,
"Difficulty": 20,
"CurrHash": "qcYXjw4xnhFt7KuDj5VFUYAevJ8cf0N0LtxDkYCDZ/k=",
"PrevHash": "uj3RWYXqgN7+4Hh0kGJa8MmU5BQ2AmcEBVOsePzrOGU=",
"Transactions": [
{
"RandBytes":
"m0dXqyVNL6lHztZTaZM4zgnMCzDPazNRo+UtGtGrhh0= ",
"PrevBlock":
"uj3RWYXqgN7+4Hh0kGJa8MmU5BQ2AmcEBVOsePzrOGU=",
"Sender":
"MEgCQQDY38h314lRok0tncoyk9tc/5UG0quwsuZZt50WzI/Xa6Nx0bErz5TX0+VMTx2l8FLW5
V5HRdEwSLCrZkbMeMIbAgMBAAE=",
"Receiver": "qqq",
"Value": 1,
"ToStorage": 0,
"CurrHash":
"2n9MXXM2j/awo/+aZHmx2vGz6o87VGicFSvkyl07JCg=",
"Signature":
"D6roOQ5/6lp8cW+xxOjS0Xip+9AzPeNOi/Aorml7sFmBtyuHyxWtD/25EeU2RKEpn2w1PduUP
y58iCosTA8ZAw=="
},
{
"RandBytes":
"3C72TAcATdyAcLzMYyQvzHnrXnK9lUC2zvGaGH7/BbA=",
"PrevBlock":
"uj3RWYXqgN7+4Hh0kGJa8MmU5BQ2AmcEBVOsePzrOGU=",
"Sender":
"MEgCQQDY38h314lRok0tncoyk9tc/5UG0quwsuZZt50WzI/Xa6Nx0bErz5TX0+VMTx2l8FLW5
V5HRdEwSLCrZkbMeMIbAgMBAAE=",
"Receiver": "www",
"Value": 1,
"ToStorage": 0,
"CurrHash":
"OUv6zdMAIXXUM5uCRvLSkL9Zw1stOk3BoUkDmSsqeG0=",
"Signature":
"nFKsei0rWYsRzoVcnS7GrOK76Jn48IFt56xNpkOOReYnmMGh2Vbz2kvkONe703z2arVi98chF
Ex2IdytBXlLhw=="
},
{
"RandBytes":
"M5ptIBYxjmd2vtY0eJORhqJVJFoeMZrewmS0c+VEHAI=",
"PrevBlock": null,
"Sender": "STORAGE-CHAIN",
"Receiver":
"MEgCQQDU6JaPgXMlugPTqNrcOqHj9WF5WEYC8IyEtSY8onEFIHeLiK8U3lrKZ9la3K6tuPD
mrat9s8qXR50gnZ8ngcOZAgMBAAE=",
"Value": 1,
"ToStorage": 0,
"CurrHash": null,
"Signature": null
}
],
"Mapping": {
"MEgCQQDU6JaPgXMlugPTqNrcOqHj9WF5WEYC8IyEtSY8onEFIHeLiK8U3lrKZ9la3
K6tuPDmrat9s8qXR50gnZ8ngcOZAgMBAAE=": 1,
"MEgCQQDY38h314lRok0tncoyk9tc/5UG0quwsuZZt50WzI/Xa6Nx0bErz5TX0+VMTx2l
8FLW5V5HRdEwSLCrZkbMeMIbAgMBAAE=": 94,
"STORAGE-CHAIN": 98,
"qqq": 1,
"www": 1
},
"Miner":
"MEgCQQDU6JaPgXMlugPTqNrcOqHj9WF5WEYC8IyEtSY8onEFIHeLiK8U3lrKZ9la3K6tuPD
mrat9s8qXR50gnZ8ngcOZAgMBAAE=",
"Signature":
"V0aX0zwR4CX+OmRWKPX5g4fPZ2hqDm0Kg204UUmiaXoE4PFEasl5EudRGP8MdQsjRcTT
xwQQuFUc681plf/s/w==",
"TimeStamp": "2021-01-15T18:17:20-04:00"
}
——————————————————————————————————
Вывод сократил до одного блока. В его состоянии можно как раз
наблюдать балансы пользователей, а в транзакциях награду за майнинг.
Можно также посмотреть и размер блокчейна при помощи команды
«/chain size».
——————————————————————————————————
/chain size
Size: 3 blocks
——————————————————————————————————
Если нужно узнать содержимое только одного блока, то можно
воспользоваться командой «/chain block». Индексация начинается с единицы.
——————————————————————————————————
/chain block 3
——————————————————————————————————
Чтобы выйти из клиентского приложения, можно воспользоваться
командой «/exit».
——————————————————————————————————
/exit
——————————————————————————————————

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

Posted Using Aeneas.Blog

Sort:  

Congratulations @mhuggu5hss! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s) :

You received more than 100 upvotes. Your next target is to reach 200 upvotes.
You received more than 10 as payout for your posts. Your next target is to reach a total payout of 50

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP